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.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     
1624     now = Time.now
1625     log_dir_name = File.join(LEAGUE.dir, 
1626                              now.strftime("%Y"),
1627                              now.strftime("%m"),
1628                              now.strftime("%d"))
1629     FileUtils.mkdir_p(log_dir_name) unless File.exist?(log_dir_name)
1630     @logfile = File.join(log_dir_name, @game_id + ".csa")
1631
1632     LEAGUE.games[@game_id] = self
1633
1634     log_message(sprintf("game created %s", @game_id))
1635
1636     @board = Board::new
1637     @board.initial
1638     @start_time = nil
1639     @fh = nil
1640     @result = nil
1641
1642     propose
1643   end
1644   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors
1645   attr_accessor :last_move, :current_turn
1646   attr_reader   :result
1647
1648   def rated?
1649     @sente.rated? && @gote.rated?
1650   end
1651
1652   def turn?(player)
1653     return player.status == "game" && @current_player == player
1654   end
1655
1656   def monitoron(monitor)
1657     @monitors.delete(monitor)
1658     @monitors.push(monitor)
1659   end
1660
1661   def monitoroff(monitor)
1662     @monitors.delete(monitor)
1663   end
1664
1665   def reject(rejector)
1666     @sente.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
1667     @gote.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
1668     finish
1669   end
1670
1671   def kill(killer)
1672     if ["agree_waiting", "start_waiting"].include?(@sente.status)
1673       reject(killer.name)
1674     elsif (@current_player == killer)
1675       abnormal_lose()
1676       finish
1677     end
1678   end
1679
1680   def finish
1681     log_message(sprintf("game finished %s", @game_id))
1682     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1683     @fh.close
1684
1685     @sente.game = nil
1686     @gote.game = nil
1687     @sente.status = "connected"
1688     @gote.status = "connected"
1689
1690     if (@current_player.protocol == LoginCSA::PROTOCOL)
1691       @current_player.finish
1692     end
1693     if (@next_player.protocol == LoginCSA::PROTOCOL)
1694       @next_player.finish
1695     end
1696     @monitors = Array::new
1697     @sente = nil
1698     @gote = nil
1699     @current_player = nil
1700     @next_player = nil
1701     LEAGUE.games.delete(@game_id)
1702   end
1703
1704   # class Game
1705   def handle_one_move(str, player)
1706     unless turn?(player)
1707       return false if str == :timeout
1708
1709       @fh.puts("'Deferred %s" % [str])
1710       log_warning("Deferred a move [%s] scince it is not %s 's turn." %
1711                   [str, player.name])
1712       player.socket_buffer << str # always in the player's thread
1713       return nil
1714     end
1715
1716     finish_flag = true
1717     @end_time = Time::new
1718     t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max
1719     
1720     move_status = nil
1721     if ((@current_player.mytime - t <= -@byoyomi) && 
1722         ((@total_time > 0) || (@byoyomi > 0)))
1723       status = :timeout
1724     elsif (str == :timeout)
1725       return false            # time isn't expired. players aren't swapped. continue game
1726     else
1727       @current_player.mytime -= t
1728       if (@current_player.mytime < 0)
1729         @current_player.mytime = 0
1730       end
1731
1732       move_status = @board.handle_one_move(str, @sente == @current_player)
1733
1734       if [:illegal, :uchifuzume, :oute_kaihimore].include?(move_status)
1735         @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1736       else
1737         if [:normal, :outori, :sennichite, :oute_sennichite_sente_lose, :oute_sennichite_gote_lose].include?(move_status)
1738           # Thinking time includes network traffic
1739           @sente.write_safe(sprintf("%s,T%d\n", str, t))
1740           @gote.write_safe(sprintf("%s,T%d\n", str, t))
1741           @fh.printf("%s\nT%d\n", str, t)
1742           @last_move = sprintf("%s,T%d", str, t)
1743           @current_turn += 1
1744         end
1745
1746         @monitors.each do |monitor|
1747           monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@game_id}] "))
1748           monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @game_id))
1749         end
1750       end
1751     end
1752
1753     if (@next_player.status != "game") # rival is logout or disconnected
1754       abnormal_win()
1755     elsif (status == :timeout)
1756       timeout_lose()
1757     elsif (move_status == :illegal)
1758       illegal_lose()
1759     elsif (move_status == :kachi_win)
1760       kachi_win()
1761     elsif (move_status == :kachi_lose)
1762       kachi_lose()
1763     elsif (move_status == :toryo)
1764       toryo_lose()
1765     elsif (move_status == :outori)
1766       outori_win()
1767     elsif (move_status == :oute_sennichite_sente_lose)
1768       oute_sennichite_win_lose(@gote, @sente) # Sente is checking
1769     elsif (move_status == :oute_sennichite_gote_lose)
1770       oute_sennichite_win_lose(@sente, @gote) # Gote is checking
1771     elsif (move_status == :sennichite)
1772       sennichite_draw()
1773     elsif (move_status == :uchifuzume)
1774       uchifuzume_lose()
1775     elsif (move_status == :oute_kaihimore)
1776       oute_kaihimore_lose()
1777     else
1778       finish_flag = false
1779     end
1780     finish() if finish_flag
1781     @current_player, @next_player = @next_player, @current_player
1782     @start_time = Time::new
1783     return finish_flag
1784   end
1785
1786   def abnormal_win
1787     @current_player.status = "connected"
1788     @next_player.status = "connected"
1789     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1790     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1791     @fh.printf("%%TORYO\n")
1792     @fh.print(@board.to_s.gsub(/^/, "\'"))
1793     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1794     @result = GameResultWin.new(@current_player, @next_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", @game_id))
1798     end
1799   end
1800
1801   def abnormal_lose
1802     @current_player.status = "connected"
1803     @next_player.status = "connected"
1804     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1805     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1806     @fh.printf("%%TORYO\n")
1807     @fh.print(@board.to_s.gsub(/^/, "\'"))
1808     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1809     @result = GameResultWin.new(@next_player, @current_player)
1810     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1811     @monitors.each do |monitor|
1812       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id))
1813     end
1814   end
1815
1816   def sennichite_draw
1817     @current_player.status = "connected"
1818     @next_player.status = "connected"
1819     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1820     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1821     @fh.print(@board.to_s.gsub(/^/, "\'"))
1822     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1823     @result = GameResultDraw.new(@current_player, @next_player)
1824     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1825     @monitors.each do |monitor|
1826       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @game_id))
1827     end
1828   end
1829
1830   def oute_sennichite_win_lose(winner, loser)
1831     @current_player.status = "connected"
1832     @next_player.status = "connected"
1833     loser.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1834     winner.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1835     @fh.print(@board.to_s.gsub(/^/, "\'"))
1836     if loser == @current_player
1837       @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1838     else
1839       @fh.printf("'summary:oute_sennichite:%s win:%s lose\n", @current_player.name, @next_player.name)
1840     end
1841     @result = GameResultWin.new(winner, loser)
1842     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1843     @monitors.each do |monitor|
1844       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @game_id))
1845     end
1846   end
1847
1848   def illegal_lose
1849     @current_player.status = "connected"
1850     @next_player.status = "connected"
1851     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1852     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1853     @fh.print(@board.to_s.gsub(/^/, "\'"))
1854     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1855     @result = GameResultWin.new(@next_player, @current_player)
1856     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1857     @monitors.each do |monitor|
1858       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
1859     end
1860   end
1861
1862   def uchifuzume_lose
1863     @current_player.status = "connected"
1864     @next_player.status = "connected"
1865     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1866     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1867     @fh.print(@board.to_s.gsub(/^/, "\'"))
1868     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1869     @result = GameResultWin.new(@next_player, @current_player)
1870     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1871     @monitors.each do |monitor|
1872       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
1873     end
1874   end
1875
1876   def oute_kaihimore_lose
1877     @current_player.status = "connected"
1878     @next_player.status = "connected"
1879     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1880     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1881     @fh.print(@board.to_s.gsub(/^/, "\'"))
1882     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1883     @result = GameResultWin.new(@next_player, @current_player)
1884     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1885     @monitors.each do |monitor|
1886       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
1887     end
1888   end
1889
1890   def timeout_lose
1891     @current_player.status = "connected"
1892     @next_player.status = "connected"
1893     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1894     @next_player.write_safe("#TIME_UP\n#WIN\n")
1895     @fh.print(@board.to_s.gsub(/^/, "\'"))
1896     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1897     @result = GameResultWin.new(@next_player, @current_player)
1898     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1899     @monitors.each do |monitor|
1900       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @game_id))
1901     end
1902   end
1903
1904   def kachi_win
1905     @current_player.status = "connected"
1906     @next_player.status = "connected"
1907     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1908     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1909     @fh.printf("%%KACHI\n")
1910     @fh.print(@board.to_s.gsub(/^/, "\'"))
1911     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1912     @result = GameResultWin.new(@current_player, @next_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", @game_id))
1916     end
1917   end
1918
1919   def kachi_lose
1920     @current_player.status = "connected"
1921     @next_player.status = "connected"
1922     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1923     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1924     @fh.printf("%%KACHI\n")
1925     @fh.print(@board.to_s.gsub(/^/, "\'"))
1926     @fh.printf("'summary:illegal kachi:%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] %%KACHI\n", @game_id))
1931     end
1932   end
1933
1934   def toryo_lose
1935     @current_player.status = "connected"
1936     @next_player.status = "connected"
1937     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1938     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1939     @fh.printf("%%TORYO\n")
1940     @fh.print(@board.to_s.gsub(/^/, "\'"))
1941     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1942     @result = GameResultWin.new(@next_player, @current_player)
1943     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1944     @monitors.each do |monitor|
1945       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id))
1946     end
1947   end
1948
1949   def outori_win
1950     @current_player.status = "connected"
1951     @next_player.status = "connected"
1952     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1953     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1954     @fh.print(@board.to_s.gsub(/^/, "\'"))
1955     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1956     @result = GameResultWin.new(@current_player, @next_player)
1957     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1958     @monitors.each do |monitor|
1959       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
1960     end
1961   end
1962
1963   def start
1964     log_message(sprintf("game started %s", @game_id))
1965     @sente.write_safe(sprintf("START:%s\n", @game_id))
1966     @gote.write_safe(sprintf("START:%s\n", @game_id))
1967     @sente.mytime = @total_time
1968     @gote.mytime = @total_time
1969     @start_time = Time::new
1970   end
1971
1972   def propose
1973     @fh = open(@logfile, "w")
1974     @fh.sync = true
1975
1976     @fh.puts("V2")
1977     @fh.puts("N+#{@sente.name}")
1978     @fh.puts("N-#{@gote.name}")
1979     @fh.puts("$EVENT:#{@game_id}")
1980
1981     @sente.write_safe(propose_message("+"))
1982     @gote.write_safe(propose_message("-"))
1983
1984     now = Time::new.strftime("%Y/%m/%d %H:%M:%S")
1985     @fh.puts("$START_TIME:#{now}")
1986     @fh.print <<EOM
1987 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1988 P2 * -HI *  *  *  *  * -KA * 
1989 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1990 P4 *  *  *  *  *  *  *  *  * 
1991 P5 *  *  *  *  *  *  *  *  * 
1992 P6 *  *  *  *  *  *  *  *  * 
1993 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1994 P8 * +KA *  *  *  *  * +HI * 
1995 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1996 +
1997 EOM
1998   end
1999
2000   def show()
2001     str0 = <<EOM
2002 BEGIN Game_Summary
2003 Protocol_Version:1.1
2004 Protocol_Mode:Server
2005 Format:Shogi 1.0
2006 Declaration:Jishogi 1.1
2007 Game_ID:#{@game_id}
2008 Name+:#{@sente.name}
2009 Name-:#{@gote.name}
2010 Rematch_On_Draw:NO
2011 To_Move:+
2012 BEGIN Time
2013 Time_Unit:1sec
2014 Total_Time:#{@total_time}
2015 Byoyomi:#{@byoyomi}
2016 Least_Time_Per_Move:#{Least_Time_Per_Move}
2017 Remaining_Time+:#{@sente.mytime}
2018 Remaining_Time-:#{@gote.mytime}
2019 Last_Move:#{@last_move}
2020 Current_Turn:#{@current_turn}
2021 END Time
2022 BEGIN Position
2023 EOM
2024
2025     str1 = <<EOM
2026 END Position
2027 END Game_Summary
2028 EOM
2029
2030     return str0 + @board.to_s + str1
2031   end
2032
2033   def propose_message(sg_flag)
2034     str = <<EOM
2035 BEGIN Game_Summary
2036 Protocol_Version:1.1
2037 Protocol_Mode:Server
2038 Format:Shogi 1.0
2039 Declaration:Jishogi 1.1
2040 Game_ID:#{@game_id}
2041 Name+:#{@sente.name}
2042 Name-:#{@gote.name}
2043 Your_Turn:#{sg_flag}
2044 Rematch_On_Draw:NO
2045 To_Move:+
2046 BEGIN Time
2047 Time_Unit:1sec
2048 Total_Time:#{@total_time}
2049 Byoyomi:#{@byoyomi}
2050 Least_Time_Per_Move:#{Least_Time_Per_Move}
2051 END Time
2052 BEGIN Position
2053 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
2054 P2 * -HI *  *  *  *  * -KA * 
2055 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
2056 P4 *  *  *  *  *  *  *  *  * 
2057 P5 *  *  *  *  *  *  *  *  * 
2058 P6 *  *  *  *  *  *  *  *  * 
2059 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
2060 P8 * +KA *  *  *  *  * +HI * 
2061 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
2062 P+
2063 P-
2064 +
2065 END Position
2066 END Game_Summary
2067 EOM
2068     return str
2069   end
2070   
2071   private
2072   
2073   def issue_current_time
2074     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
2075     @@mutex.synchronize do
2076       while time <= @@time do
2077         time += 1
2078       end
2079       @@time = time
2080     end
2081   end
2082 end
2083 end # module ShogiServer
2084
2085 #################################################
2086 # MAIN
2087 #
2088
2089 def usage
2090     print <<EOM
2091 NAME
2092         shogi-server - server for CSA server protocol
2093
2094 SYNOPSIS
2095         shogi-server [OPTIONS] event_name port_number
2096
2097 DESCRIPTION
2098         server for CSA server protocol
2099
2100 OPTIONS
2101         --pid-file file
2102                 specify filename for logging process ID
2103         --daemon dir
2104                 run as a daemon. Log files will be put in dir.
2105
2106 LICENSE
2107         this file is distributed under GPL version2 and might be compiled by Exerb
2108
2109 SEE ALSO
2110
2111 RELEASE
2112         #{ShogiServer::Release}
2113
2114 REVISION
2115         #{ShogiServer::Revision}
2116 EOM
2117 end
2118
2119 def log_debug(str)
2120   $logger.debug(str)
2121 end
2122
2123 def log_message(str)
2124   $logger.info(str)
2125 end
2126
2127 def log_warning(str)
2128   $logger.warn(str)
2129 end
2130
2131 def log_error(str)
2132   $logger.error(str)
2133 end
2134
2135
2136 def parse_command_line
2137   options = Hash::new
2138   parser = GetoptLong.new(
2139     ["--daemon",   GetoptLong::REQUIRED_ARGUMENT],
2140     ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
2141   parser.quiet = true
2142   begin
2143     parser.each_option do |name, arg|
2144       name.sub!(/^--/, '')
2145       options[name] = arg.dup
2146     end
2147   rescue
2148     usage
2149     raise parser.error_message
2150   end
2151   return options
2152 end
2153
2154 def write_pid_file(file)
2155   open(file, "w") do |fh|
2156     fh.puts "#{$$}"
2157   end
2158 end
2159
2160 def mutex_watchdog(mutex, sec)
2161   while true
2162     begin
2163       timeout(sec) do
2164         begin
2165           mutex.lock
2166         ensure
2167           mutex.unlock
2168         end
2169       end
2170       sleep(sec)
2171     rescue TimeoutError
2172       log_error("mutex watchdog timeout")
2173       exit(1)
2174     end
2175   end
2176 end
2177
2178 def login_loop(client)
2179   player = login = nil
2180  
2181   while r = select([client], nil, nil, ShogiServer::Login_Time) do
2182     break unless str = r[0].first.gets
2183     $mutex.lock # guards LEAGUE
2184     begin
2185       str =~ /([\r\n]*)$/
2186       eol = $1
2187       if (ShogiServer::Login::good_login?(str))
2188         player = ShogiServer::Player::new(str, client, eol)
2189         login  = ShogiServer::Login::factory(str, player)
2190         if (current_player = LEAGUE.find(player.name))
2191           if (current_player.password == player.password &&
2192               current_player.status != "game")
2193             log_message(sprintf("user %s login forcely", player.name))
2194             current_player.kill
2195           else
2196             login.incorrect_duplicated_player(str)
2197             player = nil
2198             break
2199           end
2200         end
2201         LEAGUE.add(player)
2202         break
2203       else
2204         client.write("LOGIN:incorrect" + eol)
2205         client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2206       end
2207     ensure
2208       $mutex.unlock
2209     end
2210   end                       # login loop
2211   return [player, login]
2212 end
2213
2214 def main
2215
2216   $mutex = Mutex::new
2217   Thread::start do
2218     Thread.pass
2219     mutex_watchdog($mutex, 10)
2220   end
2221
2222   $options = parse_command_line
2223   if (ARGV.length != 2)
2224     usage
2225     exit 2
2226   end
2227
2228   LEAGUE.event = ARGV.shift
2229   port = ARGV.shift
2230
2231   dir = $options["daemon"]
2232   dir = File.expand_path(dir) if dir
2233   if dir && ! File.exist?(dir)
2234     FileUtils.mkdir(dir)
2235   end
2236   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
2237   $logger = WEBrick::Log.new(log_file) # thread safe
2238
2239   LEAGUE.dir = dir || File.dirname(__FILE__)
2240   LEAGUE.setup_players_database
2241
2242   config = {}
2243   config[:Port]       = port
2244   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
2245   config[:Logger]     = $logger
2246   if $options["pid-file"]
2247     pid_file = File.expand_path($options["pid-file"])
2248     config[:StartCallback] = Proc.new do
2249       write_pid_file(pid_file)
2250     end
2251     config[:StopCallback] = Proc.new do
2252       FileUtils.rm(pid_file, :force => true)
2253     end
2254   end
2255
2256   server = WEBrick::GenericServer.new(config)
2257   ["INT", "TERM"].each do |signal| 
2258     trap(signal) do
2259       LEAGUE.shutdown
2260       server.shutdown
2261     end
2262   end
2263   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
2264   log_message("server started [Revision: #{ShogiServer::Revision}]")
2265
2266   server.start do |client|
2267       # client.sync = true # this is already set in WEBrick 
2268       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
2269         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
2270       player, login = login_loop(client) # loop
2271       next unless player
2272
2273       log_message(sprintf("user %s login", player.name))
2274       login.process
2275       player.run(login.csa_1st_str) # loop
2276       begin
2277         $mutex.lock
2278         if (player.game)
2279           player.game.kill(player)
2280         end
2281         player.finish # socket has been closed
2282         LEAGUE.delete(player)
2283         log_message(sprintf("user %s logout", player.name))
2284       ensure
2285         $mutex.unlock
2286       end
2287   end
2288 end
2289
2290
2291 if ($0 == __FILE__)
2292   STDOUT.sync = true
2293   STDERR.sync = true
2294   TCPSocket.do_not_reverse_lookup = true
2295   Thread.abort_on_exception = $DEBUG ? true : false
2296
2297   LEAGUE = ShogiServer::League::new
2298   main
2299 end