OSDN Git Service

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