OSDN Git Service

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