OSDN Git Service

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