OSDN Git Service

232c8f5bd84db67797c469784fed1e6a0c87dd92
[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   end
1128   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
1129   attr_reader :move_count
1130
1131   def initial
1132     PieceKY::new(self, 1, 1, false)
1133     PieceKE::new(self, 2, 1, false)
1134     PieceGI::new(self, 3, 1, false)
1135     PieceKI::new(self, 4, 1, false)
1136     PieceOU::new(self, 5, 1, false)
1137     PieceKI::new(self, 6, 1, false)
1138     PieceGI::new(self, 7, 1, false)
1139     PieceKE::new(self, 8, 1, false)
1140     PieceKY::new(self, 9, 1, false)
1141     PieceKA::new(self, 2, 2, false)
1142     PieceHI::new(self, 8, 2, false)
1143     (1..9).each do |i|
1144       PieceFU::new(self, i, 3, false)
1145     end
1146
1147     PieceKY::new(self, 1, 9, true)
1148     PieceKE::new(self, 2, 9, true)
1149     PieceGI::new(self, 3, 9, true)
1150     PieceKI::new(self, 4, 9, true)
1151     PieceOU::new(self, 5, 9, true)
1152     PieceKI::new(self, 6, 9, true)
1153     PieceGI::new(self, 7, 9, true)
1154     PieceKE::new(self, 8, 9, true)
1155     PieceKY::new(self, 9, 9, true)
1156     PieceKA::new(self, 8, 8, true)
1157     PieceHI::new(self, 2, 8, true)
1158     (1..9).each do |i|
1159       PieceFU::new(self, i, 7, true)
1160     end
1161   end
1162
1163   def have_piece?(hands, name)
1164     piece = hands.find { |i|
1165       i.name == name
1166     }
1167     return piece
1168   end
1169
1170   def move_to(x0, y0, x1, y1, name, sente)
1171     if (sente)
1172       hands = @sente_hands
1173     else
1174       hands = @gote_hands
1175     end
1176
1177     if ((x0 == 0) || (y0 == 0))
1178       piece = have_piece?(hands, name)
1179       return :illegal if (! piece.move_to?(x1, y1, name)) # TODO null check for the piece?
1180       piece.move_to(x1, y1)
1181     else
1182       return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))  # TODO null check?
1183       if (@array[x0][y0].name != name) # promoted ?
1184         @array[x0][y0].promoted = true
1185       end
1186       if (@array[x1][y1]) # capture
1187         if (@array[x1][y1].name == "OU")
1188           return :outori        # return board update
1189         end
1190         @array[x1][y1].sente = @array[x0][y0].sente
1191         @array[x1][y1].move_to(0, 0)
1192         hands.sort! {|a, b| # TODO refactor. Move to Piece class
1193           a.name <=> b.name
1194         }
1195       end
1196       @array[x0][y0].move_to(x1, y1)
1197     end
1198     @move_count += 1
1199     return true
1200   end
1201
1202   def look_for_ou(sente)
1203     x = 1
1204     while (x <= 9)
1205       y = 1
1206       while (y <= 9)
1207         if (@array[x][y] &&
1208             (@array[x][y].name == "OU") &&
1209             (@array[x][y].sente == sente))
1210           return @array[x][y]
1211         end
1212         y = y + 1
1213       end
1214       x = x + 1
1215     end
1216     raise "can't find ou"
1217   end
1218
1219   # note checkmate, but check. sente is checked.
1220   def checkmated?(sente)        # sente is loosing
1221     ou = look_for_ou(sente)
1222     x = 1
1223     while (x <= 9)
1224       y = 1
1225       while (y <= 9)
1226         if (@array[x][y] &&
1227             (@array[x][y].sente != sente))
1228           if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1229             return true
1230           end
1231         end
1232         y = y + 1
1233       end
1234       x = x + 1
1235     end
1236     return false
1237   end
1238
1239   def uchifuzume?(sente)
1240     rival_ou = look_for_ou(! sente)   # rival's ou
1241     if (sente)                  # rival is gote
1242       if ((rival_ou.y != 9) &&
1243           (@array[rival_ou.x][rival_ou.y + 1]) &&
1244           (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1245           (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1246         fu_x = rival_ou.x
1247         fu_y = rival_ou.y + 1
1248       else
1249         return false
1250       end
1251     else                        # gote
1252       if ((rival_ou.y != 1) &&
1253           (@array[rival_ou.x][rival_ou.y - 1]) &&
1254           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1255           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1256         fu_x = rival_ou.x
1257         fu_y = rival_ou.y - 1
1258       else
1259         return false
1260       end
1261     end
1262
1263     ## case: rival_ou is moving
1264     rival_ou.movable_grids.each do |(cand_x, cand_y)|
1265       tmp_board = Marshal.load(Marshal.dump(self))
1266       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1267       raise "internal error" if (s != true)
1268       if (! tmp_board.checkmated?(! sente)) # good move
1269         return false
1270       end
1271     end
1272
1273     ## case: rival is capturing fu
1274     x = 1
1275     while (x <= 9)
1276       y = 1
1277       while (y <= 9)
1278         if (@array[x][y] &&
1279             (@array[x][y].sente != sente) &&
1280             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1281           
1282           names = []
1283           if (@array[x][y].promoted)
1284             names << @array[x][y].promoted_name
1285           else
1286             names << @array[x][y].name
1287             if @array[x][y].promoted_name && 
1288                @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name)
1289               names << @array[x][y].promoted_name 
1290             end
1291           end
1292           names.map! do |name|
1293             tmp_board = Marshal.load(Marshal.dump(self))
1294             s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1295             if s == :illegal
1296               s # result
1297             else
1298               tmp_board.checkmated?(! sente) # result
1299             end
1300           end
1301           all_illegal = names.find {|a| a != :illegal}
1302           raise "internal error: legal move not found" if all_illegal == nil
1303           r = names.find {|a| a == false} # good move
1304           return false if r == false # found good move
1305         end
1306         y = y + 1
1307       end
1308       x = x + 1
1309     end
1310     return true
1311   end
1312
1313   # @[sente|gote]_history has at least one item while the player is checking the other or 
1314   # the other escapes.
1315   def update_sennichite(player)
1316     str = to_s
1317     @history[str] += 1
1318     if checkmated?(!player)
1319       if (player)
1320         @sente_history["dummy"] = 1  # flag to see Sente player is checking Gote player
1321       else
1322         @gote_history["dummy"]  = 1  # flag to see Gote player is checking Sente player
1323       end
1324     else
1325       if (player)
1326         @sente_history.clear # no more continuous check
1327       else
1328         @gote_history.clear  # no more continuous check
1329       end
1330     end
1331     if @sente_history.size > 0  # possible for Sente's or Gote's turn
1332       @sente_history[str] += 1
1333     end
1334     if @gote_history.size > 0   # possible for Sente's or Gote's turn
1335       @gote_history[str] += 1
1336     end
1337   end
1338
1339   def oute_sennichite?(player)
1340     if (@sente_history[to_s] >= 4)
1341       return :oute_sennichite_sente_lose
1342     elsif (@gote_history[to_s] >= 4)
1343       return :oute_sennichite_gote_lose
1344     else
1345       return nil
1346     end
1347   end
1348
1349   def sennichite?(sente)
1350     if (@history[to_s] >= 4) # already 3 times
1351       return true
1352     end
1353     return false
1354   end
1355
1356   def good_kachi?(sente)
1357     if (checkmated?(sente))
1358       puts "'NG: Checkmating." if $DEBUG
1359       return false 
1360     end
1361     
1362     ou = look_for_ou(sente)
1363     if (sente && (ou.y >= 4))
1364       puts "'NG: Black's OU does not enter yet." if $DEBUG
1365       return false     
1366     end  
1367     if (! sente && (ou.y <= 6))
1368       puts "'NG: White's OU does not enter yet." if $DEBUG
1369       return false 
1370     end
1371       
1372     number = 0
1373     point = 0
1374
1375     if (sente)
1376       hands = @sente_hands
1377       r = [1, 2, 3]
1378     else
1379       hands = @gote_hands
1380       r = [7, 8, 9]
1381     end
1382     r.each do |y|
1383       x = 1
1384       while (x <= 9)
1385         if (@array[x][y] &&
1386             (@array[x][y].sente == sente) &&
1387             (@array[x][y].point > 0))
1388           point = point + @array[x][y].point
1389           number = number + 1
1390         end
1391         x = x + 1
1392       end
1393     end
1394     hands.each do |piece|
1395       point = point + piece.point
1396     end
1397
1398     if (number < 10)
1399       puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1400       return false     
1401     end  
1402     if (sente)
1403       if (point < 28)
1404         puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1405         return false 
1406       end  
1407     else
1408       if (point < 27)
1409         puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1410         return false 
1411       end
1412     end
1413
1414     puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1415     return true
1416   end
1417
1418   # sente is nil only if tests in test_board run
1419   def handle_one_move(str, sente=nil)
1420     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1421       sg = $1
1422       x0 = $2.to_i
1423       y0 = $3.to_i
1424       x1 = $4.to_i
1425       y1 = $5.to_i
1426       name = $6
1427     elsif (str =~ /^%KACHI/)
1428       raise ArgumentError, "sente is null", caller if sente == nil
1429       if (good_kachi?(sente))
1430         return :kachi_win
1431       else
1432         return :kachi_lose
1433       end
1434     elsif (str =~ /^%TORYO/)
1435       return :toryo
1436     else
1437       return :illegal
1438     end
1439     
1440     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1441         ((x0 != 0) || (y0 != 0)))
1442       return :illegal
1443     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1444       return :illegal
1445     end
1446     
1447     if (sg == "+")
1448       sente = true if sente == nil           # deprecated
1449       return :illegal unless sente == true   # black player's move must be black
1450       hands = @sente_hands
1451     else
1452       sente = false if sente == nil          # deprecated
1453       return :illegal unless sente == false  # white player's move must be white
1454       hands = @gote_hands
1455     end
1456     
1457     ## source check
1458     if ((x0 == 0) && (y0 == 0))
1459       return :illegal if (! have_piece?(hands, name))
1460     elsif (! @array[x0][y0])
1461       return :illegal           # no piece
1462     elsif (@array[x0][y0].sente != sente)
1463       return :illegal           # this is not mine
1464     elsif (@array[x0][y0].name != name)
1465       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1466     end
1467
1468     ## destination check
1469     if (@array[x1][y1] &&
1470         (@array[x1][y1].sente == sente)) # can't capture mine
1471       return :illegal
1472     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1473       return :illegal           # can't put on existing piece
1474     end
1475
1476     tmp_board = Marshal.load(Marshal.dump(self))
1477     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1478     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1479     tmp_board.update_sennichite(sente)
1480     os_result = tmp_board.oute_sennichite?(sente)
1481     return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
1482     return :sennichite if tmp_board.sennichite?(sente)
1483
1484     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1485       return :uchifuzume
1486     end
1487
1488     move_to(x0, y0, x1, y1, name, sente)
1489     str = to_s
1490
1491     update_sennichite(sente)
1492     return :normal
1493   end
1494
1495   def to_s
1496     a = Array::new
1497     y = 1
1498     while (y <= 9)
1499       a.push(sprintf("P%d", y))
1500       x = 9
1501       while (x >= 1)
1502         piece = @array[x][y]
1503         if (piece)
1504           s = piece.to_s
1505         else
1506           s = " * "
1507         end
1508         a.push(s)
1509         x = x - 1
1510       end
1511       a.push(sprintf("\n"))
1512       y = y + 1
1513     end
1514     if (! sente_hands.empty?)
1515       a.push("P+")
1516       sente_hands.each do |p|
1517         a.push("00" + p.name)
1518       end
1519       a.push("\n")
1520     end
1521     if (! gote_hands.empty?)
1522       a.push("P-")
1523       gote_hands.each do |p|
1524         a.push("00" + p.name)
1525       end
1526       a.push("\n")
1527     end
1528     a.push("+\n")
1529     return a.join
1530   end
1531 end
1532
1533 class GameResult
1534   attr_reader :players, :black, :white
1535
1536   def initialize(p1, p2)
1537     @players = [p1, p2]
1538     if p1.sente && !p2.sente
1539       @black, @white = p1, p2
1540     elsif !p1.sente && p2.sente
1541       @black, @white = p2, p1
1542     else
1543       raise "Never reached!"
1544     end
1545   end
1546 end
1547
1548 class GameResultWin < GameResult
1549   attr_reader :winner, :loser
1550
1551   def initialize(winner, loser)
1552     super
1553     @winner, @loser = winner, loser
1554     @winner.last_game_win = true
1555     @loser.last_game_win  = false
1556   end
1557
1558   def to_s
1559     black_name = @black.id || @black.name
1560     white_name = @white.id || @white.name
1561     "%s:%s" % [black_name, white_name]
1562   end
1563 end
1564
1565 class GameResultDraw < GameResult
1566   def initialize(p1, p2)
1567     super
1568     p1.last_game_win = false
1569     p2.last_game_win = false
1570   end
1571 end
1572
1573 class Game
1574   @@mutex = Mutex.new
1575   @@time  = 0
1576
1577   def initialize(game_name, player0, player1)
1578     @monitors = Array::new
1579     @game_name = game_name
1580     if (@game_name =~ /-(\d+)-(\d+)$/)
1581       @total_time = $1.to_i
1582       @byoyomi = $2.to_i
1583     end
1584
1585     if (player0.sente)
1586       @sente, @gote = player0, player1
1587     else
1588       @sente, @gote = player1, player0
1589     end
1590     @current_player, @next_player = @sente, @gote
1591     @sente.game = self
1592     @gote.game  = self
1593
1594     @last_move = ""
1595     @current_turn = 0
1596
1597     @sente.status = "agree_waiting"
1598     @gote.status  = "agree_waiting"
1599
1600     @id = sprintf("%s+%s+%s+%s+%s", 
1601                   LEAGUE.event, @game_name, 
1602                   @sente.name, @gote.name, issue_current_time)
1603     @logfile = File.join(LEAGUE.dir, @id + ".csa")
1604
1605     LEAGUE.games[@id] = self
1606
1607     log_message(sprintf("game created %s", @id))
1608
1609     @board = Board::new
1610     @board.initial
1611     @start_time = nil
1612     @fh = nil
1613     @result = nil
1614
1615     propose
1616   end
1617   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1618   attr_accessor :last_move, :current_turn
1619   attr_reader   :result
1620
1621   def rated?
1622     @sente.rated? && @gote.rated?
1623   end
1624
1625   def monitoron(monitor)
1626     @monitors.delete(monitor)
1627     @monitors.push(monitor)
1628   end
1629
1630   def monitoroff(monitor)
1631     @monitors.delete(monitor)
1632   end
1633
1634   def reject(rejector)
1635     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1636     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1637     finish
1638   end
1639
1640   def kill(killer)
1641     if ["agree_waiting", "start_waiting"].include?(@sente.status)
1642       reject(killer.name)
1643     elsif (@current_player == killer)
1644       abnormal_lose()
1645       finish
1646     end
1647   end
1648
1649   def finish
1650     log_message(sprintf("game finished %s", @id))
1651     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1652     @fh.close
1653
1654     @sente.game = nil
1655     @gote.game = nil
1656     @sente.status = "connected"
1657     @gote.status = "connected"
1658
1659     if (@current_player.protocol == LoginCSA::PROTOCOL)
1660       @current_player.finish
1661     end
1662     if (@next_player.protocol == LoginCSA::PROTOCOL)
1663       @next_player.finish
1664     end
1665     @monitors = Array::new
1666     @sente = nil
1667     @gote = nil
1668     @current_player = nil
1669     @next_player = nil
1670     LEAGUE.games.delete(@id)
1671   end
1672
1673   def handle_one_move(str, player)
1674     return nil unless @current_player == player
1675
1676     finish_flag = true
1677     @end_time = Time::new
1678     t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max
1679     
1680     move_status = nil
1681     if ((@current_player.mytime - t <= -@byoyomi) && 
1682         ((@total_time > 0) || (@byoyomi > 0)))
1683       status = :timeout
1684     elsif (str == :timeout)
1685       return false            # time isn't expired. players aren't swapped. continue game
1686     else
1687       @current_player.mytime -= t
1688       if (@current_player.mytime < 0)
1689         @current_player.mytime = 0
1690       end
1691
1692       move_status = @board.handle_one_move(str, @sente == @current_player)
1693
1694       if [:illegal, :uchifuzme, :oute_kaihimore].include?(move_status)
1695         @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1696       else
1697         if [:normal, :outori, :sennichite, :oute_sennichite_sente_lose, :oute_sennichite_gote_lose].include?(move_status)
1698           @sente.write_safe(sprintf("%s,T%d\n", str, t))
1699           @gote.write_safe(sprintf("%s,T%d\n", str, t))
1700           @fh.printf("%s\nT%d\n", str, t)
1701           @last_move = sprintf("%s,T%d", str, t)
1702           @current_turn += 1
1703         end
1704
1705         @monitors.each do |monitor|
1706           monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1707           monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1708         end
1709       end
1710     end
1711
1712     if (@next_player.status != "game") # rival is logout or disconnected
1713       abnormal_win()
1714     elsif (status == :timeout)
1715       timeout_lose()
1716     elsif (move_status == :illegal)
1717       illegal_lose()
1718     elsif (move_status == :kachi_win)
1719       kachi_win()
1720     elsif (move_status == :kachi_lose)
1721       kachi_lose()
1722     elsif (move_status == :toryo)
1723       toryo_lose()
1724     elsif (move_status == :outori)
1725       outori_win()
1726     elsif (move_status == :oute_sennichite_sente_lose)
1727       oute_sennichite_win_lose(@gote, @sente) # Sente is checking
1728     elsif (move_status == :oute_sennichite_gote_lose)
1729       oute_sennichite_win_lose(@sente, @gote) # Gote is checking
1730     elsif (move_status == :sennichite)
1731       sennichite_draw()
1732     elsif (move_status == :uchifuzume)
1733       uchifuzume_lose()
1734     elsif (move_status == :oute_kaihimore)
1735       oute_kaihimore_lose()
1736     else
1737       finish_flag = false
1738     end
1739     finish() if finish_flag
1740     @current_player, @next_player = @next_player, @current_player
1741     @start_time = Time::new
1742     return finish_flag
1743   end
1744
1745   def abnormal_win
1746     @current_player.status = "connected"
1747     @next_player.status = "connected"
1748     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1749     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1750     @fh.printf("%%TORYO\n")
1751     @fh.print(@board.to_s.gsub(/^/, "\'"))
1752     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1753     @result = GameResultWin.new(@current_player, @next_player)
1754     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1755     @monitors.each do |monitor|
1756       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1757     end
1758   end
1759
1760   def abnormal_lose
1761     @current_player.status = "connected"
1762     @next_player.status = "connected"
1763     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1764     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1765     @fh.printf("%%TORYO\n")
1766     @fh.print(@board.to_s.gsub(/^/, "\'"))
1767     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1768     @result = GameResultWin.new(@next_player, @current_player)
1769     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1770     @monitors.each do |monitor|
1771       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1772     end
1773   end
1774
1775   def sennichite_draw
1776     @current_player.status = "connected"
1777     @next_player.status = "connected"
1778     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1779     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1780     @fh.print(@board.to_s.gsub(/^/, "\'"))
1781     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1782     @result = GameResultDraw.new(@current_player, @next_player)
1783     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1784     @monitors.each do |monitor|
1785       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1786     end
1787   end
1788
1789   def oute_sennichite_win_lose(winner, loser)
1790     @current_player.status = "connected"
1791     @next_player.status = "connected"
1792     loser.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1793     winner.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1794     @fh.print(@board.to_s.gsub(/^/, "\'"))
1795     if loser == @current_player
1796       @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1797     else
1798       @fh.printf("'summary:oute_sennichite:%s win:%s lose\n", @current_player.name, @next_player.name)
1799     end
1800     @result = GameResultWin.new(winner, loser)
1801     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1802     @monitors.each do |monitor|
1803       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1804     end
1805   end
1806
1807   def illegal_lose
1808     @current_player.status = "connected"
1809     @next_player.status = "connected"
1810     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1811     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1812     @fh.print(@board.to_s.gsub(/^/, "\'"))
1813     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1814     @result = GameResultWin.new(@next_player, @current_player)
1815     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1816     @monitors.each do |monitor|
1817       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1818     end
1819   end
1820
1821   def uchifuzume_lose
1822     @current_player.status = "connected"
1823     @next_player.status = "connected"
1824     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1825     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1826     @fh.print(@board.to_s.gsub(/^/, "\'"))
1827     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1828     @result = GameResultWin.new(@next_player, @current_player)
1829     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1830     @monitors.each do |monitor|
1831       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1832     end
1833   end
1834
1835   def oute_kaihimore_lose
1836     @current_player.status = "connected"
1837     @next_player.status = "connected"
1838     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1839     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1840     @fh.print(@board.to_s.gsub(/^/, "\'"))
1841     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1842     @result = GameResultWin.new(@next_player, @current_player)
1843     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1844     @monitors.each do |monitor|
1845       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1846     end
1847   end
1848
1849   def timeout_lose
1850     @current_player.status = "connected"
1851     @next_player.status = "connected"
1852     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1853     @next_player.write_safe("#TIME_UP\n#WIN\n")
1854     @fh.print(@board.to_s.gsub(/^/, "\'"))
1855     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1856     @result = GameResultWin.new(@next_player, @current_player)
1857     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1858     @monitors.each do |monitor|
1859       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1860     end
1861   end
1862
1863   def kachi_win
1864     @current_player.status = "connected"
1865     @next_player.status = "connected"
1866     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1867     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1868     @fh.printf("%%KACHI\n")
1869     @fh.print(@board.to_s.gsub(/^/, "\'"))
1870     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1871     @result = GameResultWin.new(@current_player, @next_player)
1872     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1873     @monitors.each do |monitor|
1874       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1875     end
1876   end
1877
1878   def kachi_lose
1879     @current_player.status = "connected"
1880     @next_player.status = "connected"
1881     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1882     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1883     @fh.printf("%%KACHI\n")
1884     @fh.print(@board.to_s.gsub(/^/, "\'"))
1885     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1886     @result = GameResultWin.new(@next_player, @current_player)
1887     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1888     @monitors.each do |monitor|
1889       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1890     end
1891   end
1892
1893   def toryo_lose
1894     @current_player.status = "connected"
1895     @next_player.status = "connected"
1896     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1897     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1898     @fh.printf("%%TORYO\n")
1899     @fh.print(@board.to_s.gsub(/^/, "\'"))
1900     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1901     @result = GameResultWin.new(@next_player, @current_player)
1902     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1903     @monitors.each do |monitor|
1904       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1905     end
1906   end
1907
1908   def outori_win
1909     @current_player.status = "connected"
1910     @next_player.status = "connected"
1911     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1912     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1913     @fh.print(@board.to_s.gsub(/^/, "\'"))
1914     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1915     @result = GameResultWin.new(@current_player, @next_player)
1916     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1917     @monitors.each do |monitor|
1918       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1919     end
1920   end
1921
1922   def start
1923     log_message(sprintf("game started %s", @id))
1924     @sente.write_safe(sprintf("START:%s\n", @id))
1925     @gote.write_safe(sprintf("START:%s\n", @id))
1926     @sente.mytime = @total_time
1927     @gote.mytime = @total_time
1928     @start_time = Time::new
1929   end
1930
1931   def propose
1932     @fh = open(@logfile, "w")
1933     @fh.sync = true
1934
1935     @fh.puts("V2")
1936     @fh.puts("N+#{@sente.name}")
1937     @fh.puts("N-#{@gote.name}")
1938     @fh.puts("$EVENT:#{@id}")
1939
1940     @sente.write_safe(propose_message("+"))
1941     @gote.write_safe(propose_message("-"))
1942
1943     now = Time::new.strftime("%Y/%m/%d %H:%M:%S")
1944     @fh.puts("$START_TIME:#{now}")
1945     @fh.print <<EOM
1946 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1947 P2 * -HI *  *  *  *  * -KA * 
1948 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1949 P4 *  *  *  *  *  *  *  *  * 
1950 P5 *  *  *  *  *  *  *  *  * 
1951 P6 *  *  *  *  *  *  *  *  * 
1952 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1953 P8 * +KA *  *  *  *  * +HI * 
1954 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1955 +
1956 EOM
1957   end
1958
1959   def show()
1960     str0 = <<EOM
1961 BEGIN Game_Summary
1962 Protocol_Version:1.1
1963 Protocol_Mode:Server
1964 Format:Shogi 1.0
1965 Declaration:Jishogi 1.1
1966 Game_ID:#{@id}
1967 Name+:#{@sente.name}
1968 Name-:#{@gote.name}
1969 Rematch_On_Draw:NO
1970 To_Move:+
1971 BEGIN Time
1972 Time_Unit:1sec
1973 Total_Time:#{@total_time}
1974 Byoyomi:#{@byoyomi}
1975 Least_Time_Per_Move:#{Least_Time_Per_Move}
1976 Remaining_Time+:#{@sente.mytime}
1977 Remaining_Time-:#{@gote.mytime}
1978 Last_Move:#{@last_move}
1979 Current_Turn:#{@current_turn}
1980 END Time
1981 BEGIN Position
1982 EOM
1983
1984     str1 = <<EOM
1985 END Position
1986 END Game_Summary
1987 EOM
1988
1989     return str0 + @board.to_s + str1
1990   end
1991
1992   def propose_message(sg_flag)
1993     str = <<EOM
1994 BEGIN Game_Summary
1995 Protocol_Version:1.1
1996 Protocol_Mode:Server
1997 Format:Shogi 1.0
1998 Declaration:Jishogi 1.1
1999 Game_ID:#{@id}
2000 Name+:#{@sente.name}
2001 Name-:#{@gote.name}
2002 Your_Turn:#{sg_flag}
2003 Rematch_On_Draw:NO
2004 To_Move:+
2005 BEGIN Time
2006 Time_Unit:1sec
2007 Total_Time:#{@total_time}
2008 Byoyomi:#{@byoyomi}
2009 Least_Time_Per_Move:#{Least_Time_Per_Move}
2010 END Time
2011 BEGIN Position
2012 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
2013 P2 * -HI *  *  *  *  * -KA * 
2014 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
2015 P4 *  *  *  *  *  *  *  *  * 
2016 P5 *  *  *  *  *  *  *  *  * 
2017 P6 *  *  *  *  *  *  *  *  * 
2018 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
2019 P8 * +KA *  *  *  *  * +HI * 
2020 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
2021 P+
2022 P-
2023 +
2024 END Position
2025 END Game_Summary
2026 EOM
2027     return str
2028   end
2029   
2030   private
2031   
2032   def issue_current_time
2033     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
2034     @@mutex.synchronize do
2035       while time <= @@time do
2036         time += 1
2037       end
2038       @@time = time
2039     end
2040   end
2041 end
2042 end # module ShogiServer
2043
2044 #################################################
2045 # MAIN
2046 #
2047
2048 def usage
2049     print <<EOM
2050 NAME
2051         shogi-server - server for CSA server protocol
2052
2053 SYNOPSIS
2054         shogi-server [OPTIONS] event_name port_number
2055
2056 DESCRIPTION
2057         server for CSA server protocol
2058
2059 OPTIONS
2060         --pid-file file
2061                 specify filename for logging process ID
2062         --daemon dir
2063                 run as a daemon. Log files will be put in dir.
2064
2065 LICENSE
2066         this file is distributed under GPL version2 and might be compiled by Exerb
2067
2068 SEE ALSO
2069
2070 RELEASE
2071         #{ShogiServer::Release}
2072
2073 REVISION
2074         #{ShogiServer::Revision}
2075 EOM
2076 end
2077
2078 def log_debug(str)
2079   $logger.debug(str)
2080 end
2081
2082 def log_message(str)
2083   $logger.info(str)
2084 end
2085
2086 def log_warning(str)
2087   $logger.warn(str)
2088 end
2089
2090 def log_error(str)
2091   $logger.error(str)
2092 end
2093
2094
2095 def parse_command_line
2096   options = Hash::new
2097   parser = GetoptLong.new(
2098     ["--daemon",   GetoptLong::REQUIRED_ARGUMENT],
2099     ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
2100   parser.quiet = true
2101   begin
2102     parser.each_option do |name, arg|
2103       name.sub!(/^--/, '')
2104       options[name] = arg.dup
2105     end
2106   rescue
2107     usage
2108     raise parser.error_message
2109   end
2110   return options
2111 end
2112
2113 def write_pid_file(file)
2114   open(file, "w") do |fh|
2115     fh.puts "#{$$}"
2116   end
2117 end
2118
2119 def mutex_watchdog(mutex, sec)
2120   while true
2121     begin
2122       timeout(sec) do
2123         begin
2124           mutex.lock
2125         ensure
2126           mutex.unlock
2127         end
2128       end
2129       sleep(sec)
2130     rescue TimeoutError
2131       log_error("mutex watchdog timeout")
2132       exit(1)
2133     end
2134   end
2135 end
2136
2137 def login_loop(client)
2138   player = login = nil
2139  
2140   while r = select([client], nil, nil, ShogiServer::Login_Time) do
2141     break unless str = r[0].first.gets
2142     $mutex.lock # guards LEAGUE
2143     begin
2144       str =~ /([\r\n]*)$/
2145       eol = $1
2146       if (ShogiServer::Login::good_login?(str))
2147         player = ShogiServer::Player::new(str, client, eol)
2148         login  = ShogiServer::Login::factory(str, player)
2149         if (current_player = LEAGUE.find(player.name))
2150           if (current_player.password == player.password &&
2151               current_player.status != "game")
2152             log_message(sprintf("user %s login forcely", player.name))
2153             current_player.kill
2154           else
2155             login.incorrect_duplicated_player(str)
2156             player = nil
2157             break
2158           end
2159         end
2160         LEAGUE.add(player)
2161         break
2162       else
2163         client.write_safe("LOGIN:incorrect" + eol)
2164         client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2165       end
2166     ensure
2167       $mutex.unlock
2168     end
2169   end                       # login loop
2170   return [player, login]
2171 end
2172
2173 def main
2174
2175   $mutex = Mutex::new
2176   Thread::start do
2177     Thread.pass
2178     mutex_watchdog($mutex, 10)
2179   end
2180
2181   $options = parse_command_line
2182   if (ARGV.length != 2)
2183     usage
2184     exit 2
2185   end
2186
2187   LEAGUE.event = ARGV.shift
2188   port = ARGV.shift
2189
2190   dir = $options["daemon"]
2191   dir = File.expand_path(dir) if dir
2192   if dir && ! File.exist?(dir)
2193     FileUtils.mkdir(dir)
2194   end
2195   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
2196   $logger = WEBrick::Log.new(log_file) # thread safe
2197
2198   LEAGUE.dir = dir || File.dirname(__FILE__)
2199   LEAGUE.setup_players_database
2200
2201   config = {}
2202   config[:Port]       = port
2203   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
2204   config[:Logger]     = $logger
2205   if $options["pid-file"]
2206     pid_file = File.expand_path($options["pid-file"])
2207     config[:StartCallback] = Proc.new do
2208       write_pid_file(pid_file)
2209     end
2210     config[:StopCallback] = Proc.new do
2211       FileUtils.rm(pid_file, :force => true)
2212     end
2213   end
2214
2215   server = WEBrick::GenericServer.new(config)
2216   ["INT", "TERM"].each do |signal| 
2217     trap(signal) do
2218       LEAGUE.shutdown
2219       server.shutdown
2220     end
2221   end
2222   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
2223   log_message("server started [Revision: #{ShogiServer::Revision}]")
2224
2225   server.start do |client|
2226       # client.sync = true # this is already set in WEBrick 
2227       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
2228         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
2229       player, login = login_loop(client) # loop
2230       next unless player
2231
2232       log_message(sprintf("user %s login", player.name))
2233       login.process
2234       player.run(login.csa_1st_str) # loop
2235       begin
2236         $mutex.lock
2237         if (player.game)
2238           player.game.kill(player)
2239         end
2240         player.finish # socket has been closed
2241         LEAGUE.delete(player)
2242         log_message(sprintf("user %s logout", player.name))
2243       ensure
2244         $mutex.unlock
2245       end
2246   end
2247 end
2248
2249
2250 if ($0 == __FILE__)
2251   STDOUT.sync = true
2252   STDERR.sync = true
2253   TCPSocket.do_not_reverse_lookup = true
2254   Thread.abort_on_exception = $DEBUG ? true : false
2255
2256   LEAGUE = ShogiServer::League::new
2257   main
2258 end