OSDN Git Service

00a7bad77e5b6bcf8a5fd504a32228f1a7cfa092
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 ## $Id$
3
4 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
5 ## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
6 ##
7 ## This program is free software; you can redistribute it and/or modify
8 ## it under the terms of the GNU General Public License as published by
9 ## the Free Software Foundation; either version 2 of the License, or
10 ## (at your option) any later version.
11 ##
12 ## This program is distributed in the hope that it will be useful,
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 ## GNU General Public License for more details.
16 ##
17 ## You should have received a copy of the GNU General Public License
18 ## along with this program; if not, write to the Free Software
19 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 require 'getoptlong'
22 require 'thread'
23 require 'timeout'
24 require 'socket'
25 require 'yaml'
26 require 'yaml/store'
27 require 'digest/md5'
28 require 'webrick'
29 require 'fileutils'
30
31
32 class TCPSocket
33   def gets_timeout(t = Default_Timeout)
34     begin
35       timeout(t) do
36         return self.gets
37       end
38     rescue TimeoutError
39       return nil
40     rescue
41       return nil
42     end
43   end
44   def gets_safe(t = nil)
45     if (t && t > 0)
46       begin
47         return :exception if closed?
48         timeout(t) do
49           return self.gets
50         end
51       rescue TimeoutError
52         return :timeout
53       rescue Exception => ex
54         log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
55         return :exception
56       end
57     else
58       begin
59         return nil if closed?
60         return self.gets
61       rescue
62         return nil
63       end
64     end
65   end
66   def write_safe(str)
67     begin
68       return self.write(str)
69     rescue
70       return nil
71     end
72   end
73 end
74
75
76 module ShogiServer # for a namespace
77
78 Max_Write_Queue_Size = 1000
79 Max_Identifier_Length = 32
80 Default_Timeout = 60            # for single socket operation
81
82 Default_Game_Name = "default-1500-0"
83
84 One_Time = 10
85 Least_Time_Per_Move = 1
86 Login_Time = 300                # time for LOGIN
87
88 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
89 Release.concat("-") if (Release == "")
90 Revision = "$Revision$".gsub(/[^\.\d]/, '')
91
92 class League
93
94   class Floodgate
95     class << self
96       def game_name?(str)
97         return /^floodgate-\d+-\d+$/.match(str) ? true : false
98       end
99     end
100
101     def initialize(league)
102       @league = league
103       @next_time = nil
104       charge
105     end
106
107     def run
108       @thread = Thread.new do
109         Thread.pass
110         while (true)
111           begin
112             sleep(10)
113             next if Time.now < @next_time
114             @league.reload
115             match_game
116             charge
117           rescue Exception => ex 
118             # ignore errors
119             log_error("[in Floodgate's thread] #{ex}")
120           end
121         end
122       end
123     end
124
125     def shutdown
126       @thread.kill if @thread
127     end
128
129     # private
130
131     def charge
132       now = Time.now
133       if now.min < 30
134         @next_time = Time.mktime(now.year, now.month, now.day, now.hour, 30)
135       else
136         @next_time = Time.mktime(now.year, now.month, now.day, now.hour) + 3600
137       end
138       # for test
139       # if now.sec < 30
140       #   @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
141       # else
142       #   @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60
143       # end
144     end
145
146     def match_game
147       players = @league.find_all_players do |pl|
148         pl.status == "game_waiting" &&
149         Floodgate.game_name?(pl.game_name) &&
150         pl.sente == nil
151       end
152       load File.join(File.dirname(__FILE__), "pairing.rb")
153       Pairing.default_pairing.match(players)
154     end
155   end # class Floodgate
156
157   def initialize
158     @mutex = Mutex.new # guard @players
159     @games = Hash::new
160     @players = Hash::new
161     @event = nil
162     @dir = File.dirname(__FILE__)
163     @floodgate = Floodgate.new(self)
164     @floodgate.run
165   end
166   attr_accessor :players, :games, :event, :dir
167
168   def shutdown
169     @mutex.synchronize do
170       @players.each {|a| save(a)}
171     end
172     @floodgate.shutdown
173   end
174
175   # this should be called just after instanciating a League object.
176   def setup_players_database
177     @db = YAML::Store.new(File.join(@dir, "players.yaml"))
178   end
179
180   def add(player)
181     self.load(player) if player.id
182     @mutex.synchronize do
183       @players[player.name] = player
184     end
185   end
186   
187   def delete(player)
188     @mutex.synchronize do
189       save(player)
190       @players.delete(player.name)
191     end
192   end
193
194   def reload
195     @mutex.synchronize do
196       @players.each {|player| load(player)}
197     end
198   end
199
200   def find_all_players
201     found = nil
202     @mutex.synchronize do
203       found = @players.find_all do |name, player|
204         yield player
205       end
206     end
207     return found.map {|a| a.last}
208   end
209   
210   def get_player(status, game_name, sente, searcher)
211     found = nil
212     @mutex.synchronize do
213       found = @players.find do |name, player|
214         (player.status == status) &&
215         (player.game_name == game_name) &&
216         ( (sente == nil) || 
217           (player.sente == nil) || 
218           (player.sente == sente) ) &&
219         (player.name != searcher.name)
220       end
221     end
222     return found ? found.last : nil
223   end
224   
225   def load(player)
226     hash = search(player.id)
227     return unless hash
228
229     # a current user
230     player.name          = hash['name']
231     player.rate          = hash['rate'] || 0
232     player.modified_at   = hash['last_modified']
233     player.rating_group  = hash['rating_group']
234     player.win           = hash['win']  || 0
235     player.loss          = hash['loss'] || 0
236     player.last_game_win = hash['last_game_win'] || false
237   end
238
239   def save(player)
240     @db.transaction do
241       break unless  @db["players"]
242       @db["players"].each do |group, players|
243         hash = players[player.id]
244         if hash
245           hash['last_game_win'] = player.last_game_win
246           break
247         end
248       end
249     end
250   end
251
252   def search(id)
253     hash = nil
254     @db.transaction(true) do
255       break unless  @db["players"]
256       @db["players"].each do |group, players|
257         hash = players[id]
258         break if hash
259       end
260     end
261     hash
262   end
263
264   def rated_players
265     players = []
266     @db.transaction(true) do
267       break unless  @db["players"]
268       @db["players"].each do |group, players_hash|
269         players << players_hash.keys
270       end
271     end
272     return players.flatten.collect do |id|
273       p = BasicPlayer.new
274       p.id = id
275       self.load(p)
276       p
277     end
278   end
279 end
280
281
282 ######################################################
283 # Processes the LOGIN command.
284 #
285 class Login
286   def Login.good_login?(str)
287     tokens = str.split
288     if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
289         (tokens[0] == "LOGIN") &&
290         (good_identifier?(tokens[1])))
291       return true
292     else
293       return false
294     end
295   end
296
297   def Login.good_game_name?(str)
298     if ((str =~ /^(.+)-\d+-\d+$/) && (good_identifier?($1)))
299       return true
300     else
301       return false
302     end
303   end
304
305   def Login.good_identifier?(str)
306     if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
307       return true
308     else
309       return false
310     end
311   end
312
313   def Login.factory(str, player)
314     (login, player.name, password, ext) = str.chomp.split
315     if (ext)
316       return Loginx1.new(player, password)
317     else
318       return LoginCSA.new(player, password)
319     end
320   end
321
322   attr_reader :player
323   
324   # the first command that will be executed just after LOGIN.
325   # If it is nil, the default process will be started.
326   attr_reader :csa_1st_str
327
328   def initialize(player, password)
329     @player = player
330     @csa_1st_str = nil
331     parse_password(password)
332   end
333
334   def process
335     @player.write_safe(sprintf("LOGIN:%s OK\n", @player.name))
336     log_message(sprintf("user %s run in %s mode", @player.name, @player.protocol))
337   end
338
339   def incorrect_duplicated_player(str)
340     @player.write_safe("LOGIN:incorrect\n")
341     @player.write_safe(sprintf("username %s is already connected\n", @player.name)) if (str.split.length >= 4)
342     sleep 3 # wait for sending the above messages.
343     @player.name = "%s [duplicated]" % [@player.name]
344     @player.finish
345   end
346 end
347
348 ######################################################
349 # Processes LOGIN for the CSA standard mode.
350 #
351 class LoginCSA < Login
352   PROTOCOL = "CSA"
353
354   def initialize(player, password)
355     @gamename = nil
356     super
357     @player.protocol = PROTOCOL
358   end
359
360   def parse_password(password)
361     if Login.good_game_name?(password)
362       @gamename = password
363       @player.set_password(nil)
364     elsif password.split(",").size > 1
365       @gamename, *trip = password.split(",")
366       @player.set_password(trip.join(","))
367     else
368       @player.set_password(password)
369       @gamename = Default_Game_Name
370     end
371     @gamename = self.class.good_game_name?(@gamename) ? @gamename : Default_Game_Name
372   end
373
374   def process
375     super
376     @csa_1st_str = "%%GAME #{@gamename} *"
377   end
378 end
379
380 ######################################################
381 # Processes LOGIN for the extented mode.
382 #
383 class Loginx1 < Login
384   PROTOCOL = "x1"
385
386   def initialize(player, password)
387     super
388     @player.protocol = PROTOCOL
389   end
390   
391   def parse_password(password)
392     @player.set_password(password)
393   end
394
395   def process
396     super
397     @player.write_safe(sprintf("##[LOGIN] +OK %s\n", PROTOCOL))
398   end
399 end
400
401
402 class BasicPlayer
403   def initialize
404     @id = nil
405     @name = nil
406     @password = nil
407     @last_game_win = false
408   end
409
410   # Idetifier of the player in the rating system
411   attr_accessor :id
412
413   # Name of the player
414   attr_accessor :name
415   
416   # Password of the player, which does not include a trip
417   attr_accessor :password
418
419   # Score in the rating sysem
420   attr_accessor :rate
421
422   # Number of games for win and loss in the rating system
423   attr_accessor :win, :loss
424   
425   # Group in the rating system
426   attr_accessor :rating_group
427
428   # Last timestamp when the rate was modified
429   attr_accessor :modified_at
430
431   # Whether win the previous game or not
432   attr_accessor :last_game_win
433
434   def modified_at
435     @modified_at || Time.now
436   end
437
438   def rate=(new_rate)
439     if @rate != new_rate
440       @rate = new_rate
441       @modified_at = Time.now
442     end
443   end
444
445   def rated?
446     @id != nil
447   end
448
449   def last_game_win?
450     return @last_game_win
451   end
452
453   def simple_id
454     if @trip
455       simple_name = @name.gsub(/@.*?$/, '')
456       "%s+%s" % [simple_name, @trip[0..8]]
457     else
458       @name
459     end
460   end
461
462   ##
463   # Parses str in the LOGIN command, sets up @id and @trip
464   #
465   def set_password(str)
466     if str && !str.empty?
467       @password = str.strip
468       @id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
469     else
470       @id = @password = nil
471     end
472   end
473 end
474
475
476 class Player < BasicPlayer
477   def initialize(str, socket)
478     super()
479     @socket = socket
480     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game -> finished
481
482     @protocol = nil             # CSA or x1
483     @eol = "\m"                 # favorite eol code
484     @game = nil
485     @game_name = ""
486     @mytime = 0                 # set in start method also
487     @sente = nil
488     @write_queue = Queue::new
489     @main_thread = Thread::current
490     @writer_thread = Thread::start do
491       Thread.pass
492       writer()
493     end
494   end
495
496   attr_accessor :socket, :status
497   attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
498   attr_accessor :main_thread, :writer_thread, :write_queue
499   
500   def kill
501     log_message(sprintf("user %s killed", @name))
502     if (@game)
503       @game.kill(self)
504     end
505     finish
506     Thread::kill(@main_thread) if @main_thread
507   end
508
509   def finish
510     if (@status != "finished")
511       @status = "finished"
512       log_message(sprintf("user %s finish", @name))    
513       # TODO you should confirm that there is no message in the queue.
514       Thread::kill(@writer_thread) if @writer_thread
515       begin
516 #        @socket.close if (! @socket.closed?)
517       rescue
518         log_message(sprintf("user %s finish failed", @name))    
519       end
520     end
521   end
522
523   def write_safe(str)
524     @write_queue.push(str.gsub(/[\r\n]+/, @eol))
525   end
526
527   def writer
528     while (str = @write_queue.pop)
529       begin
530         @socket.write(str)
531       rescue Exception => ex
532         log_error("Failed to send a message to #{@name}.")
533         log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
534         return
535       end
536     end
537   end
538
539   def to_s
540     if ((status == "game_waiting") ||
541         (status == "start_waiting") ||
542         (status == "agree_waiting") ||
543         (status == "game"))
544       if (@sente)
545         return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
546       elsif (@sente == false)
547         return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
548       elsif (@sente == nil)
549         return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
550       end
551     else
552       return sprintf("%s %s %s", @name, @protocol, @status)
553     end
554   end
555
556   def run(csa_1st_str=nil)
557     while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
558       begin
559         $mutex.lock
560
561         if (@writer_thread == nil || !@writer_thread.status)
562           # The writer_thread has been killed because of socket errors.
563           return
564         end
565
566         if (csa_1st_str)
567           str = csa_1st_str
568           csa_1st_str = nil
569         end
570         if (@write_queue.size > Max_Write_Queue_Size)
571           log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
572                 return
573         end
574
575         if (@status == "finished")
576           return
577         end
578         str.chomp! if (str.class == String) # may be strip! ?
579         log_message(str) if $DEBUG
580         case str 
581         when "" 
582           # Application-level protocol for Keep-Alive
583           # If the server gets LF, it sends back LF.
584           # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet.
585           write_safe("\n")
586         when /^[\+\-][^%]/
587           if (@status == "game")
588             array_str = str.split(",")
589             move = array_str.shift
590             additional = array_str.shift
591             if /^'(.*)/ =~ additional
592               comment = array_str.unshift("'*#{$1}")
593             end
594             s = @game.handle_one_move(move, self)
595             @game.fh.print("#{comment}\n") if (comment && !s)
596             return if (s && @protocol == LoginCSA::PROTOCOL)
597           end
598         when /^%[^%]/, :timeout
599           if (@status == "game")
600             s = @game.handle_one_move(str, self)
601             return if (s && @protocol == LoginCSA::PROTOCOL)
602           # else
603           #   begin
604           #     @socket.write("##[KEEPALIVE] #{Time.now}\n")
605           #   rescue Exception => ex
606           #     log_error("Failed to send a keepalive to #{@name}.")
607           #     log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
608           #     return
609           #   end
610           end
611         when :exception
612           log_error("Failed to receive a message from #{@name}.")
613           return
614         when /^REJECT/
615           if (@status == "agree_waiting")
616             @game.reject(@name)
617             return if (@protocol == LoginCSA::PROTOCOL)
618           else
619             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
620           end
621         when /^AGREE/
622           if (@status == "agree_waiting")
623             @status = "start_waiting"
624             if ((@game.sente.status == "start_waiting") &&
625                 (@game.gote.status == "start_waiting"))
626               @game.start
627               @game.sente.status = "game"
628               @game.gote.status = "game"
629             end
630           else
631             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
632           end
633         when /^%%SHOW\s+(\S+)/
634           game_id = $1
635           if (LEAGUE.games[game_id])
636             write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
637           end
638           write_safe("##[SHOW] +OK\n")
639         when /^%%MONITORON\s+(\S+)/
640           game_id = $1
641           if (LEAGUE.games[game_id])
642             LEAGUE.games[game_id].monitoron(self)
643             write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
644             write_safe("##[MONITOR][#{game_id}] +OK\n")
645           end
646         when /^%%MONITOROFF\s+(\S+)/
647           game_id = $1
648           if (LEAGUE.games[game_id])
649             LEAGUE.games[game_id].monitoroff(self)
650           end
651         when /^%%HELP/
652           write_safe(
653             %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
654         when /^%%RATING/
655           players = LEAGUE.rated_players
656           players.sort {|a,b| b.rate <=> a.rate}.each do |p|
657             write_safe("##[RATING] %s \t %4d @%s\n" % 
658                        [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
659           end
660           write_safe("##[RATING] +OK\n")
661         when /^%%VERSION/
662           write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
663           write_safe("##[VERSION] +OK\n")
664         when /^%%GAME\s*$/
665           if ((@status == "connected") || (@status == "game_waiting"))
666             @status = "connected"
667             @game_name = ""
668           else
669             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
670           end
671         when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
672           command_name = $1
673           game_name = $2
674           my_sente_str = $3
675           if (! Login::good_game_name?(game_name))
676             write_safe(sprintf("##[ERROR] bad game name\n"))
677             next
678           elsif ((@status == "connected") || (@status == "game_waiting"))
679             ## continue
680           else
681             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
682             next
683           end
684
685           rival = nil
686           if (League::Floodgate.game_name?(game_name))
687             if (my_sente_str != "*")
688               write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n"), my_sente_str, game_name)
689               next
690             end
691             @sente = nil
692           else
693             if (my_sente_str == "*")
694               rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
695             elsif (my_sente_str == "+")
696               rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
697             elsif (my_sente_str == "-")
698               rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
699             else
700               ## never reached
701               write_safe(sprintf("##[ERROR] bad game option\n"))
702               next
703             end
704           end
705
706           if (rival)
707             @game_name = game_name
708             if ((my_sente_str == "*") && (rival.sente == nil))
709               if (rand(2) == 0)
710                 @sente = true
711                 rival.sente = false
712               else
713                 @sente = false
714                 rival.sente = true
715               end
716             elsif (rival.sente == true) # rival has higher priority
717               @sente = false
718             elsif (rival.sente == false)
719               @sente = true
720             elsif (my_sente_str == "+")
721               @sente = true
722               rival.sente = false
723             elsif (my_sente_str == "-")
724               @sente = false
725               rival.sente = true
726             else
727               ## never reached
728             end
729             Game::new(@game_name, self, rival)
730           else # rival not found
731             if (command_name == "GAME")
732               @status = "game_waiting"
733               @game_name = game_name
734               if (my_sente_str == "+")
735                 @sente = true
736               elsif (my_sente_str == "-")
737                 @sente = false
738               else
739                 @sente = nil
740               end
741             else                # challenge
742               write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
743               @status = "connected"
744               @game_name = ""
745               @sente = nil
746             end
747           end
748         when /^%%CHAT\s+(.+)/
749           message = $1
750           LEAGUE.players.each do |name, player|
751             if (player.protocol != LoginCSA::PROTOCOL)
752               player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
753             end
754           end
755         when /^%%LIST/
756           buf = Array::new
757           LEAGUE.games.each do |id, game|
758             buf.push(sprintf("##[LIST] %s\n", id))
759           end
760           buf.push("##[LIST] +OK\n")
761           write_safe(buf.join)
762         when /^%%WHO/
763           buf = Array::new
764           LEAGUE.players.each do |name, player|
765             buf.push(sprintf("##[WHO] %s\n", player.to_s))
766           end
767           buf.push("##[WHO] +OK\n")
768           write_safe(buf.join)
769         when /^LOGOUT/
770           @status = "connected"
771           write_safe("LOGOUT:completed\n")
772           return
773         when /^CHALLENGE/
774           # This command is only available for CSA's official testing server.
775           # So, this means nothing for this program.
776           write_safe("CHALLENGE ACCEPTED\n")
777         when /^\s*$/
778           ## ignore null string
779         else
780           msg = "##[ERROR] unknown command %s\n" % [str]
781           write_safe(msg)
782           log_error(msg)
783         end
784       ensure
785         $mutex.unlock
786       end
787     end # enf of while
788   end # def run
789 end # class
790
791 class Piece
792   PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
793   def initialize(board, x, y, sente, promoted=false)
794     @board = board
795     @x = x
796     @y = y
797     @sente = sente
798     @promoted = promoted
799
800     if ((x == 0) || (y == 0))
801       if (sente)
802         hands = board.sente_hands
803       else
804         hands = board.gote_hands
805       end
806       hands.push(self)
807       hands.sort! {|a, b|
808         a.name <=> b.name
809       }
810     else
811       @board.array[x][y] = self
812     end
813   end
814   attr_accessor :promoted, :sente, :x, :y, :board
815
816   def room_of_head?(x, y, name)
817     true
818   end
819
820   def movable_grids
821     return adjacent_movable_grids + far_movable_grids
822   end
823
824   def far_movable_grids
825     return []
826   end
827
828   def jump_to?(x, y)
829     if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
830       if ((@board.array[x][y] == nil) || # dst is empty
831           (@board.array[x][y].sente != @sente)) # dst is enemy
832         return true
833       end
834     end
835     return false
836   end
837
838   def put_to?(x, y)
839     if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
840       if (@board.array[x][y] == nil) # dst is empty?
841         return true
842       end
843     end
844     return false
845   end
846
847   def adjacent_movable_grids
848     grids = Array::new
849     if (@promoted)
850       moves = @promoted_moves
851     else
852       moves = @normal_moves
853     end
854     moves.each do |(dx, dy)|
855       if (@sente)
856         cand_y = @y - dy
857       else
858         cand_y = @y + dy
859       end
860       cand_x = @x + dx
861       if (jump_to?(cand_x, cand_y))
862         grids.push([cand_x, cand_y])
863       end
864     end
865     return grids
866   end
867
868   def move_to?(x, y, name)
869     return false if (! room_of_head?(x, y, name))
870     return false if ((name != @name) && (name != @promoted_name))
871     return false if (@promoted && (name != @promoted_name)) # can't un-promote
872
873     if (! @promoted)
874       return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
875       if (@sente)
876         return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
877       else
878         return false if ((6 >= @y) && (6 >= y) && (name != @name))
879       end
880     end
881
882     if ((@x == 0) || (@y == 0))
883       return jump_to?(x, y)
884     else
885       return movable_grids.include?([x, y])
886     end
887   end
888
889   def move_to(x, y)
890     if ((@x == 0) || (@y == 0))
891       if (@sente)
892         @board.sente_hands.delete(self)
893       else
894         @board.gote_hands.delete(self)
895       end
896       @board.array[x][y] = self
897     elsif ((x == 0) || (y == 0))
898       @promoted = false         # clear promoted flag before moving to hands
899       if (@sente)
900         @board.sente_hands.push(self)
901       else
902         @board.gote_hands.push(self)
903       end
904       @board.array[@x][@y] = nil
905     else
906       @board.array[@x][@y] = nil
907       @board.array[x][y] = self
908     end
909     @x = x
910     @y = y
911   end
912
913   def point
914     @point
915   end
916
917   def name
918     @name
919   end
920
921   def promoted_name
922     @promoted_name
923   end
924
925   def to_s
926     if (@sente)
927       sg = "+"
928     else
929       sg = "-"
930     end
931     if (@promoted)
932       n = @promoted_name
933     else
934       n = @name
935     end
936     return sg + n
937   end
938 end
939
940 class PieceFU < Piece
941   def initialize(*arg)
942     @point = 1
943     @normal_moves = [[0, +1]]
944     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
945     @name = "FU"
946     @promoted_name = "TO"
947     super
948   end
949   def room_of_head?(x, y, name)
950     if (name == "FU")
951       if (@sente)
952         return false if (y == 1)
953       else
954         return false if (y == 9)
955       end
956       ## 2fu check
957       c = 0
958       iy = 1
959       while (iy <= 9)
960         if ((iy  != @y) &&      # not source position
961             @board.array[x][iy] &&
962             (@board.array[x][iy].sente == @sente) && # mine
963             (@board.array[x][iy].name == "FU") &&
964             (@board.array[x][iy].promoted == false))
965           return false
966         end
967         iy = iy + 1
968       end
969     end
970     return true
971   end
972 end
973
974 class PieceKY  < Piece
975   def initialize(*arg)
976     @point = 1
977     @normal_moves = []
978     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
979     @name = "KY"
980     @promoted_name = "NY"
981     super
982   end
983   def room_of_head?(x, y, name)
984     if (name == "KY")
985       if (@sente)
986         return false if (y == 1)
987       else
988         return false if (y == 9)
989       end
990     end
991     return true
992   end
993   def far_movable_grids
994     grids = Array::new
995     if (@promoted)
996       return []
997     else
998       if (@sente)                 # up
999         cand_x = @x
1000         cand_y = @y - 1
1001         while (jump_to?(cand_x, cand_y))
1002           grids.push([cand_x, cand_y])
1003           break if (! put_to?(cand_x, cand_y))
1004           cand_y = cand_y - 1
1005         end
1006       else                        # down
1007         cand_x = @x
1008         cand_y = @y + 1
1009         while (jump_to?(cand_x, cand_y))
1010           grids.push([cand_x, cand_y])
1011           break if (! put_to?(cand_x, cand_y))
1012           cand_y = cand_y + 1
1013         end
1014       end
1015       return grids
1016     end
1017   end
1018 end
1019 class PieceKE  < Piece
1020   def initialize(*arg)
1021     @point = 1
1022     @normal_moves = [[+1, +2], [-1, +2]]
1023     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
1024     @name = "KE"
1025     @promoted_name = "NK"
1026     super
1027   end
1028   def room_of_head?(x, y, name)
1029     if (name == "KE")
1030       if (@sente)
1031         return false if ((y == 1) || (y == 2))
1032       else
1033         return false if ((y == 9) || (y == 8))
1034       end
1035     end
1036     return true
1037   end
1038 end
1039 class PieceGI  < Piece
1040   def initialize(*arg)
1041     @point = 1
1042     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
1043     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
1044     @name = "GI"
1045     @promoted_name = "NG"
1046     super
1047   end
1048 end
1049 class PieceKI  < Piece
1050   def initialize(*arg)
1051     @point = 1
1052     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
1053     @promoted_moves = []
1054     @name = "KI"
1055     @promoted_name = nil
1056     super
1057   end
1058 end
1059 class PieceKA  < Piece
1060   def initialize(*arg)
1061     @point = 5
1062     @normal_moves = []
1063     @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
1064     @name = "KA"
1065     @promoted_name = "UM"
1066     super
1067   end
1068   def far_movable_grids
1069     grids = Array::new
1070     ## up right
1071     cand_x = @x - 1
1072     cand_y = @y - 1
1073     while (jump_to?(cand_x, cand_y))
1074       grids.push([cand_x, cand_y])
1075       break if (! put_to?(cand_x, cand_y))
1076       cand_x = cand_x - 1
1077       cand_y = cand_y - 1
1078     end
1079     ## down right
1080     cand_x = @x - 1
1081     cand_y = @y + 1
1082     while (jump_to?(cand_x, cand_y))
1083       grids.push([cand_x, cand_y])
1084       break if (! put_to?(cand_x, cand_y))
1085       cand_x = cand_x - 1
1086       cand_y = cand_y + 1
1087     end
1088     ## up left
1089     cand_x = @x + 1
1090     cand_y = @y - 1
1091     while (jump_to?(cand_x, cand_y))
1092       grids.push([cand_x, cand_y])
1093       break if (! put_to?(cand_x, cand_y))
1094       cand_x = cand_x + 1
1095       cand_y = cand_y - 1
1096     end
1097     ## down left
1098     cand_x = @x + 1
1099     cand_y = @y + 1
1100     while (jump_to?(cand_x, cand_y))
1101       grids.push([cand_x, cand_y])
1102       break if (! put_to?(cand_x, cand_y))
1103       cand_x = cand_x + 1
1104       cand_y = cand_y + 1
1105     end
1106     return grids
1107   end
1108 end
1109 class PieceHI  < Piece
1110   def initialize(*arg)
1111     @point = 5
1112     @normal_moves = []
1113     @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
1114     @name = "HI"
1115     @promoted_name = "RY"
1116     super
1117   end
1118   def far_movable_grids
1119     grids = Array::new
1120     ## up
1121     cand_x = @x
1122     cand_y = @y - 1
1123     while (jump_to?(cand_x, cand_y))
1124       grids.push([cand_x, cand_y])
1125       break if (! put_to?(cand_x, cand_y))
1126       cand_y = cand_y - 1
1127     end
1128     ## down
1129     cand_x = @x
1130     cand_y = @y + 1
1131     while (jump_to?(cand_x, cand_y))
1132       grids.push([cand_x, cand_y])
1133       break if (! put_to?(cand_x, cand_y))
1134       cand_y = cand_y + 1
1135     end
1136     ## right
1137     cand_x = @x - 1
1138     cand_y = @y
1139     while (jump_to?(cand_x, cand_y))
1140       grids.push([cand_x, cand_y])
1141       break if (! put_to?(cand_x, cand_y))
1142       cand_x = cand_x - 1
1143     end
1144     ## down
1145     cand_x = @x + 1
1146     cand_y = @y
1147     while (jump_to?(cand_x, cand_y))
1148       grids.push([cand_x, cand_y])
1149       break if (! put_to?(cand_x, cand_y))
1150       cand_x = cand_x + 1
1151     end
1152     return grids
1153   end
1154 end
1155 class PieceOU < Piece
1156   def initialize(*arg)
1157     @point = 0
1158     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
1159     @promoted_moves = []
1160     @name = "OU"
1161     @promoted_name = nil
1162     super
1163   end
1164 end
1165
1166 class Board
1167   def initialize
1168     @sente_hands = Array::new
1169     @gote_hands  = Array::new
1170     @history       = Hash::new(0)
1171     @sente_history = Hash::new(0)
1172     @gote_history  = Hash::new(0)
1173     @array = [[], [], [], [], [], [], [], [], [], []]
1174     @move_count = 0
1175   end
1176   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
1177   attr_reader :move_count
1178
1179   def initial
1180     PieceKY::new(self, 1, 1, false)
1181     PieceKE::new(self, 2, 1, false)
1182     PieceGI::new(self, 3, 1, false)
1183     PieceKI::new(self, 4, 1, false)
1184     PieceOU::new(self, 5, 1, false)
1185     PieceKI::new(self, 6, 1, false)
1186     PieceGI::new(self, 7, 1, false)
1187     PieceKE::new(self, 8, 1, false)
1188     PieceKY::new(self, 9, 1, false)
1189     PieceKA::new(self, 2, 2, false)
1190     PieceHI::new(self, 8, 2, false)
1191     (1..9).each do |i|
1192       PieceFU::new(self, i, 3, false)
1193     end
1194
1195     PieceKY::new(self, 1, 9, true)
1196     PieceKE::new(self, 2, 9, true)
1197     PieceGI::new(self, 3, 9, true)
1198     PieceKI::new(self, 4, 9, true)
1199     PieceOU::new(self, 5, 9, true)
1200     PieceKI::new(self, 6, 9, true)
1201     PieceGI::new(self, 7, 9, true)
1202     PieceKE::new(self, 8, 9, true)
1203     PieceKY::new(self, 9, 9, true)
1204     PieceKA::new(self, 8, 8, true)
1205     PieceHI::new(self, 2, 8, true)
1206     (1..9).each do |i|
1207       PieceFU::new(self, i, 7, true)
1208     end
1209   end
1210
1211   def have_piece?(hands, name)
1212     piece = hands.find { |i|
1213       i.name == name
1214     }
1215     return piece
1216   end
1217
1218   def move_to(x0, y0, x1, y1, name, sente)
1219     if (sente)
1220       hands = @sente_hands
1221     else
1222       hands = @gote_hands
1223     end
1224
1225     if ((x0 == 0) || (y0 == 0))
1226       piece = have_piece?(hands, name)
1227       return :illegal if (! piece.move_to?(x1, y1, name)) # TODO null check for the piece?
1228       piece.move_to(x1, y1)
1229     else
1230       return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))  # TODO null check?
1231       if (@array[x0][y0].name != name) # promoted ?
1232         @array[x0][y0].promoted = true
1233       end
1234       if (@array[x1][y1]) # capture
1235         if (@array[x1][y1].name == "OU")
1236           return :outori        # return board update
1237         end
1238         @array[x1][y1].sente = @array[x0][y0].sente
1239         @array[x1][y1].move_to(0, 0)
1240         hands.sort! {|a, b| # TODO refactor. Move to Piece class
1241           a.name <=> b.name
1242         }
1243       end
1244       @array[x0][y0].move_to(x1, y1)
1245     end
1246     @move_count += 1
1247     return true
1248   end
1249
1250   def look_for_ou(sente)
1251     x = 1
1252     while (x <= 9)
1253       y = 1
1254       while (y <= 9)
1255         if (@array[x][y] &&
1256             (@array[x][y].name == "OU") &&
1257             (@array[x][y].sente == sente))
1258           return @array[x][y]
1259         end
1260         y = y + 1
1261       end
1262       x = x + 1
1263     end
1264     raise "can't find ou"
1265   end
1266
1267   # note checkmate, but check. sente is checked.
1268   def checkmated?(sente)        # sente is loosing
1269     ou = look_for_ou(sente)
1270     x = 1
1271     while (x <= 9)
1272       y = 1
1273       while (y <= 9)
1274         if (@array[x][y] &&
1275             (@array[x][y].sente != sente))
1276           if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1277             return true
1278           end
1279         end
1280         y = y + 1
1281       end
1282       x = x + 1
1283     end
1284     return false
1285   end
1286
1287   def uchifuzume?(sente)
1288     rival_ou = look_for_ou(! sente)   # rival's ou
1289     if (sente)                  # rival is gote
1290       if ((rival_ou.y != 9) &&
1291           (@array[rival_ou.x][rival_ou.y + 1]) &&
1292           (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1293           (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1294         fu_x = rival_ou.x
1295         fu_y = rival_ou.y + 1
1296       else
1297         return false
1298       end
1299     else                        # gote
1300       if ((rival_ou.y != 0) &&
1301           (@array[rival_ou.x][rival_ou.y - 1]) &&
1302           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1303           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1304         fu_x = rival_ou.x
1305         fu_y = rival_ou.y - 1
1306       else
1307         return false
1308       end
1309     end
1310     
1311     ## case: rival_ou is moving
1312     escaped = false
1313     rival_ou.movable_grids.each do |(cand_x, cand_y)|
1314       tmp_board = Marshal.load(Marshal.dump(self))
1315       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1316       raise "internal error" if (s != true)
1317       if (! tmp_board.checkmated?(! sente)) # good move
1318         return false
1319       end
1320     end
1321
1322     ## case: rival is capturing fu
1323     x = 1
1324     while (x <= 9)
1325       y = 1
1326       while (y <= 9)
1327         if (@array[x][y] &&
1328             (@array[x][y].sente != sente) &&
1329             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1330           if (@array[x][y].promoted)
1331             name = @array[x][y].promoted_name
1332           else
1333             name = @array[x][y].name
1334           end
1335           tmp_board = Marshal.load(Marshal.dump(self))
1336           s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1337           raise "internal error" if (s != true)
1338           if (! tmp_board.checkmated?(! sente)) # good move
1339             return false
1340           end
1341         end
1342         y = y + 1
1343       end
1344       x = x + 1
1345     end
1346     return true
1347   end
1348
1349   # @[sente|gote]_history has at least one item while the player is checking the other or 
1350   # the other escapes.
1351   def update_sennichite(player)
1352     str = to_s
1353     @history[str] += 1
1354     if checkmated?(!player)
1355       if (player)
1356         @sente_history["dummy"] = 1  # flag to see Sente player is checking Gote player
1357       else
1358         @gote_history["dummy"]  = 1  # flag to see Gote player is checking Sente player
1359       end
1360     else
1361       if (player)
1362         @sente_history.clear # no more continuous check
1363       else
1364         @gote_history.clear  # no more continuous check
1365       end
1366     end
1367     if @sente_history.size > 0  # possible for Sente's or Gote's turn
1368       @sente_history[str] += 1
1369     end
1370     if @gote_history.size > 0   # possible for Sente's or Gote's turn
1371       @gote_history[str] += 1
1372     end
1373   end
1374
1375   def oute_sennichite?(player)
1376     if (@sente_history[to_s] >= 4)
1377       return :oute_sennichite_sente_lose
1378     elsif (@gote_history[to_s] >= 4)
1379       return :oute_sennichite_gote_lose
1380     else
1381       return nil
1382     end
1383   end
1384
1385   def sennichite?(sente)
1386     if (@history[to_s] >= 4) # already 3 times
1387       return true
1388     end
1389     return false
1390   end
1391
1392   def good_kachi?(sente)
1393     if (checkmated?(sente))
1394       puts "'NG: Checkmating." if $DEBUG
1395       return false 
1396     end
1397     
1398     ou = look_for_ou(sente)
1399     if (sente && (ou.y >= 4))
1400       puts "'NG: Black's OU does not enter yet." if $DEBUG
1401       return false     
1402     end  
1403     if (! sente && (ou.y <= 6))
1404       puts "'NG: White's OU does not enter yet." if $DEBUG
1405       return false 
1406     end
1407       
1408     number = 0
1409     point = 0
1410
1411     if (sente)
1412       hands = @sente_hands
1413       r = [1, 2, 3]
1414     else
1415       hands = @gote_hands
1416       r = [7, 8, 9]
1417     end
1418     r.each do |y|
1419       x = 1
1420       while (x <= 9)
1421         if (@array[x][y] &&
1422             (@array[x][y].sente == sente) &&
1423             (@array[x][y].point > 0))
1424           point = point + @array[x][y].point
1425           number = number + 1
1426         end
1427         x = x + 1
1428       end
1429     end
1430     hands.each do |piece|
1431       point = point + piece.point
1432     end
1433
1434     if (number < 10)
1435       puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1436       return false     
1437     end  
1438     if (sente)
1439       if (point < 28)
1440         puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1441         return false 
1442       end  
1443     else
1444       if (point < 27)
1445         puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1446         return false 
1447       end
1448     end
1449
1450     puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1451     return true
1452   end
1453
1454   # sente is nil only if tests in test_board run
1455   def handle_one_move(str, sente=nil)
1456     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1457       sg = $1
1458       x0 = $2.to_i
1459       y0 = $3.to_i
1460       x1 = $4.to_i
1461       y1 = $5.to_i
1462       name = $6
1463     elsif (str =~ /^%KACHI/)
1464       raise ArgumentError, "sente is null", caller if sente == nil
1465       if (good_kachi?(sente))
1466         return :kachi_win
1467       else
1468         return :kachi_lose
1469       end
1470     elsif (str =~ /^%TORYO/)
1471       return :toryo
1472     else
1473       return :illegal
1474     end
1475     
1476     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1477         ((x0 != 0) || (y0 != 0)))
1478       return :illegal
1479     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1480       return :illegal
1481     end
1482     
1483
1484     if (sg == "+")
1485       sente = true if sente == nil           # deprecated
1486       return :illegal unless sente == true   # black player's move must be black
1487       hands = @sente_hands
1488     else
1489       sente = false if sente == nil          # deprecated
1490       return :illegal unless sente == false  # white player's move must be white
1491       hands = @gote_hands
1492     end
1493     
1494     ## source check
1495     if ((x0 == 0) && (y0 == 0))
1496       return :illegal if (! have_piece?(hands, name))
1497     elsif (! @array[x0][y0])
1498       return :illegal           # no piece
1499     elsif (@array[x0][y0].sente != sente)
1500       return :illegal           # this is not mine
1501     elsif (@array[x0][y0].name != name)
1502       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1503     end
1504
1505     ## destination check
1506     if (@array[x1][y1] &&
1507         (@array[x1][y1].sente == sente)) # can't capture mine
1508       return :illegal
1509     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1510       return :illegal           # can't put on existing piece
1511     end
1512
1513     tmp_board = Marshal.load(Marshal.dump(self))
1514     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1515     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1516     tmp_board.update_sennichite(sente)
1517     os_result = tmp_board.oute_sennichite?(sente)
1518     return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
1519     return :sennichite if tmp_board.sennichite?(sente)
1520
1521     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1522       return :uchifuzume
1523     end
1524
1525     move_to(x0, y0, x1, y1, name, sente)
1526     str = to_s
1527
1528     update_sennichite(sente)
1529     return :normal
1530   end
1531
1532   def to_s
1533     a = Array::new
1534     y = 1
1535     while (y <= 9)
1536       a.push(sprintf("P%d", y))
1537       x = 9
1538       while (x >= 1)
1539         piece = @array[x][y]
1540         if (piece)
1541           s = piece.to_s
1542         else
1543           s = " * "
1544         end
1545         a.push(s)
1546         x = x - 1
1547       end
1548       a.push(sprintf("\n"))
1549       y = y + 1
1550     end
1551     if (! sente_hands.empty?)
1552       a.push("P+")
1553       sente_hands.each do |p|
1554         a.push("00" + p.name)
1555       end
1556       a.push("\n")
1557     end
1558     if (! gote_hands.empty?)
1559       a.push("P-")
1560       gote_hands.each do |p|
1561         a.push("00" + p.name)
1562       end
1563       a.push("\n")
1564     end
1565     a.push("+\n")
1566     return a.join
1567   end
1568 end
1569
1570 class GameResult
1571   attr_reader :players, :black, :white
1572
1573   def initialize(p1, p2)
1574     @players = [p1, p2]
1575     if p1.sente && !p2.sente
1576       @black, @white = p1, p2
1577     elsif !p1.sente && p2.sente
1578       @black, @white = p2, p1
1579     else
1580       raise "Never reached!"
1581     end
1582   end
1583 end
1584
1585 class GameResultWin < GameResult
1586   attr_reader :winner, :loser
1587
1588   def initialize(winner, loser)
1589     super
1590     @winner, @loser = winner, loser
1591     @winner.last_game_win = true
1592     @loser.last_game_win  = false
1593   end
1594
1595   def to_s
1596     black_name = @black.id || @black.name
1597     white_name = @white.id || @white.name
1598     "%s:%s" % [black_name, white_name]
1599   end
1600 end
1601
1602 class GameResultDraw < GameResult
1603   def initialize(p1, p2)
1604     super
1605     p1.last_game_win = false
1606     p2.last_game_win = false
1607   end
1608 end
1609
1610 class Game
1611   @@mutex = Mutex.new
1612   @@time  = 0
1613
1614   def initialize(game_name, player0, player1)
1615     @monitors = Array::new
1616     @game_name = game_name
1617     if (@game_name =~ /-(\d+)-(\d+)$/)
1618       @total_time = $1.to_i
1619       @byoyomi = $2.to_i
1620     end
1621
1622     if (player0.sente)
1623       @sente = player0
1624       @gote = player1
1625     else
1626       @sente = player1
1627       @gote = player0
1628     end
1629     @current_player = @sente
1630     @next_player = @gote
1631
1632     @sente.game = self
1633     @gote.game = self
1634
1635     @last_move = ""
1636     @current_turn = 0
1637
1638     @sente.status = "agree_waiting"
1639     @gote.status = "agree_waiting"
1640     
1641     @id = sprintf("%s+%s+%s+%s+%s", 
1642                   LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1643     @logfile = File.join(LEAGUE.dir, @id + ".csa")
1644
1645     LEAGUE.games[@id] = self
1646
1647     log_message(sprintf("game created %s", @id))
1648
1649     @board = Board::new
1650     @board.initial
1651     @start_time = nil
1652     @fh = nil
1653     @result = nil
1654
1655     propose
1656   end
1657   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1658   attr_accessor :last_move, :current_turn
1659   attr_reader   :result
1660
1661   def rated?
1662     @sente.rated? && @gote.rated?
1663   end
1664
1665   def monitoron(monitor)
1666     @monitors.delete(monitor)
1667     @monitors.push(monitor)
1668   end
1669
1670   def monitoroff(monitor)
1671     @monitors.delete(monitor)
1672   end
1673
1674   def reject(rejector)
1675     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1676     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1677     finish
1678   end
1679
1680   def kill(killer)
1681     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1682       reject(killer.name)
1683     elsif (@current_player == killer)
1684       abnormal_lose()
1685       finish
1686     end
1687   end
1688
1689   def finish
1690     log_message(sprintf("game finished %s", @id))
1691     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1692     @fh.close
1693
1694     @sente.game = nil
1695     @gote.game = nil
1696     @sente.status = "connected"
1697     @gote.status = "connected"
1698
1699     if (@current_player.protocol == LoginCSA::PROTOCOL)
1700       @current_player.finish
1701     end
1702     if (@next_player.protocol == LoginCSA::PROTOCOL)
1703       @next_player.finish
1704     end
1705     @monitors = Array::new
1706     @sente = nil
1707     @gote = nil
1708     @current_player = nil
1709     @next_player = nil
1710     LEAGUE.games.delete(@id)
1711   end
1712
1713   def handle_one_move(str, player)
1714     finish_flag = true
1715     if (@current_player == player)
1716       @end_time = Time::new
1717       t = (@end_time - @start_time).floor
1718       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1719       
1720       move_status = nil
1721       if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1722         status = :timeout
1723       elsif (str == :timeout)
1724         return false            # time isn't expired. players aren't swapped. continue game
1725       else
1726         @current_player.mytime = @current_player.mytime - t
1727         if (@current_player.mytime < 0)
1728           @current_player.mytime = 0
1729         end
1730
1731 #        begin
1732           move_status = @board.handle_one_move(str, @sente == @current_player)
1733 #        rescue
1734 #          log_error("handle_one_move raise exception for #{str}")
1735 #          move_status = :illegal
1736 #        end
1737
1738         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1739           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1740         else
1741           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite_sente_lose) || (move_status == :oute_sennichite_gote_lose))
1742             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1743             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1744             @fh.printf("%s\nT%d\n", str, t)
1745             @last_move = sprintf("%s,T%d", str, t)
1746             @current_turn = @current_turn + 1
1747           end
1748
1749           @monitors.each do |monitor|
1750             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1751             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1752           end
1753         end
1754       end
1755
1756       if (@next_player.status != "game") # rival is logout or disconnected
1757         abnormal_win()
1758       elsif (status == :timeout)
1759         timeout_lose()
1760       elsif (move_status == :illegal)
1761         illegal_lose()
1762       elsif (move_status == :kachi_win)
1763         kachi_win()
1764       elsif (move_status == :kachi_lose)
1765         kachi_lose()
1766       elsif (move_status == :toryo)
1767         toryo_lose()
1768       elsif (move_status == :outori)
1769         outori_win()
1770       elsif (move_status == :oute_sennichite_sente_lose)
1771         oute_sennichite_win_lose(@gote, @sente) # Sente is checking
1772       elsif (move_status == :oute_sennichite_gote_lose)
1773         oute_sennichite_win_lose(@sente, @gote) # Gote is checking
1774       elsif (move_status == :sennichite)
1775         sennichite_draw()
1776       elsif (move_status == :uchifuzume)
1777         uchifuzume_lose()
1778       elsif (move_status == :oute_kaihimore)
1779         oute_kaihimore_lose()
1780       else
1781         finish_flag = false
1782       end
1783       finish() if finish_flag
1784       (@current_player, @next_player) = [@next_player, @current_player]
1785       @start_time = Time::new
1786       return finish_flag
1787     end
1788   end
1789
1790   def abnormal_win
1791     @current_player.status = "connected"
1792     @next_player.status = "connected"
1793     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1794     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1795     @fh.printf("%%TORYO\n")
1796     @fh.print(@board.to_s.gsub(/^/, "\'"))
1797     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1798     @result = GameResultWin.new(@current_player, @next_player)
1799     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1800     @monitors.each do |monitor|
1801       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1802     end
1803   end
1804
1805   def abnormal_lose
1806     @current_player.status = "connected"
1807     @next_player.status = "connected"
1808     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1809     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1810     @fh.printf("%%TORYO\n")
1811     @fh.print(@board.to_s.gsub(/^/, "\'"))
1812     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1813     @result = GameResultWin.new(@next_player, @current_player)
1814     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1815     @monitors.each do |monitor|
1816       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1817     end
1818   end
1819
1820   def sennichite_draw
1821     @current_player.status = "connected"
1822     @next_player.status = "connected"
1823     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1824     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1825     @fh.print(@board.to_s.gsub(/^/, "\'"))
1826     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1827     @result = GameResultDraw.new(@current_player, @next_player)
1828     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1829     @monitors.each do |monitor|
1830       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1831     end
1832   end
1833
1834   def oute_sennichite_win_lose(winner, loser)
1835     @current_player.status = "connected"
1836     @next_player.status = "connected"
1837     loser.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1838     winner.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1839     @fh.print(@board.to_s.gsub(/^/, "\'"))
1840     if loser == @current_player
1841       @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1842     else
1843       @fh.printf("'summary:oute_sennichite:%s win:%s lose\n", @current_player.name, @next_player.name)
1844     end
1845     @result = GameResultWin.new(winner, loser)
1846     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1847     @monitors.each do |monitor|
1848       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1849     end
1850   end
1851
1852   def illegal_lose
1853     @current_player.status = "connected"
1854     @next_player.status = "connected"
1855     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1856     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1857     @fh.print(@board.to_s.gsub(/^/, "\'"))
1858     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1859     @result = GameResultWin.new(@next_player, @current_player)
1860     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1861     @monitors.each do |monitor|
1862       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1863     end
1864   end
1865
1866   def uchifuzume_lose
1867     @current_player.status = "connected"
1868     @next_player.status = "connected"
1869     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1870     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1871     @fh.print(@board.to_s.gsub(/^/, "\'"))
1872     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1873     @result = GameResultWin.new(@next_player, @current_player)
1874     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1875     @monitors.each do |monitor|
1876       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1877     end
1878   end
1879
1880   def oute_kaihimore_lose
1881     @current_player.status = "connected"
1882     @next_player.status = "connected"
1883     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1884     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1885     @fh.print(@board.to_s.gsub(/^/, "\'"))
1886     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1887     @result = GameResultWin.new(@next_player, @current_player)
1888     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1889     @monitors.each do |monitor|
1890       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1891     end
1892   end
1893
1894   def timeout_lose
1895     @current_player.status = "connected"
1896     @next_player.status = "connected"
1897     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1898     @next_player.write_safe("#TIME_UP\n#WIN\n")
1899     @fh.print(@board.to_s.gsub(/^/, "\'"))
1900     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1901     @result = GameResultWin.new(@next_player, @current_player)
1902     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1903     @monitors.each do |monitor|
1904       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1905     end
1906   end
1907
1908   def kachi_win
1909     @current_player.status = "connected"
1910     @next_player.status = "connected"
1911     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1912     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1913     @fh.printf("%%KACHI\n")
1914     @fh.print(@board.to_s.gsub(/^/, "\'"))
1915     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1916     @result = GameResultWin.new(@current_player, @next_player)
1917     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1918     @monitors.each do |monitor|
1919       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1920     end
1921   end
1922
1923   def kachi_lose
1924     @current_player.status = "connected"
1925     @next_player.status = "connected"
1926     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1927     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1928     @fh.printf("%%KACHI\n")
1929     @fh.print(@board.to_s.gsub(/^/, "\'"))
1930     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1931     @result = GameResultWin.new(@next_player, @current_player)
1932     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1933     @monitors.each do |monitor|
1934       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1935     end
1936   end
1937
1938   def toryo_lose
1939     @current_player.status = "connected"
1940     @next_player.status = "connected"
1941     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1942     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1943     @fh.printf("%%TORYO\n")
1944     @fh.print(@board.to_s.gsub(/^/, "\'"))
1945     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1946     @result = GameResultWin.new(@next_player, @current_player)
1947     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1948     @monitors.each do |monitor|
1949       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1950     end
1951   end
1952
1953   def outori_win
1954     @current_player.status = "connected"
1955     @next_player.status = "connected"
1956     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1957     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1958     @fh.print(@board.to_s.gsub(/^/, "\'"))
1959     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1960     @result = GameResultWin.new(@current_player, @next_player)
1961     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1962     @monitors.each do |monitor|
1963       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1964     end
1965   end
1966
1967   def start
1968     log_message(sprintf("game started %s", @id))
1969     @sente.write_safe(sprintf("START:%s\n", @id))
1970     @gote.write_safe(sprintf("START:%s\n", @id))
1971     @sente.mytime = @total_time
1972     @gote.mytime = @total_time
1973     @start_time = Time::new
1974   end
1975
1976   def propose
1977     @fh = open(@logfile, "w")
1978     @fh.sync = true
1979
1980     @fh.puts("V2")
1981     @fh.puts("N+#{@sente.name}")
1982     @fh.puts("N-#{@gote.name}")
1983     @fh.puts("$EVENT:#{@id}")
1984
1985     @sente.write_safe(propose_message("+"))
1986     @gote.write_safe(propose_message("-"))
1987
1988     now = Time::new.strftime("%Y/%m/%d %H:%M:%S")
1989     @fh.puts("$START_TIME:#{now}")
1990     @fh.print <<EOM
1991 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1992 P2 * -HI *  *  *  *  * -KA * 
1993 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1994 P4 *  *  *  *  *  *  *  *  * 
1995 P5 *  *  *  *  *  *  *  *  * 
1996 P6 *  *  *  *  *  *  *  *  * 
1997 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1998 P8 * +KA *  *  *  *  * +HI * 
1999 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
2000 +
2001 EOM
2002   end
2003
2004   def show()
2005     str0 = <<EOM
2006 BEGIN Game_Summary
2007 Protocol_Version:1.1
2008 Protocol_Mode:Server
2009 Format:Shogi 1.0
2010 Declaration:Jishogi 1.1
2011 Game_ID:#{@id}
2012 Name+:#{@sente.name}
2013 Name-:#{@gote.name}
2014 Rematch_On_Draw:NO
2015 To_Move:+
2016 BEGIN Time
2017 Time_Unit:1sec
2018 Total_Time:#{@total_time}
2019 Byoyomi:#{@byoyomi}
2020 Least_Time_Per_Move:#{Least_Time_Per_Move}
2021 Remaining_Time+:#{@sente.mytime}
2022 Remaining_Time-:#{@gote.mytime}
2023 Last_Move:#{@last_move}
2024 Current_Turn:#{@current_turn}
2025 END Time
2026 BEGIN Position
2027 EOM
2028
2029     str1 = <<EOM
2030 END Position
2031 END Game_Summary
2032 EOM
2033
2034     return str0 + @board.to_s + str1
2035   end
2036
2037   def propose_message(sg_flag)
2038     str = <<EOM
2039 BEGIN Game_Summary
2040 Protocol_Version:1.1
2041 Protocol_Mode:Server
2042 Format:Shogi 1.0
2043 Declaration:Jishogi 1.1
2044 Game_ID:#{@id}
2045 Name+:#{@sente.name}
2046 Name-:#{@gote.name}
2047 Your_Turn:#{sg_flag}
2048 Rematch_On_Draw:NO
2049 To_Move:+
2050 BEGIN Time
2051 Time_Unit:1sec
2052 Total_Time:#{@total_time}
2053 Byoyomi:#{@byoyomi}
2054 Least_Time_Per_Move:#{Least_Time_Per_Move}
2055 END Time
2056 BEGIN Position
2057 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
2058 P2 * -HI *  *  *  *  * -KA * 
2059 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
2060 P4 *  *  *  *  *  *  *  *  * 
2061 P5 *  *  *  *  *  *  *  *  * 
2062 P6 *  *  *  *  *  *  *  *  * 
2063 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
2064 P8 * +KA *  *  *  *  * +HI * 
2065 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
2066 P+
2067 P-
2068 +
2069 END Position
2070 END Game_Summary
2071 EOM
2072     return str
2073   end
2074   
2075   private
2076   
2077   def issue_current_time
2078     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
2079     @@mutex.synchronize do
2080       while time <= @@time do
2081         time += 1
2082       end
2083       @@time = time
2084     end
2085   end
2086 end
2087 end # module ShogiServer
2088
2089 #################################################
2090 # MAIN
2091 #
2092
2093 def usage
2094     print <<EOM
2095 NAME
2096         shogi-server - server for CSA server protocol
2097
2098 SYNOPSIS
2099         shogi-server [OPTIONS] event_name port_number
2100
2101 DESCRIPTION
2102         server for CSA server protocol
2103
2104 OPTIONS
2105         --pid-file file
2106                 specify filename for logging process ID
2107     --daemon dir
2108         run as a daemon. Log files will be put in dir.
2109
2110 LICENSE
2111         this file is distributed under GPL version2 and might be compiled by Exerb
2112
2113 SEE ALSO
2114
2115 RELEASE
2116         #{ShogiServer::Release}
2117
2118 REVISION
2119         #{ShogiServer::Revision}
2120 EOM
2121 end
2122
2123 def log_debug(str)
2124   $logger.debug(str)
2125 end
2126
2127 def log_message(str)
2128   $logger.info(str)
2129 end
2130
2131 def log_warning(str)
2132   $logger.warn(str)
2133 end
2134
2135 def log_error(str)
2136   $logger.error(str)
2137 end
2138
2139
2140 def parse_command_line
2141   options = Hash::new
2142   parser = GetoptLong.new( ["--daemon",         GetoptLong::REQUIRED_ARGUMENT],
2143                            ["--pid-file",       GetoptLong::REQUIRED_ARGUMENT]
2144                          )
2145   parser.quiet = true
2146   begin
2147     parser.each_option do |name, arg|
2148       name.sub!(/^--/, '')
2149       options[name] = arg.dup
2150     end
2151   rescue
2152     usage
2153     raise parser.error_message
2154   end
2155   return options
2156 end
2157
2158 def write_pid_file(file)
2159   open(file, "w") do |fh|
2160     fh.puts "#{$$}"
2161   end
2162 end
2163
2164 def mutex_watchdog(mutex, sec)
2165   while true
2166     begin
2167       timeout(sec) do
2168         begin
2169           mutex.lock
2170         ensure
2171           mutex.unlock
2172         end
2173       end
2174       sleep(sec)
2175     rescue TimeoutError
2176       log_error("mutex watchdog timeout")
2177       exit(1)
2178     end
2179   end
2180 end
2181
2182 def main
2183
2184   $mutex = Mutex::new
2185   Thread::start do
2186     Thread.pass
2187     mutex_watchdog($mutex, 10)
2188   end
2189
2190   $options = parse_command_line
2191   if (ARGV.length != 2)
2192     usage
2193     exit 2
2194   end
2195
2196   LEAGUE.event = ARGV.shift
2197   port = ARGV.shift
2198
2199   dir = $options["daemon"]
2200   dir = File.expand_path(dir) if dir
2201   if dir && ! File.exist?(dir)
2202     FileUtils.mkdir(dir)
2203   end
2204   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
2205   $logger = WEBrick::Log.new(log_file)
2206
2207   LEAGUE.dir = dir || File.dirname(__FILE__)
2208   LEAGUE.setup_players_database
2209
2210   config = {}
2211   config[:Port]       = port
2212   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
2213   config[:Logger]     = $logger
2214   if $options["pid-file"]
2215     pid_file = File.expand_path($options["pid-file"])
2216     config[:StartCallback] = Proc.new do
2217       write_pid_file(pid_file)
2218     end
2219     config[:StopCallback] = Proc.new do
2220       FileUtils.rm(pid_file, :force => true)
2221     end
2222   end
2223
2224   server = WEBrick::GenericServer.new(config)
2225   ["INT", "TERM"].each do |signal| 
2226     trap(signal) do
2227       LEAGUE.shutdown
2228       server.shutdown
2229     end
2230   end
2231   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
2232   log_message("server started [Revision: #{ShogiServer::Revision}]")
2233
2234   server.start do |client|
2235       # client.sync = true # this is already set in WEBrick 
2236       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
2237         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
2238       player = nil
2239       login  = nil
2240       while (str = client.gets_timeout(ShogiServer::Login_Time))
2241         begin
2242           $mutex.lock
2243           str =~ /([\r\n]*)$/
2244           eol = $1
2245           if (ShogiServer::Login::good_login?(str))
2246             player = ShogiServer::Player::new(str, client)
2247             player.eol = eol
2248             login  = ShogiServer::Login::factory(str, player)
2249             if (LEAGUE.players[player.name])
2250               if ((LEAGUE.players[player.name].password == player.password) &&
2251                   (LEAGUE.players[player.name].status != "game"))
2252                 log_message(sprintf("user %s login forcely", player.name))
2253                 LEAGUE.players[player.name].kill
2254               else
2255                 login.incorrect_duplicated_player(str)
2256                 #Thread::exit
2257                 #return
2258                 # TODO
2259                 player = nil
2260                 break
2261               end
2262             end
2263             LEAGUE.add(player)
2264             break
2265           else
2266             client.write_safe("LOGIN:incorrect" + eol)
2267             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2268           end
2269         ensure
2270           $mutex.unlock
2271         end
2272       end                       # login loop
2273       if (! player)
2274         #client.close
2275         #Thread::exit
2276         #return
2277         next
2278       end
2279       log_message(sprintf("user %s login", player.name))
2280       login.process
2281       player.run(login.csa_1st_str)
2282       begin
2283         $mutex.lock
2284         if (player.game)
2285           player.game.kill(player)
2286         end
2287         player.finish # socket has been closed
2288         LEAGUE.delete(player)
2289         log_message(sprintf("user %s logout", player.name))
2290       ensure
2291         $mutex.unlock
2292       end
2293   end
2294 end
2295
2296
2297 if ($0 == __FILE__)
2298   STDOUT.sync = true
2299   STDERR.sync = true
2300   TCPSocket.do_not_reverse_lookup = true
2301   Thread.abort_on_exception = false
2302
2303   LEAGUE = ShogiServer::League::new
2304   main
2305 end