OSDN Git Service

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