OSDN Git Service

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