OSDN Git Service

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