OSDN Git Service

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