OSDN Git Service

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