OSDN Git Service

An illegal teban specified for Floodgate login could crash the server.
[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 != 1) &&
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     rival_ou.movable_grids.each do |(cand_x, cand_y)|
1313       tmp_board = Marshal.load(Marshal.dump(self))
1314       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1315       raise "internal error" if (s != true)
1316       if (! tmp_board.checkmated?(! sente)) # good move
1317         return false
1318       end
1319     end
1320
1321     ## case: rival is capturing fu
1322     x = 1
1323     while (x <= 9)
1324       y = 1
1325       while (y <= 9)
1326         if (@array[x][y] &&
1327             (@array[x][y].sente != sente) &&
1328             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1329           
1330           names = []
1331           if (@array[x][y].promoted)
1332             names << @array[x][y].promoted_name
1333           else
1334             names << @array[x][y].name
1335             if @array[x][y].promoted_name && 
1336                @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name)
1337               names << @array[x][y].promoted_name 
1338             end
1339           end
1340           names.map! do |name|
1341             tmp_board = Marshal.load(Marshal.dump(self))
1342             s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1343             if s == :illegal
1344               s # result
1345             else
1346               tmp_board.checkmated?(! sente) # result
1347             end
1348           end
1349           all_illegal = names.find {|a| a != :illegal}
1350           raise "internal error: legal move not found" if all_illegal == nil
1351           r = names.find {|a| a == false} # good move
1352           return false if r == false # found good move
1353         end
1354         y = y + 1
1355       end
1356       x = x + 1
1357     end
1358     return true
1359   end
1360
1361   # @[sente|gote]_history has at least one item while the player is checking the other or 
1362   # the other escapes.
1363   def update_sennichite(player)
1364     str = to_s
1365     @history[str] += 1
1366     if checkmated?(!player)
1367       if (player)
1368         @sente_history["dummy"] = 1  # flag to see Sente player is checking Gote player
1369       else
1370         @gote_history["dummy"]  = 1  # flag to see Gote player is checking Sente player
1371       end
1372     else
1373       if (player)
1374         @sente_history.clear # no more continuous check
1375       else
1376         @gote_history.clear  # no more continuous check
1377       end
1378     end
1379     if @sente_history.size > 0  # possible for Sente's or Gote's turn
1380       @sente_history[str] += 1
1381     end
1382     if @gote_history.size > 0   # possible for Sente's or Gote's turn
1383       @gote_history[str] += 1
1384     end
1385   end
1386
1387   def oute_sennichite?(player)
1388     if (@sente_history[to_s] >= 4)
1389       return :oute_sennichite_sente_lose
1390     elsif (@gote_history[to_s] >= 4)
1391       return :oute_sennichite_gote_lose
1392     else
1393       return nil
1394     end
1395   end
1396
1397   def sennichite?(sente)
1398     if (@history[to_s] >= 4) # already 3 times
1399       return true
1400     end
1401     return false
1402   end
1403
1404   def good_kachi?(sente)
1405     if (checkmated?(sente))
1406       puts "'NG: Checkmating." if $DEBUG
1407       return false 
1408     end
1409     
1410     ou = look_for_ou(sente)
1411     if (sente && (ou.y >= 4))
1412       puts "'NG: Black's OU does not enter yet." if $DEBUG
1413       return false     
1414     end  
1415     if (! sente && (ou.y <= 6))
1416       puts "'NG: White's OU does not enter yet." if $DEBUG
1417       return false 
1418     end
1419       
1420     number = 0
1421     point = 0
1422
1423     if (sente)
1424       hands = @sente_hands
1425       r = [1, 2, 3]
1426     else
1427       hands = @gote_hands
1428       r = [7, 8, 9]
1429     end
1430     r.each do |y|
1431       x = 1
1432       while (x <= 9)
1433         if (@array[x][y] &&
1434             (@array[x][y].sente == sente) &&
1435             (@array[x][y].point > 0))
1436           point = point + @array[x][y].point
1437           number = number + 1
1438         end
1439         x = x + 1
1440       end
1441     end
1442     hands.each do |piece|
1443       point = point + piece.point
1444     end
1445
1446     if (number < 10)
1447       puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1448       return false     
1449     end  
1450     if (sente)
1451       if (point < 28)
1452         puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1453         return false 
1454       end  
1455     else
1456       if (point < 27)
1457         puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1458         return false 
1459       end
1460     end
1461
1462     puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1463     return true
1464   end
1465
1466   # sente is nil only if tests in test_board run
1467   def handle_one_move(str, sente=nil)
1468     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1469       sg = $1
1470       x0 = $2.to_i
1471       y0 = $3.to_i
1472       x1 = $4.to_i
1473       y1 = $5.to_i
1474       name = $6
1475     elsif (str =~ /^%KACHI/)
1476       raise ArgumentError, "sente is null", caller if sente == nil
1477       if (good_kachi?(sente))
1478         return :kachi_win
1479       else
1480         return :kachi_lose
1481       end
1482     elsif (str =~ /^%TORYO/)
1483       return :toryo
1484     else
1485       return :illegal
1486     end
1487     
1488     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1489         ((x0 != 0) || (y0 != 0)))
1490       return :illegal
1491     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1492       return :illegal
1493     end
1494     
1495
1496     if (sg == "+")
1497       sente = true if sente == nil           # deprecated
1498       return :illegal unless sente == true   # black player's move must be black
1499       hands = @sente_hands
1500     else
1501       sente = false if sente == nil          # deprecated
1502       return :illegal unless sente == false  # white player's move must be white
1503       hands = @gote_hands
1504     end
1505     
1506     ## source check
1507     if ((x0 == 0) && (y0 == 0))
1508       return :illegal if (! have_piece?(hands, name))
1509     elsif (! @array[x0][y0])
1510       return :illegal           # no piece
1511     elsif (@array[x0][y0].sente != sente)
1512       return :illegal           # this is not mine
1513     elsif (@array[x0][y0].name != name)
1514       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1515     end
1516
1517     ## destination check
1518     if (@array[x1][y1] &&
1519         (@array[x1][y1].sente == sente)) # can't capture mine
1520       return :illegal
1521     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1522       return :illegal           # can't put on existing piece
1523     end
1524
1525     tmp_board = Marshal.load(Marshal.dump(self))
1526     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1527     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1528     tmp_board.update_sennichite(sente)
1529     os_result = tmp_board.oute_sennichite?(sente)
1530     return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
1531     return :sennichite if tmp_board.sennichite?(sente)
1532
1533     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1534       return :uchifuzume
1535     end
1536
1537     move_to(x0, y0, x1, y1, name, sente)
1538     str = to_s
1539
1540     update_sennichite(sente)
1541     return :normal
1542   end
1543
1544   def to_s
1545     a = Array::new
1546     y = 1
1547     while (y <= 9)
1548       a.push(sprintf("P%d", y))
1549       x = 9
1550       while (x >= 1)
1551         piece = @array[x][y]
1552         if (piece)
1553           s = piece.to_s
1554         else
1555           s = " * "
1556         end
1557         a.push(s)
1558         x = x - 1
1559       end
1560       a.push(sprintf("\n"))
1561       y = y + 1
1562     end
1563     if (! sente_hands.empty?)
1564       a.push("P+")
1565       sente_hands.each do |p|
1566         a.push("00" + p.name)
1567       end
1568       a.push("\n")
1569     end
1570     if (! gote_hands.empty?)
1571       a.push("P-")
1572       gote_hands.each do |p|
1573         a.push("00" + p.name)
1574       end
1575       a.push("\n")
1576     end
1577     a.push("+\n")
1578     return a.join
1579   end
1580 end
1581
1582 class GameResult
1583   attr_reader :players, :black, :white
1584
1585   def initialize(p1, p2)
1586     @players = [p1, p2]
1587     if p1.sente && !p2.sente
1588       @black, @white = p1, p2
1589     elsif !p1.sente && p2.sente
1590       @black, @white = p2, p1
1591     else
1592       raise "Never reached!"
1593     end
1594   end
1595 end
1596
1597 class GameResultWin < GameResult
1598   attr_reader :winner, :loser
1599
1600   def initialize(winner, loser)
1601     super
1602     @winner, @loser = winner, loser
1603     @winner.last_game_win = true
1604     @loser.last_game_win  = false
1605   end
1606
1607   def to_s
1608     black_name = @black.id || @black.name
1609     white_name = @white.id || @white.name
1610     "%s:%s" % [black_name, white_name]
1611   end
1612 end
1613
1614 class GameResultDraw < GameResult
1615   def initialize(p1, p2)
1616     super
1617     p1.last_game_win = false
1618     p2.last_game_win = false
1619   end
1620 end
1621
1622 class Game
1623   @@mutex = Mutex.new
1624   @@time  = 0
1625
1626   def initialize(game_name, player0, player1)
1627     @monitors = Array::new
1628     @game_name = game_name
1629     if (@game_name =~ /-(\d+)-(\d+)$/)
1630       @total_time = $1.to_i
1631       @byoyomi = $2.to_i
1632     end
1633
1634     if (player0.sente)
1635       @sente = player0
1636       @gote = player1
1637     else
1638       @sente = player1
1639       @gote = player0
1640     end
1641     @current_player = @sente
1642     @next_player = @gote
1643
1644     @sente.game = self
1645     @gote.game = self
1646
1647     @last_move = ""
1648     @current_turn = 0
1649
1650     @sente.status = "agree_waiting"
1651     @gote.status = "agree_waiting"
1652     
1653     @id = sprintf("%s+%s+%s+%s+%s", 
1654                   LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1655     @logfile = File.join(LEAGUE.dir, @id + ".csa")
1656
1657     LEAGUE.games[@id] = self
1658
1659     log_message(sprintf("game created %s", @id))
1660
1661     @board = Board::new
1662     @board.initial
1663     @start_time = nil
1664     @fh = nil
1665     @result = nil
1666
1667     propose
1668   end
1669   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1670   attr_accessor :last_move, :current_turn
1671   attr_reader   :result
1672
1673   def rated?
1674     @sente.rated? && @gote.rated?
1675   end
1676
1677   def monitoron(monitor)
1678     @monitors.delete(monitor)
1679     @monitors.push(monitor)
1680   end
1681
1682   def monitoroff(monitor)
1683     @monitors.delete(monitor)
1684   end
1685
1686   def reject(rejector)
1687     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1688     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1689     finish
1690   end
1691
1692   def kill(killer)
1693     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1694       reject(killer.name)
1695     elsif (@current_player == killer)
1696       abnormal_lose()
1697       finish
1698     end
1699   end
1700
1701   def finish
1702     log_message(sprintf("game finished %s", @id))
1703     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1704     @fh.close
1705
1706     @sente.game = nil
1707     @gote.game = nil
1708     @sente.status = "connected"
1709     @gote.status = "connected"
1710
1711     if (@current_player.protocol == LoginCSA::PROTOCOL)
1712       @current_player.finish
1713     end
1714     if (@next_player.protocol == LoginCSA::PROTOCOL)
1715       @next_player.finish
1716     end
1717     @monitors = Array::new
1718     @sente = nil
1719     @gote = nil
1720     @current_player = nil
1721     @next_player = nil
1722     LEAGUE.games.delete(@id)
1723   end
1724
1725   def handle_one_move(str, player)
1726     finish_flag = true
1727     if (@current_player == player)
1728       @end_time = Time::new
1729       t = (@end_time - @start_time).floor
1730       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1731       
1732       move_status = nil
1733       if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1734         status = :timeout
1735       elsif (str == :timeout)
1736         return false            # time isn't expired. players aren't swapped. continue game
1737       else
1738         @current_player.mytime = @current_player.mytime - t
1739         if (@current_player.mytime < 0)
1740           @current_player.mytime = 0
1741         end
1742
1743 #        begin
1744           move_status = @board.handle_one_move(str, @sente == @current_player)
1745 #        rescue
1746 #          log_error("handle_one_move raise exception for #{str}")
1747 #          move_status = :illegal
1748 #        end
1749
1750         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1751           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1752         else
1753           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite_sente_lose) || (move_status == :oute_sennichite_gote_lose))
1754             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1755             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1756             @fh.printf("%s\nT%d\n", str, t)
1757             @last_move = sprintf("%s,T%d", str, t)
1758             @current_turn = @current_turn + 1
1759           end
1760
1761           @monitors.each do |monitor|
1762             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1763             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1764           end
1765         end
1766       end
1767
1768       if (@next_player.status != "game") # rival is logout or disconnected
1769         abnormal_win()
1770       elsif (status == :timeout)
1771         timeout_lose()
1772       elsif (move_status == :illegal)
1773         illegal_lose()
1774       elsif (move_status == :kachi_win)
1775         kachi_win()
1776       elsif (move_status == :kachi_lose)
1777         kachi_lose()
1778       elsif (move_status == :toryo)
1779         toryo_lose()
1780       elsif (move_status == :outori)
1781         outori_win()
1782       elsif (move_status == :oute_sennichite_sente_lose)
1783         oute_sennichite_win_lose(@gote, @sente) # Sente is checking
1784       elsif (move_status == :oute_sennichite_gote_lose)
1785         oute_sennichite_win_lose(@sente, @gote) # Gote is checking
1786       elsif (move_status == :sennichite)
1787         sennichite_draw()
1788       elsif (move_status == :uchifuzume)
1789         uchifuzume_lose()
1790       elsif (move_status == :oute_kaihimore)
1791         oute_kaihimore_lose()
1792       else
1793         finish_flag = false
1794       end
1795       finish() if finish_flag
1796       (@current_player, @next_player) = [@next_player, @current_player]
1797       @start_time = Time::new
1798       return finish_flag
1799     end
1800   end
1801
1802   def abnormal_win
1803     @current_player.status = "connected"
1804     @next_player.status = "connected"
1805     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1806     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1807     @fh.printf("%%TORYO\n")
1808     @fh.print(@board.to_s.gsub(/^/, "\'"))
1809     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1810     @result = GameResultWin.new(@current_player, @next_player)
1811     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1812     @monitors.each do |monitor|
1813       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1814     end
1815   end
1816
1817   def abnormal_lose
1818     @current_player.status = "connected"
1819     @next_player.status = "connected"
1820     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1821     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1822     @fh.printf("%%TORYO\n")
1823     @fh.print(@board.to_s.gsub(/^/, "\'"))
1824     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1825     @result = GameResultWin.new(@next_player, @current_player)
1826     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1827     @monitors.each do |monitor|
1828       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1829     end
1830   end
1831
1832   def sennichite_draw
1833     @current_player.status = "connected"
1834     @next_player.status = "connected"
1835     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1836     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1837     @fh.print(@board.to_s.gsub(/^/, "\'"))
1838     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1839     @result = GameResultDraw.new(@current_player, @next_player)
1840     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1841     @monitors.each do |monitor|
1842       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1843     end
1844   end
1845
1846   def oute_sennichite_win_lose(winner, loser)
1847     @current_player.status = "connected"
1848     @next_player.status = "connected"
1849     loser.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1850     winner.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1851     @fh.print(@board.to_s.gsub(/^/, "\'"))
1852     if loser == @current_player
1853       @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1854     else
1855       @fh.printf("'summary:oute_sennichite:%s win:%s lose\n", @current_player.name, @next_player.name)
1856     end
1857     @result = GameResultWin.new(winner, loser)
1858     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1859     @monitors.each do |monitor|
1860       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1861     end
1862   end
1863
1864   def illegal_lose
1865     @current_player.status = "connected"
1866     @next_player.status = "connected"
1867     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1868     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1869     @fh.print(@board.to_s.gsub(/^/, "\'"))
1870     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1871     @result = GameResultWin.new(@next_player, @current_player)
1872     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1873     @monitors.each do |monitor|
1874       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1875     end
1876   end
1877
1878   def uchifuzume_lose
1879     @current_player.status = "connected"
1880     @next_player.status = "connected"
1881     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1882     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1883     @fh.print(@board.to_s.gsub(/^/, "\'"))
1884     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1885     @result = GameResultWin.new(@next_player, @current_player)
1886     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1887     @monitors.each do |monitor|
1888       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1889     end
1890   end
1891
1892   def oute_kaihimore_lose
1893     @current_player.status = "connected"
1894     @next_player.status = "connected"
1895     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1896     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1897     @fh.print(@board.to_s.gsub(/^/, "\'"))
1898     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1899     @result = GameResultWin.new(@next_player, @current_player)
1900     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1901     @monitors.each do |monitor|
1902       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1903     end
1904   end
1905
1906   def timeout_lose
1907     @current_player.status = "connected"
1908     @next_player.status = "connected"
1909     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1910     @next_player.write_safe("#TIME_UP\n#WIN\n")
1911     @fh.print(@board.to_s.gsub(/^/, "\'"))
1912     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1913     @result = GameResultWin.new(@next_player, @current_player)
1914     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1915     @monitors.each do |monitor|
1916       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1917     end
1918   end
1919
1920   def kachi_win
1921     @current_player.status = "connected"
1922     @next_player.status = "connected"
1923     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1924     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1925     @fh.printf("%%KACHI\n")
1926     @fh.print(@board.to_s.gsub(/^/, "\'"))
1927     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1928     @result = GameResultWin.new(@current_player, @next_player)
1929     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1930     @monitors.each do |monitor|
1931       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1932     end
1933   end
1934
1935   def kachi_lose
1936     @current_player.status = "connected"
1937     @next_player.status = "connected"
1938     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1939     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1940     @fh.printf("%%KACHI\n")
1941     @fh.print(@board.to_s.gsub(/^/, "\'"))
1942     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1943     @result = GameResultWin.new(@next_player, @current_player)
1944     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1945     @monitors.each do |monitor|
1946       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1947     end
1948   end
1949
1950   def toryo_lose
1951     @current_player.status = "connected"
1952     @next_player.status = "connected"
1953     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1954     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1955     @fh.printf("%%TORYO\n")
1956     @fh.print(@board.to_s.gsub(/^/, "\'"))
1957     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1958     @result = GameResultWin.new(@next_player, @current_player)
1959     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1960     @monitors.each do |monitor|
1961       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1962     end
1963   end
1964
1965   def outori_win
1966     @current_player.status = "connected"
1967     @next_player.status = "connected"
1968     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1969     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1970     @fh.print(@board.to_s.gsub(/^/, "\'"))
1971     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1972     @result = GameResultWin.new(@current_player, @next_player)
1973     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1974     @monitors.each do |monitor|
1975       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1976     end
1977   end
1978
1979   def start
1980     log_message(sprintf("game started %s", @id))
1981     @sente.write_safe(sprintf("START:%s\n", @id))
1982     @gote.write_safe(sprintf("START:%s\n", @id))
1983     @sente.mytime = @total_time
1984     @gote.mytime = @total_time
1985     @start_time = Time::new
1986   end
1987
1988   def propose
1989     @fh = open(@logfile, "w")
1990     @fh.sync = true
1991
1992     @fh.puts("V2")
1993     @fh.puts("N+#{@sente.name}")
1994     @fh.puts("N-#{@gote.name}")
1995     @fh.puts("$EVENT:#{@id}")
1996
1997     @sente.write_safe(propose_message("+"))
1998     @gote.write_safe(propose_message("-"))
1999
2000     now = Time::new.strftime("%Y/%m/%d %H:%M:%S")
2001     @fh.puts("$START_TIME:#{now}")
2002     @fh.print <<EOM
2003 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
2004 P2 * -HI *  *  *  *  * -KA * 
2005 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
2006 P4 *  *  *  *  *  *  *  *  * 
2007 P5 *  *  *  *  *  *  *  *  * 
2008 P6 *  *  *  *  *  *  *  *  * 
2009 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
2010 P8 * +KA *  *  *  *  * +HI * 
2011 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
2012 +
2013 EOM
2014   end
2015
2016   def show()
2017     str0 = <<EOM
2018 BEGIN Game_Summary
2019 Protocol_Version:1.1
2020 Protocol_Mode:Server
2021 Format:Shogi 1.0
2022 Declaration:Jishogi 1.1
2023 Game_ID:#{@id}
2024 Name+:#{@sente.name}
2025 Name-:#{@gote.name}
2026 Rematch_On_Draw:NO
2027 To_Move:+
2028 BEGIN Time
2029 Time_Unit:1sec
2030 Total_Time:#{@total_time}
2031 Byoyomi:#{@byoyomi}
2032 Least_Time_Per_Move:#{Least_Time_Per_Move}
2033 Remaining_Time+:#{@sente.mytime}
2034 Remaining_Time-:#{@gote.mytime}
2035 Last_Move:#{@last_move}
2036 Current_Turn:#{@current_turn}
2037 END Time
2038 BEGIN Position
2039 EOM
2040
2041     str1 = <<EOM
2042 END Position
2043 END Game_Summary
2044 EOM
2045
2046     return str0 + @board.to_s + str1
2047   end
2048
2049   def propose_message(sg_flag)
2050     str = <<EOM
2051 BEGIN Game_Summary
2052 Protocol_Version:1.1
2053 Protocol_Mode:Server
2054 Format:Shogi 1.0
2055 Declaration:Jishogi 1.1
2056 Game_ID:#{@id}
2057 Name+:#{@sente.name}
2058 Name-:#{@gote.name}
2059 Your_Turn:#{sg_flag}
2060 Rematch_On_Draw:NO
2061 To_Move:+
2062 BEGIN Time
2063 Time_Unit:1sec
2064 Total_Time:#{@total_time}
2065 Byoyomi:#{@byoyomi}
2066 Least_Time_Per_Move:#{Least_Time_Per_Move}
2067 END Time
2068 BEGIN Position
2069 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
2070 P2 * -HI *  *  *  *  * -KA * 
2071 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
2072 P4 *  *  *  *  *  *  *  *  * 
2073 P5 *  *  *  *  *  *  *  *  * 
2074 P6 *  *  *  *  *  *  *  *  * 
2075 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
2076 P8 * +KA *  *  *  *  * +HI * 
2077 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
2078 P+
2079 P-
2080 +
2081 END Position
2082 END Game_Summary
2083 EOM
2084     return str
2085   end
2086   
2087   private
2088   
2089   def issue_current_time
2090     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
2091     @@mutex.synchronize do
2092       while time <= @@time do
2093         time += 1
2094       end
2095       @@time = time
2096     end
2097   end
2098 end
2099 end # module ShogiServer
2100
2101 #################################################
2102 # MAIN
2103 #
2104
2105 def usage
2106     print <<EOM
2107 NAME
2108         shogi-server - server for CSA server protocol
2109
2110 SYNOPSIS
2111         shogi-server [OPTIONS] event_name port_number
2112
2113 DESCRIPTION
2114         server for CSA server protocol
2115
2116 OPTIONS
2117         --pid-file file
2118                 specify filename for logging process ID
2119     --daemon dir
2120         run as a daemon. Log files will be put in dir.
2121
2122 LICENSE
2123         this file is distributed under GPL version2 and might be compiled by Exerb
2124
2125 SEE ALSO
2126
2127 RELEASE
2128         #{ShogiServer::Release}
2129
2130 REVISION
2131         #{ShogiServer::Revision}
2132 EOM
2133 end
2134
2135 def log_debug(str)
2136   $logger.debug(str)
2137 end
2138
2139 def log_message(str)
2140   $logger.info(str)
2141 end
2142
2143 def log_warning(str)
2144   $logger.warn(str)
2145 end
2146
2147 def log_error(str)
2148   $logger.error(str)
2149 end
2150
2151
2152 def parse_command_line
2153   options = Hash::new
2154   parser = GetoptLong.new( ["--daemon",         GetoptLong::REQUIRED_ARGUMENT],
2155                            ["--pid-file",       GetoptLong::REQUIRED_ARGUMENT]
2156                          )
2157   parser.quiet = true
2158   begin
2159     parser.each_option do |name, arg|
2160       name.sub!(/^--/, '')
2161       options[name] = arg.dup
2162     end
2163   rescue
2164     usage
2165     raise parser.error_message
2166   end
2167   return options
2168 end
2169
2170 def write_pid_file(file)
2171   open(file, "w") do |fh|
2172     fh.puts "#{$$}"
2173   end
2174 end
2175
2176 def mutex_watchdog(mutex, sec)
2177   while true
2178     begin
2179       timeout(sec) do
2180         begin
2181           mutex.lock
2182         ensure
2183           mutex.unlock
2184         end
2185       end
2186       sleep(sec)
2187     rescue TimeoutError
2188       log_error("mutex watchdog timeout")
2189       exit(1)
2190     end
2191   end
2192 end
2193
2194 def main
2195
2196   $mutex = Mutex::new
2197   Thread::start do
2198     Thread.pass
2199     mutex_watchdog($mutex, 10)
2200   end
2201
2202   $options = parse_command_line
2203   if (ARGV.length != 2)
2204     usage
2205     exit 2
2206   end
2207
2208   LEAGUE.event = ARGV.shift
2209   port = ARGV.shift
2210
2211   dir = $options["daemon"]
2212   dir = File.expand_path(dir) if dir
2213   if dir && ! File.exist?(dir)
2214     FileUtils.mkdir(dir)
2215   end
2216   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
2217   $logger = WEBrick::Log.new(log_file)
2218
2219   LEAGUE.dir = dir || File.dirname(__FILE__)
2220   LEAGUE.setup_players_database
2221
2222   config = {}
2223   config[:Port]       = port
2224   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
2225   config[:Logger]     = $logger
2226   if $options["pid-file"]
2227     pid_file = File.expand_path($options["pid-file"])
2228     config[:StartCallback] = Proc.new do
2229       write_pid_file(pid_file)
2230     end
2231     config[:StopCallback] = Proc.new do
2232       FileUtils.rm(pid_file, :force => true)
2233     end
2234   end
2235
2236   server = WEBrick::GenericServer.new(config)
2237   ["INT", "TERM"].each do |signal| 
2238     trap(signal) do
2239       LEAGUE.shutdown
2240       server.shutdown
2241     end
2242   end
2243   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
2244   log_message("server started [Revision: #{ShogiServer::Revision}]")
2245
2246   server.start do |client|
2247       # client.sync = true # this is already set in WEBrick 
2248       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
2249         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
2250       player = nil
2251       login  = nil
2252       while (str = client.gets_timeout(ShogiServer::Login_Time))
2253         begin
2254           $mutex.lock
2255           str =~ /([\r\n]*)$/
2256           eol = $1
2257           if (ShogiServer::Login::good_login?(str))
2258             player = ShogiServer::Player::new(str, client)
2259             player.eol = eol
2260             login  = ShogiServer::Login::factory(str, player)
2261             if (LEAGUE.players[player.name])
2262               if ((LEAGUE.players[player.name].password == player.password) &&
2263                   (LEAGUE.players[player.name].status != "game"))
2264                 log_message(sprintf("user %s login forcely", player.name))
2265                 LEAGUE.players[player.name].kill
2266               else
2267                 login.incorrect_duplicated_player(str)
2268                 #Thread::exit
2269                 #return
2270                 # TODO
2271                 player = nil
2272                 break
2273               end
2274             end
2275             LEAGUE.add(player)
2276             break
2277           else
2278             client.write_safe("LOGIN:incorrect" + eol)
2279             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2280           end
2281         ensure
2282           $mutex.unlock
2283         end
2284       end                       # login loop
2285       if (! player)
2286         #client.close
2287         #Thread::exit
2288         #return
2289         next
2290       end
2291       log_message(sprintf("user %s login", player.name))
2292       login.process
2293       player.run(login.csa_1st_str)
2294       begin
2295         $mutex.lock
2296         if (player.game)
2297           player.game.kill(player)
2298         end
2299         player.finish # socket has been closed
2300         LEAGUE.delete(player)
2301         log_message(sprintf("user %s logout", player.name))
2302       ensure
2303         $mutex.unlock
2304       end
2305   end
2306 end
2307
2308
2309 if ($0 == __FILE__)
2310   STDOUT.sync = true
2311   STDERR.sync = true
2312   TCPSocket.do_not_reverse_lookup = true
2313   Thread.abort_on_exception = false
2314
2315   LEAGUE = ShogiServer::League::new
2316   main
2317 end