OSDN Git Service

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