OSDN Git Service

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