OSDN Git Service

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