OSDN Git Service

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