OSDN Git Service

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