OSDN Git Service

14b27677dd5fa508dd57ca5cccc2a78d95b698c4
[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)
115     found = @players.find do |name, player|
116       (player.status == status) &&
117       (player.game_name == game_name) &&
118       ( (sente == nil) || 
119         (player.sente == nil) || 
120         (player.sente == sente) ) &&
121       (player != searcher)
122     end
123     return found ? found.last : 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
558           if (my_sente_str == "*")
559             rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
560           elsif (my_sente_str == "+")
561             rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
562           elsif (my_sente_str == "-")
563             rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
564           else
565             ## never reached
566             write_safe(sprintf("##[ERROR] bad game option\n"))
567             next
568           end
569
570           if (rival)
571             @game_name = game_name
572             if ((my_sente_str == "*") && (rival.sente == nil))
573               if (rand(2) == 0)
574                 @sente = true
575                 rival.sente = false
576               else
577                 @sente = false
578                 rival.sente = true
579               end
580             elsif (rival.sente == true) # rival has higher priority
581               @sente = false
582             elsif (rival.sente == false)
583               @sente = true
584             elsif (my_sente_str == "+")
585               @sente = true
586               rival.sente = false
587             elsif (my_sente_str == "-")
588               @sente = false
589               rival.sente = true
590             else
591               ## never reached
592             end
593             Game::new(@game_name, self, rival)
594             self.status = "agree_waiting"
595             rival.status = "agree_waiting"
596           else # rival not found
597             if (command_name == "GAME")
598               @status = "game_waiting"
599               @game_name = game_name
600               if (my_sente_str == "+")
601                 @sente = true
602               elsif (my_sente_str == "-")
603                 @sente = false
604               else
605                 @sente = nil
606               end
607             else                # challenge
608               write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
609               @status = "connected"
610               @game_name = ""
611               @sente = nil
612             end
613           end
614         when /^%%CHAT\s+(.+)/
615           message = $1
616           LEAGUE.players.each do |name, player|
617             if (player.protocol != LoginCSA::PROTOCOL)
618               player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
619             end
620           end
621         when /^%%LIST/
622           buf = Array::new
623           LEAGUE.games.each do |id, game|
624             buf.push(sprintf("##[LIST] %s\n", id))
625           end
626           buf.push("##[LIST] +OK\n")
627           write_safe(buf.join)
628         when /^%%WHO/
629           buf = Array::new
630           LEAGUE.players.each do |name, player|
631             buf.push(sprintf("##[WHO] %s\n", player.to_s))
632           end
633           buf.push("##[WHO] +OK\n")
634           write_safe(buf.join)
635         when /^LOGOUT/
636           @status = "connected"
637           write_safe("LOGOUT:completed\n")
638           return
639         when /^CHALLENGE/
640           # This command is only available for CSA's official testing server.
641           # So, this means nothing for this program.
642           write_safe("CHALLENGE ACCEPTED\n")
643         when /^\s*$/
644           ## ignore null string
645         else
646           msg = "##[ERROR] unknown command %s\n" % [str]
647           write_safe(msg)
648           log_error(msg)
649         end
650       ensure
651         $mutex.unlock
652       end
653     end # enf of while
654   end # def run
655 end # class
656
657 class Piece
658   PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
659   def initialize(board, x, y, sente, promoted=false)
660     @board = board
661     @x = x
662     @y = y
663     @sente = sente
664     @promoted = promoted
665
666     if ((x == 0) || (y == 0))
667       if (sente)
668         hands = board.sente_hands
669       else
670         hands = board.gote_hands
671       end
672       hands.push(self)
673       hands.sort! {|a, b|
674         a.name <=> b.name
675       }
676     else
677       @board.array[x][y] = self
678     end
679   end
680   attr_accessor :promoted, :sente, :x, :y, :board
681
682   def room_of_head?(x, y, name)
683     true
684   end
685
686   def movable_grids
687     return adjacent_movable_grids + far_movable_grids
688   end
689
690   def far_movable_grids
691     return []
692   end
693
694   def jump_to?(x, y)
695     if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
696       if ((@board.array[x][y] == nil) || # dst is empty
697           (@board.array[x][y].sente != @sente)) # dst is enemy
698         return true
699       end
700     end
701     return false
702   end
703
704   def put_to?(x, y)
705     if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
706       if (@board.array[x][y] == nil) # dst is empty?
707         return true
708       end
709     end
710     return false
711   end
712
713   def adjacent_movable_grids
714     grids = Array::new
715     if (@promoted)
716       moves = @promoted_moves
717     else
718       moves = @normal_moves
719     end
720     moves.each do |(dx, dy)|
721       if (@sente)
722         cand_y = @y - dy
723       else
724         cand_y = @y + dy
725       end
726       cand_x = @x + dx
727       if (jump_to?(cand_x, cand_y))
728         grids.push([cand_x, cand_y])
729       end
730     end
731     return grids
732   end
733
734   def move_to?(x, y, name)
735     return false if (! room_of_head?(x, y, name))
736     return false if ((name != @name) && (name != @promoted_name))
737     return false if (@promoted && (name != @promoted_name)) # can't un-promote
738
739     if (! @promoted)
740       return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
741       if (@sente)
742         return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
743       else
744         return false if ((6 >= @y) && (6 >= y) && (name != @name))
745       end
746     end
747
748     if ((@x == 0) || (@y == 0))
749       return jump_to?(x, y)
750     else
751       return movable_grids.include?([x, y])
752     end
753   end
754
755   def move_to(x, y)
756     if ((@x == 0) || (@y == 0))
757       if (@sente)
758         @board.sente_hands.delete(self)
759       else
760         @board.gote_hands.delete(self)
761       end
762       @board.array[x][y] = self
763     elsif ((x == 0) || (y == 0))
764       @promoted = false         # clear promoted flag before moving to hands
765       if (@sente)
766         @board.sente_hands.push(self)
767       else
768         @board.gote_hands.push(self)
769       end
770       @board.array[@x][@y] = nil
771     else
772       @board.array[@x][@y] = nil
773       @board.array[x][y] = self
774     end
775     @x = x
776     @y = y
777   end
778
779   def point
780     @point
781   end
782
783   def name
784     @name
785   end
786
787   def promoted_name
788     @promoted_name
789   end
790
791   def to_s
792     if (@sente)
793       sg = "+"
794     else
795       sg = "-"
796     end
797     if (@promoted)
798       n = @promoted_name
799     else
800       n = @name
801     end
802     return sg + n
803   end
804 end
805
806 class PieceFU < Piece
807   def initialize(*arg)
808     @point = 1
809     @normal_moves = [[0, +1]]
810     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
811     @name = "FU"
812     @promoted_name = "TO"
813     super
814   end
815   def room_of_head?(x, y, name)
816     if (name == "FU")
817       if (@sente)
818         return false if (y == 1)
819       else
820         return false if (y == 9)
821       end
822       ## 2fu check
823       c = 0
824       iy = 1
825       while (iy <= 9)
826         if ((iy  != @y) &&      # not source position
827             @board.array[x][iy] &&
828             (@board.array[x][iy].sente == @sente) && # mine
829             (@board.array[x][iy].name == "FU") &&
830             (@board.array[x][iy].promoted == false))
831           return false
832         end
833         iy = iy + 1
834       end
835     end
836     return true
837   end
838 end
839
840 class PieceKY  < Piece
841   def initialize(*arg)
842     @point = 1
843     @normal_moves = []
844     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
845     @name = "KY"
846     @promoted_name = "NY"
847     super
848   end
849   def room_of_head?(x, y, name)
850     if (name == "KY")
851       if (@sente)
852         return false if (y == 1)
853       else
854         return false if (y == 9)
855       end
856     end
857     return true
858   end
859   def far_movable_grids
860     grids = Array::new
861     if (@promoted)
862       return []
863     else
864       if (@sente)                 # up
865         cand_x = @x
866         cand_y = @y - 1
867         while (jump_to?(cand_x, cand_y))
868           grids.push([cand_x, cand_y])
869           break if (! put_to?(cand_x, cand_y))
870           cand_y = cand_y - 1
871         end
872       else                        # down
873         cand_x = @x
874         cand_y = @y + 1
875         while (jump_to?(cand_x, cand_y))
876           grids.push([cand_x, cand_y])
877           break if (! put_to?(cand_x, cand_y))
878           cand_y = cand_y + 1
879         end
880       end
881       return grids
882     end
883   end
884 end
885 class PieceKE  < Piece
886   def initialize(*arg)
887     @point = 1
888     @normal_moves = [[+1, +2], [-1, +2]]
889     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
890     @name = "KE"
891     @promoted_name = "NK"
892     super
893   end
894   def room_of_head?(x, y, name)
895     if (name == "KE")
896       if (@sente)
897         return false if ((y == 1) || (y == 2))
898       else
899         return false if ((y == 9) || (y == 8))
900       end
901     end
902     return true
903   end
904 end
905 class PieceGI  < Piece
906   def initialize(*arg)
907     @point = 1
908     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
909     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
910     @name = "GI"
911     @promoted_name = "NG"
912     super
913   end
914 end
915 class PieceKI  < Piece
916   def initialize(*arg)
917     @point = 1
918     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
919     @promoted_moves = []
920     @name = "KI"
921     @promoted_name = nil
922     super
923   end
924 end
925 class PieceKA  < Piece
926   def initialize(*arg)
927     @point = 5
928     @normal_moves = []
929     @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
930     @name = "KA"
931     @promoted_name = "UM"
932     super
933   end
934   def far_movable_grids
935     grids = Array::new
936     ## up right
937     cand_x = @x - 1
938     cand_y = @y - 1
939     while (jump_to?(cand_x, cand_y))
940       grids.push([cand_x, cand_y])
941       break if (! put_to?(cand_x, cand_y))
942       cand_x = cand_x - 1
943       cand_y = cand_y - 1
944     end
945     ## down right
946     cand_x = @x - 1
947     cand_y = @y + 1
948     while (jump_to?(cand_x, cand_y))
949       grids.push([cand_x, cand_y])
950       break if (! put_to?(cand_x, cand_y))
951       cand_x = cand_x - 1
952       cand_y = cand_y + 1
953     end
954     ## up left
955     cand_x = @x + 1
956     cand_y = @y - 1
957     while (jump_to?(cand_x, cand_y))
958       grids.push([cand_x, cand_y])
959       break if (! put_to?(cand_x, cand_y))
960       cand_x = cand_x + 1
961       cand_y = cand_y - 1
962     end
963     ## down left
964     cand_x = @x + 1
965     cand_y = @y + 1
966     while (jump_to?(cand_x, cand_y))
967       grids.push([cand_x, cand_y])
968       break if (! put_to?(cand_x, cand_y))
969       cand_x = cand_x + 1
970       cand_y = cand_y + 1
971     end
972     return grids
973   end
974 end
975 class PieceHI  < Piece
976   def initialize(*arg)
977     @point = 5
978     @normal_moves = []
979     @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
980     @name = "HI"
981     @promoted_name = "RY"
982     super
983   end
984   def far_movable_grids
985     grids = Array::new
986     ## up
987     cand_x = @x
988     cand_y = @y - 1
989     while (jump_to?(cand_x, cand_y))
990       grids.push([cand_x, cand_y])
991       break if (! put_to?(cand_x, cand_y))
992       cand_y = cand_y - 1
993     end
994     ## down
995     cand_x = @x
996     cand_y = @y + 1
997     while (jump_to?(cand_x, cand_y))
998       grids.push([cand_x, cand_y])
999       break if (! put_to?(cand_x, cand_y))
1000       cand_y = cand_y + 1
1001     end
1002     ## right
1003     cand_x = @x - 1
1004     cand_y = @y
1005     while (jump_to?(cand_x, cand_y))
1006       grids.push([cand_x, cand_y])
1007       break if (! put_to?(cand_x, cand_y))
1008       cand_x = cand_x - 1
1009     end
1010     ## down
1011     cand_x = @x + 1
1012     cand_y = @y
1013     while (jump_to?(cand_x, cand_y))
1014       grids.push([cand_x, cand_y])
1015       break if (! put_to?(cand_x, cand_y))
1016       cand_x = cand_x + 1
1017     end
1018     return grids
1019   end
1020 end
1021 class PieceOU < Piece
1022   def initialize(*arg)
1023     @point = 0
1024     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
1025     @promoted_moves = []
1026     @name = "OU"
1027     @promoted_name = nil
1028     super
1029   end
1030 end
1031
1032 class Board
1033   def initialize
1034     @sente_hands = Array::new
1035     @gote_hands  = Array::new
1036     @history       = Hash::new(0)
1037     @sente_history = Hash::new(0)
1038     @gote_history  = Hash::new(0)
1039     @array = [[], [], [], [], [], [], [], [], [], []]
1040     @move_count = 0
1041   end
1042   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
1043   attr_reader :move_count
1044
1045   def initial
1046     PieceKY::new(self, 1, 1, false)
1047     PieceKE::new(self, 2, 1, false)
1048     PieceGI::new(self, 3, 1, false)
1049     PieceKI::new(self, 4, 1, false)
1050     PieceOU::new(self, 5, 1, false)
1051     PieceKI::new(self, 6, 1, false)
1052     PieceGI::new(self, 7, 1, false)
1053     PieceKE::new(self, 8, 1, false)
1054     PieceKY::new(self, 9, 1, false)
1055     PieceKA::new(self, 2, 2, false)
1056     PieceHI::new(self, 8, 2, false)
1057     (1..9).each do |i|
1058       PieceFU::new(self, i, 3, false)
1059     end
1060
1061     PieceKY::new(self, 1, 9, true)
1062     PieceKE::new(self, 2, 9, true)
1063     PieceGI::new(self, 3, 9, true)
1064     PieceKI::new(self, 4, 9, true)
1065     PieceOU::new(self, 5, 9, true)
1066     PieceKI::new(self, 6, 9, true)
1067     PieceGI::new(self, 7, 9, true)
1068     PieceKE::new(self, 8, 9, true)
1069     PieceKY::new(self, 9, 9, true)
1070     PieceKA::new(self, 8, 8, true)
1071     PieceHI::new(self, 2, 8, true)
1072     (1..9).each do |i|
1073       PieceFU::new(self, i, 7, true)
1074     end
1075   end
1076
1077   def have_piece?(hands, name)
1078     piece = hands.find { |i|
1079       i.name == name
1080     }
1081     return piece
1082   end
1083
1084   def move_to(x0, y0, x1, y1, name, sente)
1085     if (sente)
1086       hands = @sente_hands
1087     else
1088       hands = @gote_hands
1089     end
1090
1091     if ((x0 == 0) || (y0 == 0))
1092       piece = have_piece?(hands, name)
1093       return :illegal if (! piece.move_to?(x1, y1, name)) # TODO null check for the piece?
1094       piece.move_to(x1, y1)
1095     else
1096       return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))  # TODO null check?
1097       if (@array[x0][y0].name != name) # promoted ?
1098         @array[x0][y0].promoted = true
1099       end
1100       if (@array[x1][y1]) # capture
1101         if (@array[x1][y1].name == "OU")
1102           return :outori        # return board update
1103         end
1104         @array[x1][y1].sente = @array[x0][y0].sente
1105         @array[x1][y1].move_to(0, 0)
1106         hands.sort! {|a, b| # TODO refactor. Move to Piece class
1107           a.name <=> b.name
1108         }
1109       end
1110       @array[x0][y0].move_to(x1, y1)
1111     end
1112     @move_count += 1
1113     return true
1114   end
1115
1116   def look_for_ou(sente)
1117     x = 1
1118     while (x <= 9)
1119       y = 1
1120       while (y <= 9)
1121         if (@array[x][y] &&
1122             (@array[x][y].name == "OU") &&
1123             (@array[x][y].sente == sente))
1124           return @array[x][y]
1125         end
1126         y = y + 1
1127       end
1128       x = x + 1
1129     end
1130     raise "can't find ou"
1131   end
1132
1133   # note checkmate, but check. sente is checked.
1134   def checkmated?(sente)        # sente is loosing
1135     ou = look_for_ou(sente)
1136     x = 1
1137     while (x <= 9)
1138       y = 1
1139       while (y <= 9)
1140         if (@array[x][y] &&
1141             (@array[x][y].sente != sente))
1142           if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1143             return true
1144           end
1145         end
1146         y = y + 1
1147       end
1148       x = x + 1
1149     end
1150     return false
1151   end
1152
1153   def uchifuzume?(sente)
1154     rival_ou = look_for_ou(! sente)   # rival's ou
1155     if (sente)                  # rival is gote
1156       if ((rival_ou.y != 9) &&
1157           (@array[rival_ou.x][rival_ou.y + 1]) &&
1158           (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1159           (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1160         fu_x = rival_ou.x
1161         fu_y = rival_ou.y + 1
1162       else
1163         return false
1164       end
1165     else                        # gote
1166       if ((rival_ou.y != 0) &&
1167           (@array[rival_ou.x][rival_ou.y - 1]) &&
1168           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1169           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1170         fu_x = rival_ou.x
1171         fu_y = rival_ou.y - 1
1172       else
1173         return false
1174       end
1175     end
1176     
1177     ## case: rival_ou is moving
1178     escaped = false
1179     rival_ou.movable_grids.each do |(cand_x, cand_y)|
1180       tmp_board = Marshal.load(Marshal.dump(self))
1181       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1182       raise "internal error" if (s != true)
1183       if (! tmp_board.checkmated?(! sente)) # good move
1184         return false
1185       end
1186     end
1187
1188     ## case: rival is capturing fu
1189     x = 1
1190     while (x <= 9)
1191       y = 1
1192       while (y <= 9)
1193         if (@array[x][y] &&
1194             (@array[x][y].sente != sente) &&
1195             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1196           if (@array[x][y].promoted)
1197             name = @array[x][y].promoted_name
1198           else
1199             name = @array[x][y].name
1200           end
1201           tmp_board = Marshal.load(Marshal.dump(self))
1202           s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1203           raise "internal error" if (s != true)
1204           if (! tmp_board.checkmated?(! sente)) # good move
1205             return false
1206           end
1207         end
1208         y = y + 1
1209       end
1210       x = x + 1
1211     end
1212     return true
1213   end
1214
1215   # @[sente|gote]_history has at least one item while the player is checking the other or 
1216   # the other escapes.
1217   def update_sennichite(player)
1218     str = to_s
1219     @history[str] += 1
1220     if checkmated?(!player)
1221       if (player)
1222         @sente_history["dummy"] = 1  # flag to see Sente player is checking Gote player
1223       else
1224         @gote_history["dummy"]  = 1  # flag to see Gote player is checking Sente player
1225       end
1226     else
1227       if (player)
1228         @sente_history.clear # no more continuous check
1229       else
1230         @gote_history.clear  # no more continuous check
1231       end
1232     end
1233     if @sente_history.size > 0  # possible for Sente's or Gote's turn
1234       @sente_history[str] += 1
1235     end
1236     if @gote_history.size > 0   # possible for Sente's or Gote's turn
1237       @gote_history[str] += 1
1238     end
1239   end
1240
1241   def oute_sennichite?(player)
1242     if (@sente_history[to_s] >= 4)
1243       return :oute_sennichite_sente_lose
1244     elsif (@gote_history[to_s] >= 4)
1245       return :oute_sennichite_gote_lose
1246     else
1247       return nil
1248     end
1249   end
1250
1251   def sennichite?(sente)
1252     if (@history[to_s] >= 4) # already 3 times
1253       return true
1254     end
1255     return false
1256   end
1257
1258   def good_kachi?(sente)
1259     if (checkmated?(sente))
1260       puts "'NG: Checkmating." if $DEBUG
1261       return false 
1262     end
1263     
1264     ou = look_for_ou(sente)
1265     if (sente && (ou.y >= 4))
1266       puts "'NG: Black's OU does not enter yet." if $DEBUG
1267       return false     
1268     end  
1269     if (! sente && (ou.y <= 6))
1270       puts "'NG: White's OU does not enter yet." if $DEBUG
1271       return false 
1272     end
1273       
1274     number = 0
1275     point = 0
1276
1277     if (sente)
1278       hands = @sente_hands
1279       r = [1, 2, 3]
1280     else
1281       hands = @gote_hands
1282       r = [7, 8, 9]
1283     end
1284     r.each do |y|
1285       x = 1
1286       while (x <= 9)
1287         if (@array[x][y] &&
1288             (@array[x][y].sente == sente) &&
1289             (@array[x][y].point > 0))
1290           point = point + @array[x][y].point
1291           number = number + 1
1292         end
1293         x = x + 1
1294       end
1295     end
1296     hands.each do |piece|
1297       point = point + piece.point
1298     end
1299
1300     if (number < 10)
1301       puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1302       return false     
1303     end  
1304     if (sente)
1305       if (point < 28)
1306         puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1307         return false 
1308       end  
1309     else
1310       if (point < 27)
1311         puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1312         return false 
1313       end
1314     end
1315
1316     puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1317     return true
1318   end
1319
1320   # sente is nil only if tests in test_board run
1321   def handle_one_move(str, sente=nil)
1322     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1323       sg = $1
1324       x0 = $2.to_i
1325       y0 = $3.to_i
1326       x1 = $4.to_i
1327       y1 = $5.to_i
1328       name = $6
1329     elsif (str =~ /^%KACHI/)
1330       raise ArgumentError, "sente is null", caller if sente == nil
1331       if (good_kachi?(sente))
1332         return :kachi_win
1333       else
1334         return :kachi_lose
1335       end
1336     elsif (str =~ /^%TORYO/)
1337       return :toryo
1338     else
1339       return :illegal
1340     end
1341     
1342     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1343         ((x0 != 0) || (y0 != 0)))
1344       return :illegal
1345     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1346       return :illegal
1347     end
1348     
1349
1350     if (sg == "+")
1351       sente = true if sente == nil           # deprecated
1352       return :illegal unless sente == true   # black player's move must be black
1353       hands = @sente_hands
1354     else
1355       sente = false if sente == nil          # deprecated
1356       return :illegal unless sente == false  # white player's move must be white
1357       hands = @gote_hands
1358     end
1359     
1360     ## source check
1361     if ((x0 == 0) && (y0 == 0))
1362       return :illegal if (! have_piece?(hands, name))
1363     elsif (! @array[x0][y0])
1364       return :illegal           # no piece
1365     elsif (@array[x0][y0].sente != sente)
1366       return :illegal           # this is not mine
1367     elsif (@array[x0][y0].name != name)
1368       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1369     end
1370
1371     ## destination check
1372     if (@array[x1][y1] &&
1373         (@array[x1][y1].sente == sente)) # can't capture mine
1374       return :illegal
1375     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1376       return :illegal           # can't put on existing piece
1377     end
1378
1379     tmp_board = Marshal.load(Marshal.dump(self))
1380     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1381     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1382     tmp_board.update_sennichite(sente)
1383     os_result = tmp_board.oute_sennichite?(sente)
1384     return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
1385     return :sennichite if tmp_board.sennichite?(sente)
1386
1387     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1388       return :uchifuzume
1389     end
1390
1391     move_to(x0, y0, x1, y1, name, sente)
1392     str = to_s
1393
1394     update_sennichite(sente)
1395     return :normal
1396   end
1397
1398   def to_s
1399     a = Array::new
1400     y = 1
1401     while (y <= 9)
1402       a.push(sprintf("P%d", y))
1403       x = 9
1404       while (x >= 1)
1405         piece = @array[x][y]
1406         if (piece)
1407           s = piece.to_s
1408         else
1409           s = " * "
1410         end
1411         a.push(s)
1412         x = x - 1
1413       end
1414       a.push(sprintf("\n"))
1415       y = y + 1
1416     end
1417     if (! sente_hands.empty?)
1418       a.push("P+")
1419       sente_hands.each do |p|
1420         a.push("00" + p.name)
1421       end
1422       a.push("\n")
1423     end
1424     if (! gote_hands.empty?)
1425       a.push("P-")
1426       gote_hands.each do |p|
1427         a.push("00" + p.name)
1428       end
1429       a.push("\n")
1430     end
1431     a.push("+\n")
1432     return a.join
1433   end
1434 end
1435
1436 class GameResult
1437   attr_reader :players, :black, :white
1438
1439   def initialize(p1, p2)
1440     @players = [p1, p2]
1441     if p1.sente && !p2.sente
1442       @black, @white = p1, p2
1443     elsif !p1.sente && p2.sente
1444       @black, @white = p2, p1
1445     else
1446       raise "Never reached!"
1447     end
1448   end
1449 end
1450
1451 class GameResultWin < GameResult
1452   attr_reader :winner, :loser
1453
1454   def initialize(winner, loser)
1455     super
1456     @winner, @loser = winner, loser
1457   end
1458
1459   def to_s
1460     black_name = @black.id || @black.name
1461     white_name = @white.id || @white.name
1462     "%s:%s" % [black_name, white_name]
1463   end
1464 end
1465
1466 class GameResultDraw < GameResult
1467
1468 end
1469
1470 class Game
1471   @@mutex = Mutex.new
1472   @@time  = 0
1473
1474   def initialize(game_name, player0, player1)
1475     @monitors = Array::new
1476     @game_name = game_name
1477     if (@game_name =~ /-(\d+)-(\d+)$/)
1478       @total_time = $1.to_i
1479       @byoyomi = $2.to_i
1480     end
1481
1482     if (player0.sente)
1483       @sente = player0
1484       @gote = player1
1485     else
1486       @sente = player1
1487       @gote = player0
1488     end
1489     @current_player = @sente
1490     @next_player = @gote
1491
1492     @sente.game = self
1493     @gote.game = self
1494
1495     @last_move = ""
1496     @current_turn = 0
1497
1498     @sente.status = "agree_waiting"
1499     @gote.status = "agree_waiting"
1500     
1501     @id = sprintf("%s+%s+%s+%s+%s", 
1502                   LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1503     @logfile = File.join(LEAGUE.dir, @id + ".csa")
1504
1505     LEAGUE.games[@id] = self
1506
1507     log_message(sprintf("game created %s", @id))
1508
1509     @board = Board::new
1510     @board.initial
1511     @start_time = nil
1512     @fh = nil
1513     @result = nil
1514
1515     propose
1516   end
1517   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1518   attr_accessor :last_move, :current_turn
1519   attr_reader   :result
1520
1521   def rated?
1522     @sente.rated? && @gote.rated?
1523   end
1524
1525   def monitoron(monitor)
1526     @monitors.delete(monitor)
1527     @monitors.push(monitor)
1528   end
1529
1530   def monitoroff(monitor)
1531     @monitors.delete(monitor)
1532   end
1533
1534   def reject(rejector)
1535     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1536     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1537     finish
1538   end
1539
1540   def kill(killer)
1541     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1542       reject(killer.name)
1543     elsif (@current_player == killer)
1544       abnormal_lose()
1545       finish
1546     end
1547   end
1548
1549   def finish
1550     log_message(sprintf("game finished %s", @id))
1551     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1552     @fh.close
1553
1554     @sente.game = nil
1555     @gote.game = nil
1556     @sente.status = "connected"
1557     @gote.status = "connected"
1558
1559     if (@current_player.protocol == LoginCSA::PROTOCOL)
1560       @current_player.finish
1561     end
1562     if (@next_player.protocol == LoginCSA::PROTOCOL)
1563       @next_player.finish
1564     end
1565     @monitors = Array::new
1566     @sente = nil
1567     @gote = nil
1568     @current_player = nil
1569     @next_player = nil
1570     LEAGUE.games.delete(@id)
1571   end
1572
1573   def handle_one_move(str, player)
1574     finish_flag = true
1575     if (@current_player == player)
1576       @end_time = Time::new
1577       t = (@end_time - @start_time).floor
1578       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1579       
1580       move_status = nil
1581       if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1582         status = :timeout
1583       elsif (str == :timeout)
1584         return false            # time isn't expired. players aren't swapped. continue game
1585       else
1586         @current_player.mytime = @current_player.mytime - t
1587         if (@current_player.mytime < 0)
1588           @current_player.mytime = 0
1589         end
1590
1591 #        begin
1592           move_status = @board.handle_one_move(str, @sente == @current_player)
1593 #        rescue
1594 #          log_error("handle_one_move raise exception for #{str}")
1595 #          move_status = :illegal
1596 #        end
1597
1598         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1599           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1600         else
1601           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite_sente_lose) || (move_status == :oute_sennichite_gote_lose))
1602             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1603             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1604             @fh.printf("%s\nT%d\n", str, t)
1605             @last_move = sprintf("%s,T%d", str, t)
1606             @current_turn = @current_turn + 1
1607           end
1608
1609           @monitors.each do |monitor|
1610             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1611             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1612           end
1613         end
1614       end
1615
1616       if (@next_player.status != "game") # rival is logout or disconnected
1617         abnormal_win()
1618       elsif (status == :timeout)
1619         timeout_lose()
1620       elsif (move_status == :illegal)
1621         illegal_lose()
1622       elsif (move_status == :kachi_win)
1623         kachi_win()
1624       elsif (move_status == :kachi_lose)
1625         kachi_lose()
1626       elsif (move_status == :toryo)
1627         toryo_lose()
1628       elsif (move_status == :outori)
1629         outori_win()
1630       elsif (move_status == :oute_sennichite_sente_lose)
1631         oute_sennichite_win_lose(@gote, @sente) # Sente is checking
1632       elsif (move_status == :oute_sennichite_gote_lose)
1633         oute_sennichite_win_lose(@sente, @gote) # Gote is checking
1634       elsif (move_status == :sennichite)
1635         sennichite_draw()
1636       elsif (move_status == :uchifuzume)
1637         uchifuzume_lose()
1638       elsif (move_status == :oute_kaihimore)
1639         oute_kaihimore_lose()
1640       else
1641         finish_flag = false
1642       end
1643       finish() if finish_flag
1644       (@current_player, @next_player) = [@next_player, @current_player]
1645       @start_time = Time::new
1646       return finish_flag
1647     end
1648   end
1649
1650   def abnormal_win
1651     @current_player.status = "connected"
1652     @next_player.status = "connected"
1653     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1654     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1655     @fh.printf("%%TORYO\n")
1656     @fh.print(@board.to_s.gsub(/^/, "\'"))
1657     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1658     @result = GameResultWin.new(@current_player, @next_player)
1659     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1660     @monitors.each do |monitor|
1661       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1662     end
1663   end
1664
1665   def abnormal_lose
1666     @current_player.status = "connected"
1667     @next_player.status = "connected"
1668     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1669     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1670     @fh.printf("%%TORYO\n")
1671     @fh.print(@board.to_s.gsub(/^/, "\'"))
1672     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1673     @result = GameResultWin.new(@next_player, @current_player)
1674     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1675     @monitors.each do |monitor|
1676       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1677     end
1678   end
1679
1680   def sennichite_draw
1681     @current_player.status = "connected"
1682     @next_player.status = "connected"
1683     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1684     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1685     @fh.print(@board.to_s.gsub(/^/, "\'"))
1686     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1687     @result = GameResultDraw.new(@current_player, @next_player)
1688     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1689     @monitors.each do |monitor|
1690       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1691     end
1692   end
1693
1694   def oute_sennichite_win_lose(winner, loser)
1695     @current_player.status = "connected"
1696     @next_player.status = "connected"
1697     loser.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1698     winner.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1699     @fh.print(@board.to_s.gsub(/^/, "\'"))
1700     if loser == @current_player
1701       @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1702     else
1703       @fh.printf("'summary:oute_sennichite:%s win:%s lose\n", @current_player.name, @next_player.name)
1704     end
1705     @result = GameResultWin.new(winner, loser)
1706     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1707     @monitors.each do |monitor|
1708       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1709     end
1710   end
1711
1712   def illegal_lose
1713     @current_player.status = "connected"
1714     @next_player.status = "connected"
1715     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1716     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1717     @fh.print(@board.to_s.gsub(/^/, "\'"))
1718     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1719     @result = GameResultWin.new(@next_player, @current_player)
1720     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1721     @monitors.each do |monitor|
1722       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1723     end
1724   end
1725
1726   def uchifuzume_lose
1727     @current_player.status = "connected"
1728     @next_player.status = "connected"
1729     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1730     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1731     @fh.print(@board.to_s.gsub(/^/, "\'"))
1732     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1733     @result = GameResultWin.new(@next_player, @current_player)
1734     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1735     @monitors.each do |monitor|
1736       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1737     end
1738   end
1739
1740   def oute_kaihimore_lose
1741     @current_player.status = "connected"
1742     @next_player.status = "connected"
1743     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1744     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1745     @fh.print(@board.to_s.gsub(/^/, "\'"))
1746     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1747     @result = GameResultWin.new(@next_player, @current_player)
1748     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1749     @monitors.each do |monitor|
1750       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1751     end
1752   end
1753
1754   def timeout_lose
1755     @current_player.status = "connected"
1756     @next_player.status = "connected"
1757     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1758     @next_player.write_safe("#TIME_UP\n#WIN\n")
1759     @fh.print(@board.to_s.gsub(/^/, "\'"))
1760     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1761     @result = GameResultWin.new(@next_player, @current_player)
1762     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1763     @monitors.each do |monitor|
1764       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1765     end
1766   end
1767
1768   def kachi_win
1769     @current_player.status = "connected"
1770     @next_player.status = "connected"
1771     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1772     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1773     @fh.printf("%%KACHI\n")
1774     @fh.print(@board.to_s.gsub(/^/, "\'"))
1775     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1776     @result = GameResultWin.new(@current_player, @next_player)
1777     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1778     @monitors.each do |monitor|
1779       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1780     end
1781   end
1782
1783   def kachi_lose
1784     @current_player.status = "connected"
1785     @next_player.status = "connected"
1786     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1787     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1788     @fh.printf("%%KACHI\n")
1789     @fh.print(@board.to_s.gsub(/^/, "\'"))
1790     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1791     @result = GameResultWin.new(@next_player, @current_player)
1792     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1793     @monitors.each do |monitor|
1794       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1795     end
1796   end
1797
1798   def toryo_lose
1799     @current_player.status = "connected"
1800     @next_player.status = "connected"
1801     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1802     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1803     @fh.printf("%%TORYO\n")
1804     @fh.print(@board.to_s.gsub(/^/, "\'"))
1805     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1806     @result = GameResultWin.new(@next_player, @current_player)
1807     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1808     @monitors.each do |monitor|
1809       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1810     end
1811   end
1812
1813   def outori_win
1814     @current_player.status = "connected"
1815     @next_player.status = "connected"
1816     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1817     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1818     @fh.print(@board.to_s.gsub(/^/, "\'"))
1819     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1820     @result = GameResultWin.new(@current_player, @next_player)
1821     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1822     @monitors.each do |monitor|
1823       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1824     end
1825   end
1826
1827   def start
1828     log_message(sprintf("game started %s", @id))
1829     @sente.write_safe(sprintf("START:%s\n", @id))
1830     @gote.write_safe(sprintf("START:%s\n", @id))
1831     @sente.mytime = @total_time
1832     @gote.mytime = @total_time
1833     @start_time = Time::new
1834   end
1835
1836   def propose
1837     @fh = open(@logfile, "w")
1838     @fh.sync = true
1839
1840     @fh.puts("V2")
1841     @fh.puts("N+#{@sente.name}")
1842     @fh.puts("N-#{@gote.name}")
1843     @fh.puts("$EVENT:#{@id}")
1844
1845     @sente.write_safe(propose_message("+"))
1846     @gote.write_safe(propose_message("-"))
1847
1848     now = Time::new.strftime("%Y/%m/%d %H:%M:%S")
1849     @fh.puts("$START_TIME:#{now}")
1850     @fh.print <<EOM
1851 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1852 P2 * -HI *  *  *  *  * -KA * 
1853 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1854 P4 *  *  *  *  *  *  *  *  * 
1855 P5 *  *  *  *  *  *  *  *  * 
1856 P6 *  *  *  *  *  *  *  *  * 
1857 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1858 P8 * +KA *  *  *  *  * +HI * 
1859 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1860 +
1861 EOM
1862   end
1863
1864   def show()
1865     str0 = <<EOM
1866 BEGIN Game_Summary
1867 Protocol_Version:1.1
1868 Protocol_Mode:Server
1869 Format:Shogi 1.0
1870 Declaration:Jishogi 1.1
1871 Game_ID:#{@id}
1872 Name+:#{@sente.name}
1873 Name-:#{@gote.name}
1874 Rematch_On_Draw:NO
1875 To_Move:+
1876 BEGIN Time
1877 Time_Unit:1sec
1878 Total_Time:#{@total_time}
1879 Byoyomi:#{@byoyomi}
1880 Least_Time_Per_Move:#{Least_Time_Per_Move}
1881 Remaining_Time+:#{@sente.mytime}
1882 Remaining_Time-:#{@gote.mytime}
1883 Last_Move:#{@last_move}
1884 Current_Turn:#{@current_turn}
1885 END Time
1886 BEGIN Position
1887 EOM
1888
1889     str1 = <<EOM
1890 END Position
1891 END Game_Summary
1892 EOM
1893
1894     return str0 + @board.to_s + str1
1895   end
1896
1897   def propose_message(sg_flag)
1898     str = <<EOM
1899 BEGIN Game_Summary
1900 Protocol_Version:1.1
1901 Protocol_Mode:Server
1902 Format:Shogi 1.0
1903 Declaration:Jishogi 1.1
1904 Game_ID:#{@id}
1905 Name+:#{@sente.name}
1906 Name-:#{@gote.name}
1907 Your_Turn:#{sg_flag}
1908 Rematch_On_Draw:NO
1909 To_Move:+
1910 BEGIN Time
1911 Time_Unit:1sec
1912 Total_Time:#{@total_time}
1913 Byoyomi:#{@byoyomi}
1914 Least_Time_Per_Move:#{Least_Time_Per_Move}
1915 END Time
1916 BEGIN Position
1917 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1918 P2 * -HI *  *  *  *  * -KA * 
1919 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1920 P4 *  *  *  *  *  *  *  *  * 
1921 P5 *  *  *  *  *  *  *  *  * 
1922 P6 *  *  *  *  *  *  *  *  * 
1923 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1924 P8 * +KA *  *  *  *  * +HI * 
1925 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1926 P+
1927 P-
1928 +
1929 END Position
1930 END Game_Summary
1931 EOM
1932     return str
1933   end
1934   
1935   private
1936   
1937   def issue_current_time
1938     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1939     @@mutex.synchronize do
1940       while time <= @@time do
1941         time += 1
1942       end
1943       @@time = time
1944     end
1945   end
1946 end
1947 end # module ShogiServer
1948
1949 #################################################
1950 # MAIN
1951 #
1952
1953 def usage
1954     print <<EOM
1955 NAME
1956         shogi-server - server for CSA server protocol
1957
1958 SYNOPSIS
1959         shogi-server [OPTIONS] event_name port_number
1960
1961 DESCRIPTION
1962         server for CSA server protocol
1963
1964 OPTIONS
1965         --pid-file file
1966                 specify filename for logging process ID
1967     --daemon dir
1968         run as a daemon. Log files will be put in dir.
1969
1970 LICENSE
1971         this file is distributed under GPL version2 and might be compiled by Exerb
1972
1973 SEE ALSO
1974
1975 RELEASE
1976         #{ShogiServer::Release}
1977
1978 REVISION
1979         #{ShogiServer::Revision}
1980 EOM
1981 end
1982
1983 def log_debug(str)
1984   $logger.debug(str)
1985 end
1986
1987 def log_message(str)
1988   $logger.info(str)
1989 end
1990
1991 def log_warning(str)
1992   $logger.warn(str)
1993 end
1994
1995 def log_error(str)
1996   $logger.error(str)
1997 end
1998
1999
2000 def parse_command_line
2001   options = Hash::new
2002   parser = GetoptLong.new( ["--daemon",         GetoptLong::REQUIRED_ARGUMENT],
2003                            ["--pid-file",       GetoptLong::REQUIRED_ARGUMENT]
2004                          )
2005   parser.quiet = true
2006   begin
2007     parser.each_option do |name, arg|
2008       name.sub!(/^--/, '')
2009       options[name] = arg.dup
2010     end
2011   rescue
2012     usage
2013     raise parser.error_message
2014   end
2015   return options
2016 end
2017
2018 def write_pid_file(file)
2019   open(file, "w") do |fh|
2020     fh.print Process::pid, "\n"
2021   end
2022 end
2023
2024 def mutex_watchdog(mutex, sec)
2025   while true
2026     begin
2027       timeout(sec) do
2028         begin
2029           mutex.lock
2030         ensure
2031           mutex.unlock
2032         end
2033       end
2034       sleep(sec)
2035     rescue TimeoutError
2036       log_error("mutex watchdog timeout")
2037       exit(1)
2038     end
2039   end
2040 end
2041
2042 def main
2043
2044   $mutex = Mutex::new
2045   Thread::start do
2046     Thread.pass
2047     mutex_watchdog($mutex, 10)
2048   end
2049
2050   $options = parse_command_line
2051   if (ARGV.length != 2)
2052     usage
2053     exit 2
2054   end
2055
2056   LEAGUE.event = ARGV.shift
2057   port = ARGV.shift
2058
2059   write_pid_file($options["pid-file"]) if ($options["pid-file"])
2060
2061   dir = $options["daemon"] || nil
2062   if dir && ! File.exist?(dir)
2063     FileUtils.mkdir(dir)
2064   end
2065   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
2066   $logger = WEBrick::Log.new(log_file)
2067
2068   LEAGUE.dir = dir || File.dirname(__FILE__)
2069   LEAGUE.setup_players_database
2070
2071   config = {}
2072   config[:Port]       = port
2073   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
2074   config[:Logger]     = $logger
2075
2076   server = WEBrick::GenericServer.new(config)
2077   ["INT", "TERM"].each {|signal| trap(signal){ server.shutdown } }
2078   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
2079   log_message("server started [Revision: #{ShogiServer::Revision}]")
2080
2081   server.start do |client|
2082       # client.sync = true # this is already set in WEBrick 
2083       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
2084         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
2085       player = nil
2086       login  = nil
2087       while (str = client.gets_timeout(ShogiServer::Login_Time))
2088         begin
2089           $mutex.lock
2090           str =~ /([\r\n]*)$/
2091           eol = $1
2092           if (ShogiServer::Login::good_login?(str))
2093             player = ShogiServer::Player::new(str, client)
2094             player.eol = eol
2095             login  = ShogiServer::Login::factory(str, player)
2096             if (LEAGUE.players[player.name])
2097               if ((LEAGUE.players[player.name].password == player.password) &&
2098                   (LEAGUE.players[player.name].status != "game"))
2099                 log_message(sprintf("user %s login forcely", player.name))
2100                 LEAGUE.players[player.name].kill
2101               else
2102                 login.incorrect_duplicated_player(str)
2103                 #Thread::exit
2104                 #return
2105                 # TODO
2106                 player = nil
2107                 break
2108               end
2109             end
2110             LEAGUE.add(player)
2111             break
2112           else
2113             client.write_safe("LOGIN:incorrect" + eol)
2114             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2115           end
2116         ensure
2117           $mutex.unlock
2118         end
2119       end                       # login loop
2120       if (! player)
2121         #client.close
2122         #Thread::exit
2123         #return
2124         next
2125       end
2126       log_message(sprintf("user %s login", player.name))
2127       login.process
2128       player.run(login.csa_1st_str)
2129       begin
2130         $mutex.lock
2131         if (player.game)
2132           player.game.kill(player)
2133         end
2134         player.finish # socket has been closed
2135         LEAGUE.delete(player)
2136         log_message(sprintf("user %s logout", player.name))
2137       ensure
2138         $mutex.unlock
2139       end
2140   end
2141 end
2142
2143
2144 if ($0 == __FILE__)
2145   STDOUT.sync = true
2146   STDERR.sync = true
2147   TCPSocket.do_not_reverse_lookup = true
2148   Thread.abort_on_exception = true
2149
2150   LEAGUE = ShogiServer::League::new
2151   main
2152 end