OSDN Git Service

total_time = 0 means no limit
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 ## -*-Ruby-*- $RCSfile$ $Revision$ $Name$
3
4 ## Copyright (C) 2004 nanami@2ch
5 ##
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
10 ##
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 ## GNU General Public License for more details.
15 ##
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, write to the Free Software
18 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 Max_Identifier_Length = 32
21 Default_Timeout = 60            # for single socket operation
22
23 Default_Game_Name = "default:1500:0"
24
25 One_Time = 10
26 Least_Time_Per_Move = 1
27 Watchdog_Time = 30              # time for ping
28 Login_Time = 300                # time for LOGIN
29
30 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
31 Release.concat("-") if (Release == "")
32 Revision = "$Revision$".gsub(/[^\.\d]/, '')
33
34 STDOUT.sync = true
35 STDERR.sync = true
36
37 require 'getoptlong'
38 require 'thread'
39 require 'timeout'
40 require 'socket'
41 require 'ping'
42
43 TCPSocket.do_not_reverse_lookup = true
44
45 class TCPSocket
46   def gets_timeout(t = Default_Timeout)
47     begin
48       timeout(t) do
49         return self.gets
50       end
51     rescue TimeoutError
52       return nil
53     rescue
54       return nil
55     end
56   end
57   def gets_safe(t = nil)
58     if (t && t > 0)
59       begin
60         timeout(t) do
61           return self.gets
62         end
63       rescue TimeoutError
64         return :timeout
65       rescue
66         return nil
67       end
68     else
69       begin
70         return self.gets
71       rescue
72         return nil
73       end
74     end
75   end
76   def write_safe(str)
77     begin
78       return self.write(str)
79     rescue
80       return nil
81     end
82   end
83 end
84
85
86 class League
87   def initialize
88     @games = Hash::new
89     @players = Hash::new
90     @event = nil
91   end
92   attr_accessor :players, :games, :event
93
94   def add(player)
95     @players[player.name] = player
96   end
97   def delete(player)
98     @players.delete(player.name)
99   end
100   def get_player(status, game_name, sente, searcher=nil)
101     @players.each do |name, player|
102       if ((player.status == status) &&
103           (player.game_name == game_name) &&
104           ((player.sente == nil) || (player.sente == sente)) &&
105           ((searcher == nil) || (player != searcher)))
106         return player
107       end
108     end
109     return nil
110   end
111 end
112
113 class Player
114   def initialize(str, socket)
115     @name = nil
116     @password = nil
117     @socket = socket
118     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game -> finished
119
120     @protocol = nil             # CSA or x1
121     @eol = "\m"                 # favorite eol code
122     @game = nil
123     @game_name = ""
124     @mytime = 0                 # set in start method also
125     @sente = nil
126     @watchdog_thread = nil
127     @main_thread = nil
128     login(str)
129   end
130
131   attr_accessor :name, :password, :socket, :status
132   attr_accessor :protocol, :eol, :game, :mytime, :main_thread, :watchdog_thread, :game_name, :sente
133   def kill
134     finish
135     Thread::kill(@main_thread) if @main_thread
136   end
137
138   def finish
139     if (@status != "finished")
140       @status = "finished"
141       log_message(sprintf("user %s finish", @name))    
142       Thread::kill(@watchdog_thread) if @watchdog_thread
143       @socket.close if (! @socket.closed?)
144     end
145   end
146
147   def watchdog(time)
148     while true
149       begin
150         Ping.pingecho(@socket.addr[3])
151       rescue
152       end
153       sleep(time)
154     end
155   end
156
157   def to_s
158     if ((status == "game_waiting") ||
159         (status == "agree_waiting") ||
160         (status == "game"))
161       if (@sente)
162         return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
163       elsif (@sente == false)
164         return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
165       elsif (@sente == nil)
166         return sprintf("%s %s %s %s +-", @name, @protocol, @status, @game_name)
167       end
168     else
169       return sprintf("%s %s %s", @name, @protocol, @status)
170     end
171   end
172
173   def write_help
174     @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
175   end
176
177   def write_safe(str)
178     @socket.write_safe(str.gsub(/[\r\n]+/, @eol))
179   end
180
181   def login(str)
182     str =~ /([\r\n]*)$/
183     @eol = $1
184     str.chomp!
185     (login, @name, @password, ext) = str.split
186     if (ext)
187       @protocol = "x1"
188     else
189       @protocol = "CSA"
190     end
191     @main_thread = Thread::current
192     @watchdog_thread = Thread::start do
193       watchdog(Watchdog_Time)
194     end
195   end
196     
197   def run
198     write_safe(sprintf("LOGIN:%s OK\n", @name))
199     if (@protocol != "CSA")
200       log_message(sprintf("user %s run in %s mode", @name, @protocol))
201       write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
202     else
203       log_message(sprintf("user %s run in CSA mode", @name))
204       csa_1st_str = "%%GAME #{Default_Game_Name} +-"
205     end
206     
207     while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
208       begin
209         $mutex.lock
210         if (csa_1st_str)
211           str = csa_1st_str
212           csa_1st_str = nil
213         end
214         if (@status == "finished")
215           return
216         end
217         str.chomp! if (str.class == String)
218         case str
219         when /^[\+\-%][^%]/, :timeout
220           if (@status == "game")
221             s = @game.handle_one_move(str, self)
222             return if (s && @protocol == "CSA")
223           end
224         when /^REJECT/
225           if (@status == "agree_waiting")
226             @game.reject(@name)
227             return if (@protocol == "CSA")
228           else
229             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
230           end
231         when /^AGREE/
232           if (@status == "agree_waiting")
233             @status = "start_waiting"
234             if ((@game.sente.status == "start_waiting") &&
235                 (@game.gote.status == "start_waiting"))
236               @game.start
237               @game.sente.status = "game"
238               @game.gote.status = "game"
239             end
240           else
241             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
242           end
243         when /^%%HELP/
244           write_help
245         when /^%%GAME\s+(\S+)\s+([\+\-]+)$/
246           game_name = $1
247           sente_str = $2
248           if (! good_game_name?(game_name))
249             write_safe(sprintf("##[ERROR] bad game name\n"))
250           elsif ((@status == "connected") || (@status == "game_waiting"))
251             @status = "game_waiting"
252           else
253             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
254           end
255           @status = "game_waiting"
256           @game_name = $1
257           if (sente_str == "+")
258             @sente = true
259             rival_sente = false
260           elsif (sente_str == "-")
261             @sente = false
262             rival_sente = true
263           else
264             @sente = nil
265             rival_sente = nil
266           end
267           rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self)
268           rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival)
269           if (rival)
270             if (@sente == nil)
271               if (rand(2) == 0)
272                 @sente = true
273                 rival_sente = false
274               else
275                 @sente = false
276                 rival_sente = true
277               end
278             elsif (rival_sente == nil)
279               if (@sente)
280                 rival_sente = false
281               else
282                 rival_sente = true
283               end
284             end
285             rival.sente = rival_sente
286             Game::new(@game_name, self, rival)
287             self.status = "agree_waiting"
288             rival.status = "agree_waiting"
289           end
290         when /^%%CHAT\s+(.+)/
291           message = $1
292           LEAGUE.players.each do |name, player|
293             if (player.protocol != "CSA")
294               s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
295               player.status = "zombie" if (! s)
296             end
297           end
298         when /^%%LIST/
299           buf = Array::new
300           LEAGUE.games.each do |id, game|
301             buf.push(sprintf("##[LIST] %s\n", id))
302           end
303           buf.push("##[LIST] +OK\n")
304           write_safe(buf.join)
305         when /^%%SHOW\s+(\S+)/
306           id = $1
307           if (LEAGUE.games[id])
308             write_safe(LEAGUE.games[id].board.to_s.gsub(/^/, '##[SHOW] '))
309           end
310           write_safe("##[SHOW] +OK\n")
311         when /^%%WHO/
312           buf = Array::new
313           LEAGUE.players.each do |name, player|
314             buf.push(sprintf("##[WHO] %s\n", player.to_s))
315           end
316           buf.push("##[WHO] +OK\n")
317           write_safe(buf.join)
318         when /^LOGOUT/
319           @status = "connected"
320           write_safe("LOGOUT:completed\n")
321           return
322         else
323           write_safe(sprintf("##[ERROR] unknown command %s\n", str))
324         end
325       ensure
326         $mutex.unlock
327       end
328     end                         # enf of while
329   end
330 end
331
332 class Piece
333   PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
334   def initialize(name, sente)
335     @name = name
336     @sente = sente
337     @promoted = false
338   end
339   attr_accessor :name, :promoted, :sente
340
341   def promoted_name
342     PROMOTE[name]
343   end
344
345   def to_s
346     if (@sente)
347       sg = "+"
348     else
349       sg = "-"
350     end
351     if (@promoted)
352       n = PROMOTE[@name]
353     else
354       n = @name
355     end
356     return sg + n
357   end
358 end
359
360
361
362 class Board
363   def initialize
364     @sente_hands = Array::new
365     @gote_hands = Array::new
366     @array = [[], [], [], [], [], [], [], [], [], []]
367   end
368   attr_accessor :array, :sente_hands, :gote_hands
369
370   def initial
371     @array[1][1] = Piece::new("KY", false)
372     @array[2][1] = Piece::new("KE", false)
373     @array[3][1] = Piece::new("GI", false)
374     @array[4][1] = Piece::new("KI", false)
375     @array[5][1] = Piece::new("OU", false)
376     @array[6][1] = Piece::new("KI", false)
377     @array[7][1] = Piece::new("GI", false)
378     @array[8][1] = Piece::new("KE", false)
379     @array[9][1] = Piece::new("KY", false)
380     @array[2][2] = Piece::new("KA", false)
381     @array[8][2] = Piece::new("HI", false)
382     @array[1][3] = Piece::new("FU", false)
383     @array[2][3] = Piece::new("FU", false)
384     @array[3][3] = Piece::new("FU", false)
385     @array[4][3] = Piece::new("FU", false)
386     @array[5][3] = Piece::new("FU", false)
387     @array[6][3] = Piece::new("FU", false)
388     @array[7][3] = Piece::new("FU", false)
389     @array[8][3] = Piece::new("FU", false)
390     @array[9][3] = Piece::new("FU", false)
391
392     @array[1][9] = Piece::new("KY", true)
393     @array[2][9] = Piece::new("KE", true)
394     @array[3][9] = Piece::new("GI", true)
395     @array[4][9] = Piece::new("KI", true)
396     @array[5][9] = Piece::new("OU", true)
397     @array[6][9] = Piece::new("KI", true)
398     @array[7][9] = Piece::new("GI", true)
399     @array[8][9] = Piece::new("KE", true)
400     @array[9][9] = Piece::new("KY", true)
401     @array[2][8] = Piece::new("HI", true)
402     @array[8][8] = Piece::new("KA", true)
403     @array[1][7] = Piece::new("FU", true)
404     @array[2][7] = Piece::new("FU", true)
405     @array[3][7] = Piece::new("FU", true)
406     @array[4][7] = Piece::new("FU", true)
407     @array[5][7] = Piece::new("FU", true)
408     @array[6][7] = Piece::new("FU", true)
409     @array[7][7] = Piece::new("FU", true)
410     @array[8][7] = Piece::new("FU", true)
411     @array[9][7] = Piece::new("FU", true)
412   end
413
414   def get_piece_from_hands(hands, name)
415     p = hands.find { |i|
416       i.name == name
417     }
418     if (p)
419       hands.delete(p)
420     end
421     return p
422   end
423
424   def handle_one_move(str)
425     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
426       p = $1
427       x0 = $2.to_i
428       y0 = $3.to_i
429       x1 = $4.to_i
430       y1 = $5.to_i
431       name = $6
432     elsif (str =~ /^%KACHI/)
433       return "kachi"
434     elsif (str =~ /^%TORYO/)
435       return "toryo"
436     end
437
438     if (p == "+")
439       sente = true
440       hands = @sente_hands
441     else
442       sente = false
443       hands = @gote_hands
444     end
445     if (@array[x1][y1])
446       if (@array[x1][y1] == sente) # this is mine
447         return "illegal"
448       elsif (@array[x1][y1].name == "OU")
449         return "ootori"
450       end
451       hands.push(@array[x1][y1])
452       @array[x1][y1] = nil
453     end
454     if ((x0 == 0) && (y0 == 0))
455       p = get_piece_from_hands(hands, name)
456       return "illegal" if (! p)     # i don't have this one
457       @array[x1][y1] = p
458       p.sente = sente
459       p.promoted = false
460     else
461       @array[x1][y1] = @array[x0][y0]
462       @array[x0][y0] = nil
463       if (@array[x1][y1].name != name) # promoted ?
464         return "illegal" if (@array[x1][y1].promoted_name != name) # can't promote
465         @array[x1][y1].promoted = true
466       end
467     end
468     return "normal"             # legal move
469   end
470
471   def to_s
472     a = Array::new
473     y = 1
474     while (y <= 9)
475       a.push(sprintf("P%d", y))
476       x = 9
477       while (x >= 1)
478         piece = @array[x][y]
479         if (piece)
480           s = piece.to_s
481         else
482           s = " * "
483         end
484         a.push(s)
485         x = x - 1
486       end
487       a.push(sprintf("\n"))
488       y = y + 1
489     end
490     if (! sente_hands.empty?)
491       a.push("P+")
492       sente_hands.each do |p|
493         a.push("00" + p.name)
494       end
495       a.push("\n")
496     end
497     if (! gote_hands.empty?)
498       a.push("P-")
499       gote_hands.each do |p|
500         a.push("00" + p.name)
501       end
502       a.push("\n")
503     end
504     return a.join
505   end
506 end
507
508 class Game
509   def initialize(game_name, player0, player1)
510     @game_name = game_name
511     if (@game_name =~ /:(\d+):(\d+)/)
512       @total_time = $1.to_i
513       @byoyomi = $2.to_i
514     end
515
516     if (player0.sente)
517       @sente = player0
518       @gote = player1
519     else
520       @sente = player1
521       @gote = player0
522     end
523     @current_player = @sente
524     @next_player = @gote
525
526     @sente.game = self
527     @gote.game = self
528
529     @sente.status = "agree_waiting"
530     @gote.status = "agree_waiting"
531     @id = sprintf("%s+%s+%s+%s+%s", 
532                   LEAGUE.event, @game_name, @sente.name, @gote.name,
533                   Time::new.strftime("%Y%m%d%H%M%S"))
534
535     LEAGUE.games[@id] = self
536
537
538     log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name))
539
540     @logfile = @id + ".csa"
541     @board = Board::new
542     @board.initial
543     @start_time = nil
544     @fh = nil
545
546     propose
547   end
548   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh
549
550   def reject(rejector)
551     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
552     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
553     finish
554   end
555
556   def kill(killer)
557     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
558       reject(killer.name)
559     elsif (@current_player == killer)
560       abnormal_lose()
561     end
562   end
563
564   def finish
565     log_message(sprintf("game finished %s %s %s", game_name, sente.name, gote.name))
566     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
567     @fh.close
568
569     @sente.game = nil
570     @gote.game = nil
571     @sente.status = "connected"
572     @gote.status = "connected"
573
574     if (@current_player.protocol == "CSA")
575       @current_player.finish
576     end
577     if (@next_player.protocol == "CSA")
578       @next_player.finish
579     end
580     LEAGUE.games.delete(@id)
581   end
582
583   def handle_one_move(str, player)
584     finish_flag = true
585     if (@current_player == player)
586       @end_time = Time::new
587       t = @end_time - @start_time
588       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
589       
590       move_status = nil
591       if ((@current_player.mytime - t <= 0) && (@total_time > 0))
592         status = :timeout
593       elsif (str == :timeout)
594         return false            # time isn't expired. players aren't swapped. continue game
595       else
596         move_status = @board.handle_one_move(str)
597         if (move_status == "normal")
598           @sente.write_safe(sprintf("%s,T%d\n", str, t))
599           @gote.write_safe(sprintf("%s,T%d\n", str, t))
600           @fh.printf("%s\nT%d\n", str, t)
601         else
602           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
603         end
604       end
605
606       if (@current_player.mytime - t < @byoyomi)
607         @current_player.mytime = @byoyomi
608       else
609         @current_player.mytime = @current_player.mytime - t
610       end
611
612       if (@next_player.status != "game") # rival is logout or disconnected
613         abnormal_win()
614       elsif (status == :timeout)
615         timeout_lose()
616       elsif (move_status == "illegal")
617         illegal_lose()
618       elsif (move_status == "kachi")
619         kachi_win()
620       elsif (move_status == "toryo")
621         toryo_lose()
622       elsif (move_status == "ootori")
623         ootori_win()
624       end
625       (@current_player, @next_player) = [@next_player, @current_player]
626       @start_time = Time::new
627       return finish_flag
628     end
629   end
630
631   def abnormal_win
632     @current_player.status = "connected"
633     @next_player.status = "connected"
634     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
635     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
636   end
637
638   def abnormal_lose
639     @current_player.status = "connected"
640     @next_player.status = "connected"
641     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
642     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
643   end
644
645   def illegal_lose
646     @current_player.status = "connected"
647     @next_player.status = "connected"
648     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
649     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
650   end
651
652   def timeout_lose
653     @current_player.status = "connected"
654     @next_player.status = "connected"
655     @current_player.write_safe("#TIME_UP\n#LOSE\n")
656     @next_player.write_safe("#TIME_UP\n#WIN\n")
657   end
658
659   def kachi_win
660     @current_player.status = "connected"
661     @next_player.status = "connected"
662     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
663     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
664   end
665
666   def toryo_lose
667     @current_player.status = "connected"
668     @next_player.status = "connected"
669     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
670     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
671   end
672
673   def ootori_win
674     @current_player.status = "connected"
675     @next_player.status = "connected"
676     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
677     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
678   end
679
680   def start
681     log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name))
682     @sente.write_safe(sprintf("START:%s\n", @id))
683     @gote.write_safe(sprintf("START:%s\n", @id))
684     @sente.mytime = @total_time
685     @gote.mytime = @total_time
686     @start_time = Time::new
687   end
688
689   def propose
690     begin
691       @fh = open(@logfile, "w")
692       @fh.sync = true
693
694       @fh.printf("V2\n")
695       @fh.printf("N+%s\n", @sente.name)
696       @fh.printf("N-%s\n", @gote.name)
697       @fh.printf("$EVENT:%s\n", @id)
698
699       @sente.write_safe(propose_message("+"))
700       @gote.write_safe(propose_message("-"))
701
702       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
703       @fh.print <<EOM
704 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
705 P2 * -HI *  *  *  *  * -KA *
706 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
707 P4 *  *  *  *  *  *  *  *  *
708 P5 *  *  *  *  *  *  *  *  *
709 P6 *  *  *  *  *  *  *  *  *
710 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
711 P8 * +KA *  *  *  *  * +HI *
712 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
713 +
714 EOM
715     end
716   end
717
718   def propose_message(sg_flag)
719     str = <<EOM
720 BEGIN Game_Summary
721 Protocol_Version:1.0
722 Protocol_Mode:Server
723 Format:Shogi 1.0
724 Game_ID:#{@id}
725 Name+:#{@sente.name}
726 Name-:#{@gote.name}
727 Your_Turn:#{sg_flag}
728 Rematch_On_Draw:NO
729 To_Move:+
730 BEGIN Time
731 Time_Unit:1sec
732 Total_Time:#{@total_time}
733 Byoyomi:#{@byoyomi}
734 Least_Time_Per_Move:#{Least_Time_Per_Move}
735 END Time
736 BEGIN Position
737 Jishogi_Declaration:1.1
738 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
739 P2 * -HI *  *  *  *  * -KA *
740 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
741 P4 *  *  *  *  *  *  *  *  *
742 P5 *  *  *  *  *  *  *  *  *
743 P6 *  *  *  *  *  *  *  *  *
744 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
745 P8 * +KA *  *  *  *  * +HI *
746 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
747 P+
748 P-
749 +
750 END Position
751 END Game_Summary
752 EOM
753     return str
754   end
755 end
756
757 def usage
758     print <<EOM
759 NAME
760         shogi-server - server for CSA server protocol
761
762 SYNOPSIS
763         shogi-server event_name port_number
764
765 DESCRIPTION
766         server for CSA server protocol
767
768 OPTIONS
769         --pid-file file
770                 specify filename for logging process ID
771
772 LICENSE
773         this file is distributed under GPL version2 and might be compiled by Exerb
774
775 SEE ALSO
776
777 RELEASE
778         #{Release}
779
780 REVISION
781         #{Revision}
782 EOM
783 end
784
785 def log_message(str)
786   printf("%s message: %s\n", Time::new.to_s, str)
787 end
788
789 def log_warning(str)
790   printf("%s message: %s\n", Time::new.to_s, str)
791 end
792
793 def log_error(str)
794   printf("%s error: %s\n", Time::new.to_s, str)
795 end
796
797
798 def parse_command_line
799   options = Hash::new
800   parser = GetoptLong.new
801   parser.ordering = GetoptLong::REQUIRE_ORDER
802   parser.set_options(
803                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
804
805   parser.quiet = true
806   begin
807     parser.each_option do |name, arg|
808       name.sub!(/^--/, '')
809       options[name] = arg.dup
810     end
811   rescue
812     usage
813     raise parser.error_message
814   end
815   return options
816 end
817
818 LEAGUE = League::new
819
820 def good_game_name?(str)
821   if ((str =~ /^(.+):\d+:\d+$/) &&
822       (good_identifier?($1)))
823     return true
824   else
825     return false
826   end
827 end
828
829 def good_identifier?(str)
830   if ((str =~ /\A[\w\d_@\-\.]+\z/) &&
831       (str.length < Max_Identifier_Length))
832     return true
833   else
834     return false
835   end
836 end
837
838 def good_login?(str)
839   tokens = str.split
840   if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
841       (tokens[0] == "LOGIN") &&
842       (good_identifier?(tokens[1])))
843     return true
844   else
845     return false
846   end
847 end
848
849 def  write_pid_file(file)
850   open(file, "w") do |fh|
851     fh.print Process::pid, "\n"
852   end
853 end
854
855 def main
856   $mutex = Mutex::new
857   $options = parse_command_line
858   if (ARGV.length != 2)
859     usage
860     exit 2
861   end
862
863   LEAGUE.event = ARGV.shift
864   port = ARGV.shift
865
866   write_pid_file($options["pid-file"]) if ($options["pid-file"])
867
868
869   Thread.abort_on_exception = true
870
871   server = TCPserver.open(port)
872   log_message("server started")
873
874   while true
875     Thread::start(server.accept) do |client|
876       client.sync = true
877       player = nil
878       while (str = client.gets_timeout(Login_Time))
879         begin
880           $mutex.lock
881           Thread::kill(Thread::current) if (! str) # disconnected
882           str =~ /([\r\n]*)$/
883           eol = $1
884           if (good_login?(str))
885             player = Player::new(str, client)
886             if (LEAGUE.players[player.name])
887               if ((LEAGUE.players[player.name].password == player.password) &&
888                   (LEAGUE.players[player.name].status != "game"))
889                 log_message(sprintf("user %s login forcely", player.name))
890                 LEAGUE.players[player.name].kill
891               else
892                 client.write_safe("LOGIN:incorrect" + eol)
893                 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
894                 client.close
895                 Thread::kill(Thread::current)
896               end
897             end
898             LEAGUE.add(player)
899             break
900           else
901             client.write_safe("LOGIN:incorrect" + eol)
902             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
903           end
904         ensure
905           $mutex.unlock
906         end
907       end                       # login loop
908       if (! player)
909         client.close
910         Thread::kill(Thread::current)
911       end
912       log_message(sprintf("user %s login", player.name))
913       player.run
914       begin
915         $mutex.lock
916         if (player.game)
917           player.game.kill(player)
918         end
919         player.finish
920         LEAGUE.delete(player)
921         log_message(sprintf("user %s logout", player.name))
922       ensure
923         $mutex.unlock
924       end
925     end
926   end
927 end
928
929 if ($0 == __FILE__)
930   main
931 end