OSDN Git Service

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