OSDN Git Service

Some refactoring
[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     (1..9).each do |i|
1061       PieceFU::new(self, i, 3, false)
1062     end
1063
1064     PieceKY::new(self, 1, 9, true)
1065     PieceKE::new(self, 2, 9, true)
1066     PieceGI::new(self, 3, 9, true)
1067     PieceKI::new(self, 4, 9, true)
1068     PieceOU::new(self, 5, 9, true)
1069     PieceKI::new(self, 6, 9, true)
1070     PieceGI::new(self, 7, 9, true)
1071     PieceKE::new(self, 8, 9, true)
1072     PieceKY::new(self, 9, 9, true)
1073     PieceKA::new(self, 8, 8, true)
1074     PieceHI::new(self, 2, 8, true)
1075     (1..9).each do |i|
1076       PieceFU::new(self, i, 7, true)
1077     end
1078   end
1079
1080   def have_piece?(hands, name)
1081     piece = hands.find { |i|
1082       i.name == name
1083     }
1084     return piece
1085   end
1086
1087   def move_to(x0, y0, x1, y1, name, sente)
1088     if (sente)
1089       hands = @sente_hands
1090     else
1091       hands = @gote_hands
1092     end
1093
1094     if ((x0 == 0) || (y0 == 0))
1095       piece = have_piece?(hands, name)
1096       return :illegal if (! piece.move_to?(x1, y1, name)) # TODO null check for the piece?
1097       piece.move_to(x1, y1)
1098     else
1099       return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))  # TODO null check?
1100       if (@array[x0][y0].name != name) # promoted ?
1101         @array[x0][y0].promoted = true
1102       end
1103       if (@array[x1][y1]) # capture
1104         if (@array[x1][y1].name == "OU")
1105           return :outori        # return board update
1106         end
1107         @array[x1][y1].sente = @array[x0][y0].sente
1108         @array[x1][y1].move_to(0, 0)
1109         hands.sort! {|a, b| # TODO refactor. Move to Piece class
1110           a.name <=> b.name
1111         }
1112       end
1113       @array[x0][y0].move_to(x1, y1)
1114     end
1115     @move_count += 1
1116     return true
1117   end
1118
1119   def look_for_ou(sente)
1120     x = 1
1121     while (x <= 9)
1122       y = 1
1123       while (y <= 9)
1124         if (@array[x][y] &&
1125             (@array[x][y].name == "OU") &&
1126             (@array[x][y].sente == sente))
1127           return @array[x][y]
1128         end
1129         y = y + 1
1130       end
1131       x = x + 1
1132     end
1133     raise "can't find ou"
1134   end
1135
1136   # note checkmate, but check. sente is checked.
1137   def checkmated?(sente)        # sente is loosing
1138     ou = look_for_ou(sente)
1139     x = 1
1140     while (x <= 9)
1141       y = 1
1142       while (y <= 9)
1143         if (@array[x][y] &&
1144             (@array[x][y].sente != sente))
1145           if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1146             return true
1147           end
1148         end
1149         y = y + 1
1150       end
1151       x = x + 1
1152     end
1153     return false
1154   end
1155
1156   def uchifuzume?(sente)
1157     rival_ou = look_for_ou(! sente)   # rival's ou
1158     if (sente)                  # rival is gote
1159       if ((rival_ou.y != 9) &&
1160           (@array[rival_ou.x][rival_ou.y + 1]) &&
1161           (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1162           (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1163         fu_x = rival_ou.x
1164         fu_y = rival_ou.y + 1
1165       else
1166         return false
1167       end
1168     else                        # gote
1169       if ((rival_ou.y != 0) &&
1170           (@array[rival_ou.x][rival_ou.y - 1]) &&
1171           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1172           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1173         fu_x = rival_ou.x
1174         fu_y = rival_ou.y - 1
1175       else
1176         return false
1177       end
1178     end
1179     
1180     ## case: rival_ou is moving
1181     escaped = false
1182     rival_ou.movable_grids.each do |(cand_x, cand_y)|
1183       tmp_board = Marshal.load(Marshal.dump(self))
1184       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1185       raise "internal error" if (s != true)
1186       if (! tmp_board.checkmated?(! sente)) # good move
1187         return false
1188       end
1189     end
1190
1191     ## case: rival is capturing fu
1192     x = 1
1193     while (x <= 9)
1194       y = 1
1195       while (y <= 9)
1196         if (@array[x][y] &&
1197             (@array[x][y].sente != sente) &&
1198             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1199           if (@array[x][y].promoted)
1200             name = @array[x][y].promoted_name
1201           else
1202             name = @array[x][y].name
1203           end
1204           tmp_board = Marshal.load(Marshal.dump(self))
1205           s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1206           raise "internal error" if (s != true)
1207           if (! tmp_board.checkmated?(! sente)) # good move
1208             return false
1209           end
1210         end
1211         y = y + 1
1212       end
1213       x = x + 1
1214     end
1215     return true
1216   end
1217
1218   def oute_sennichite?(sente)
1219     if (checkmated?(! sente))
1220       str = to_s
1221       if (sente)
1222         if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
1223           return true
1224         end
1225       else
1226         if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
1227           return true
1228         end
1229       end
1230     end
1231     return false
1232   end
1233
1234   def sennichite?(sente)
1235     str = to_s
1236     if (@history[str] && (@history[str] >= 3)) # already 3 times
1237       return true
1238     end
1239     return false
1240   end
1241
1242   def good_kachi?(sente)
1243     if (checkmated?(sente))
1244       puts "'NG: Checkmating." if $DEBUG
1245       return false 
1246     end
1247     
1248     ou = look_for_ou(sente)
1249     if (sente && (ou.y >= 4))
1250       puts "'NG: Black's OU does not enter yet." if $DEBUG
1251       return false     
1252     end  
1253     if (! sente && (ou.y <= 6))
1254       puts "'NG: White's OU does not enter yet." if $DEBUG
1255       return false 
1256     end
1257       
1258     number = 0
1259     point = 0
1260
1261     if (sente)
1262       hands = @sente_hands
1263       r = [1, 2, 3]
1264     else
1265       hands = @gote_hands
1266       r = [7, 8, 9]
1267     end
1268     r.each do |y|
1269       x = 1
1270       while (x <= 9)
1271         if (@array[x][y] &&
1272             (@array[x][y].sente == sente) &&
1273             (@array[x][y].point > 0))
1274           point = point + @array[x][y].point
1275           number = number + 1
1276         end
1277         x = x + 1
1278       end
1279     end
1280     hands.each do |piece|
1281       point = point + piece.point
1282     end
1283
1284     if (number < 10)
1285       puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1286       return false     
1287     end  
1288     if (sente)
1289       if (point < 28)
1290         puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1291         return false 
1292       end  
1293     else
1294       if (point < 27)
1295         puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1296         return false 
1297       end
1298     end
1299
1300     puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1301     return true
1302   end
1303
1304   # sente is nil only if tests in test_board run
1305   def handle_one_move(str, sente=nil)
1306     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1307       sg = $1
1308       x0 = $2.to_i
1309       y0 = $3.to_i
1310       x1 = $4.to_i
1311       y1 = $5.to_i
1312       name = $6
1313     elsif (str =~ /^%KACHI/)
1314       raise ArgumentError, "sente is null", caller if sente == nil
1315       if (good_kachi?(sente))
1316         return :kachi_win
1317       else
1318         return :kachi_lose
1319       end
1320     elsif (str =~ /^%TORYO/)
1321       return :toryo
1322     else
1323       return :illegal
1324     end
1325     
1326     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1327         ((x0 != 0) || (y0 != 0)))
1328       return :illegal
1329     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1330       return :illegal
1331     end
1332     
1333
1334     if (sg == "+")
1335       sente = true if sente == nil           # deprecated
1336       return :illegal unless sente == true   # black player's move must be black
1337       hands = @sente_hands
1338     else
1339       sente = false if sente == nil          # deprecated
1340       return :illegal unless sente == false  # white player's move must be white
1341       hands = @gote_hands
1342     end
1343     
1344     ## source check
1345     if ((x0 == 0) && (y0 == 0))
1346       return :illegal if (! have_piece?(hands, name))
1347     elsif (! @array[x0][y0])
1348       return :illegal           # no piece
1349     elsif (@array[x0][y0].sente != sente)
1350       return :illegal           # this is not mine
1351     elsif (@array[x0][y0].name != name)
1352       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1353     end
1354
1355     ## destination check
1356     if (@array[x1][y1] &&
1357         (@array[x1][y1].sente == sente)) # can't capture mine
1358       return :illegal
1359     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1360       return :illegal           # can't put on existing piece
1361     end
1362
1363     tmp_board = Marshal.load(Marshal.dump(self))
1364     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1365     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1366     return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1367     return :sennichite if tmp_board.sennichite?(sente)
1368
1369     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1370       return :uchifuzume
1371     end
1372
1373     move_to(x0, y0, x1, y1, name, sente)
1374     str = to_s
1375
1376     if (checkmated?(! sente))
1377       if (sente)
1378         @sente_history[str] = (@sente_history[str] || 0) + 1
1379       else
1380         @gote_history[str] = (@gote_history[str] || 0) + 1
1381       end
1382     else
1383       if (sente)
1384         @sente_history.clear
1385       else
1386         @gote_history.clear
1387       end
1388     end
1389     @history[str] = (@history[str] || 0) + 1
1390     return :normal
1391   end
1392
1393   def to_s
1394     a = Array::new
1395     y = 1
1396     while (y <= 9)
1397       a.push(sprintf("P%d", y))
1398       x = 9
1399       while (x >= 1)
1400         piece = @array[x][y]
1401         if (piece)
1402           s = piece.to_s
1403         else
1404           s = " * "
1405         end
1406         a.push(s)
1407         x = x - 1
1408       end
1409       a.push(sprintf("\n"))
1410       y = y + 1
1411     end
1412     if (! sente_hands.empty?)
1413       a.push("P+")
1414       sente_hands.each do |p|
1415         a.push("00" + p.name)
1416       end
1417       a.push("\n")
1418     end
1419     if (! gote_hands.empty?)
1420       a.push("P-")
1421       gote_hands.each do |p|
1422         a.push("00" + p.name)
1423       end
1424       a.push("\n")
1425     end
1426     a.push("+\n")
1427     return a.join
1428   end
1429 end
1430
1431 class GameResult
1432   attr_reader :players, :black, :white
1433
1434   def initialize(p1, p2)
1435     @players = []
1436     @players << p1
1437     @players << p2
1438     if p1.sente && !p2.sente
1439       @black, @white = p1, p2
1440     elsif !p1.sente && p2.sente
1441       @black, @white = p2, p1
1442     else
1443       raise "Never reached!"
1444     end
1445   end
1446 end
1447
1448 class GameResultWin < GameResult
1449   attr_reader :winner, :loser
1450
1451   def initialize(winner, loser)
1452     super
1453     @winner, @loser = winner, loser
1454   end
1455
1456   def to_s
1457     black_name = @black.id || @black.name
1458     white_name = @white.id || @white.name
1459     "%s:%s" % [black_name, white_name]
1460   end
1461 end
1462
1463 class GameResultDraw < GameResult
1464
1465 end
1466
1467 class Game
1468   @@mutex = Mutex.new
1469   @@time  = 0
1470
1471   def initialize(game_name, player0, player1)
1472     @monitors = Array::new
1473     @game_name = game_name
1474     if (@game_name =~ /-(\d+)-(\d+)$/)
1475       @total_time = $1.to_i
1476       @byoyomi = $2.to_i
1477     end
1478
1479     if (player0.sente)
1480       @sente = player0
1481       @gote = player1
1482     else
1483       @sente = player1
1484       @gote = player0
1485     end
1486     @current_player = @sente
1487     @next_player = @gote
1488
1489     @sente.game = self
1490     @gote.game = self
1491
1492     @last_move = ""
1493     @current_turn = 0
1494
1495     @sente.status = "agree_waiting"
1496     @gote.status = "agree_waiting"
1497     
1498     @id = sprintf("%s+%s+%s+%s+%s", 
1499                   LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1500     @logfile = File.join(LEAGUE.dir, @id + ".csa")
1501
1502     LEAGUE.games[@id] = self
1503
1504     log_message(sprintf("game created %s", @id))
1505
1506     @board = Board::new
1507     @board.initial
1508     @start_time = nil
1509     @fh = nil
1510     @result = nil
1511
1512     propose
1513   end
1514   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1515   attr_accessor :last_move, :current_turn
1516   attr_reader   :result
1517
1518   def rated?
1519     @sente.rated? && @gote.rated?
1520   end
1521
1522   def monitoron(monitor)
1523     @monitors.delete(monitor)
1524     @monitors.push(monitor)
1525   end
1526
1527   def monitoroff(monitor)
1528     @monitors.delete(monitor)
1529   end
1530
1531   def reject(rejector)
1532     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1533     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1534     finish
1535   end
1536
1537   def kill(killer)
1538     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1539       reject(killer.name)
1540     elsif (@current_player == killer)
1541       abnormal_lose()
1542       finish
1543     end
1544   end
1545
1546   def finish
1547     log_message(sprintf("game finished %s", @id))
1548     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1549     @fh.close
1550
1551     @sente.game = nil
1552     @gote.game = nil
1553     @sente.status = "connected"
1554     @gote.status = "connected"
1555
1556     if (@current_player.protocol == LoginCSA::PROTOCOL)
1557       @current_player.finish
1558     end
1559     if (@next_player.protocol == LoginCSA::PROTOCOL)
1560       @next_player.finish
1561     end
1562     @monitors = Array::new
1563     @sente = nil
1564     @gote = nil
1565     @current_player = nil
1566     @next_player = nil
1567     LEAGUE.games.delete(@id)
1568   end
1569
1570   def handle_one_move(str, player)
1571     finish_flag = true
1572     if (@current_player == player)
1573       @end_time = Time::new
1574       t = (@end_time - @start_time).floor
1575       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1576       
1577       move_status = nil
1578       if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1579         status = :timeout
1580       elsif (str == :timeout)
1581         return false            # time isn't expired. players aren't swapped. continue game
1582       else
1583         @current_player.mytime = @current_player.mytime - t
1584         if (@current_player.mytime < 0)
1585           @current_player.mytime = 0
1586         end
1587
1588 #        begin
1589           move_status = @board.handle_one_move(str, @sente == @current_player)
1590 #        rescue
1591 #          log_error("handle_one_move raise exception for #{str}")
1592 #          move_status = :illegal
1593 #        end
1594
1595         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1596           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1597         else
1598           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1599             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1600             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1601             @fh.printf("%s\nT%d\n", str, t)
1602             @last_move = sprintf("%s,T%d", str, t)
1603             @current_turn = @current_turn + 1
1604           end
1605
1606           @monitors.each do |monitor|
1607             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1608             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1609           end
1610         end
1611       end
1612
1613       if (@next_player.status != "game") # rival is logout or disconnected
1614         abnormal_win()
1615       elsif (status == :timeout)
1616         timeout_lose()
1617       elsif (move_status == :illegal)
1618         illegal_lose()
1619       elsif (move_status == :kachi_win)
1620         kachi_win()
1621       elsif (move_status == :kachi_lose)
1622         kachi_lose()
1623       elsif (move_status == :toryo)
1624         toryo_lose()
1625       elsif (move_status == :outori)
1626         outori_win()
1627       elsif (move_status == :sennichite)
1628         sennichite_draw()
1629       elsif (move_status == :oute_sennichite)
1630         oute_sennichite_lose()
1631       elsif (move_status == :uchifuzume)
1632         uchifuzume_lose()
1633       elsif (move_status == :oute_kaihimore)
1634         oute_kaihimore_lose()
1635       else
1636         finish_flag = false
1637       end
1638       finish() if finish_flag
1639       (@current_player, @next_player) = [@next_player, @current_player]
1640       @start_time = Time::new
1641       return finish_flag
1642     end
1643   end
1644
1645   def abnormal_win
1646     @current_player.status = "connected"
1647     @next_player.status = "connected"
1648     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1649     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1650     @fh.printf("%%TORYO\n")
1651     @fh.print(@board.to_s.gsub(/^/, "\'"))
1652     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1653     @result = GameResultWin.new(@current_player, @next_player)
1654     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1655     @monitors.each do |monitor|
1656       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1657     end
1658   end
1659
1660   def abnormal_lose
1661     @current_player.status = "connected"
1662     @next_player.status = "connected"
1663     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1664     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1665     @fh.printf("%%TORYO\n")
1666     @fh.print(@board.to_s.gsub(/^/, "\'"))
1667     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1668     @result = GameResultWin.new(@next_player, @current_player)
1669     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1670     @monitors.each do |monitor|
1671       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1672     end
1673   end
1674
1675   def sennichite_draw
1676     @current_player.status = "connected"
1677     @next_player.status = "connected"
1678     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1679     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1680     @fh.print(@board.to_s.gsub(/^/, "\'"))
1681     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1682     @result = GameResultDraw.new(@current_player, @next_player)
1683     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1684     @monitors.each do |monitor|
1685       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1686     end
1687   end
1688
1689   def oute_sennichite_lose
1690     @current_player.status = "connected"
1691     @next_player.status = "connected"
1692     @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1693     @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1694     @fh.print(@board.to_s.gsub(/^/, "\'"))
1695     @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1696     @result = GameResultWin.new(@next_player, @current_player)
1697     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1698     @monitors.each do |monitor|
1699       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1700     end
1701   end
1702
1703   def illegal_lose
1704     @current_player.status = "connected"
1705     @next_player.status = "connected"
1706     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1707     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1708     @fh.print(@board.to_s.gsub(/^/, "\'"))
1709     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1710     @result = GameResultWin.new(@next_player, @current_player)
1711     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1712     @monitors.each do |monitor|
1713       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1714     end
1715   end
1716
1717   def uchifuzume_lose
1718     @current_player.status = "connected"
1719     @next_player.status = "connected"
1720     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1721     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1722     @fh.print(@board.to_s.gsub(/^/, "\'"))
1723     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1724     @result = GameResultWin.new(@next_player, @current_player)
1725     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1726     @monitors.each do |monitor|
1727       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1728     end
1729   end
1730
1731   def oute_kaihimore_lose
1732     @current_player.status = "connected"
1733     @next_player.status = "connected"
1734     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1735     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1736     @fh.print(@board.to_s.gsub(/^/, "\'"))
1737     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1738     @result = GameResultWin.new(@next_player, @current_player)
1739     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1740     @monitors.each do |monitor|
1741       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1742     end
1743   end
1744
1745   def timeout_lose
1746     @current_player.status = "connected"
1747     @next_player.status = "connected"
1748     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1749     @next_player.write_safe("#TIME_UP\n#WIN\n")
1750     @fh.print(@board.to_s.gsub(/^/, "\'"))
1751     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1752     @result = GameResultWin.new(@next_player, @current_player)
1753     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1754     @monitors.each do |monitor|
1755       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1756     end
1757   end
1758
1759   def kachi_win
1760     @current_player.status = "connected"
1761     @next_player.status = "connected"
1762     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1763     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1764     @fh.printf("%%KACHI\n")
1765     @fh.print(@board.to_s.gsub(/^/, "\'"))
1766     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1767     @result = GameResultWin.new(@current_player, @next_player)
1768     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1769     @monitors.each do |monitor|
1770       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1771     end
1772   end
1773
1774   def kachi_lose
1775     @current_player.status = "connected"
1776     @next_player.status = "connected"
1777     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1778     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1779     @fh.printf("%%KACHI\n")
1780     @fh.print(@board.to_s.gsub(/^/, "\'"))
1781     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1782     @result = GameResultWin.new(@next_player, @current_player)
1783     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1784     @monitors.each do |monitor|
1785       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1786     end
1787   end
1788
1789   def toryo_lose
1790     @current_player.status = "connected"
1791     @next_player.status = "connected"
1792     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1793     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1794     @fh.printf("%%TORYO\n")
1795     @fh.print(@board.to_s.gsub(/^/, "\'"))
1796     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1797     @result = GameResultWin.new(@next_player, @current_player)
1798     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1799     @monitors.each do |monitor|
1800       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1801     end
1802   end
1803
1804   def outori_win
1805     @current_player.status = "connected"
1806     @next_player.status = "connected"
1807     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1808     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1809     @fh.print(@board.to_s.gsub(/^/, "\'"))
1810     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1811     @result = GameResultWin.new(@current_player, @next_player)
1812     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1813     @monitors.each do |monitor|
1814       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1815     end
1816   end
1817
1818   def start
1819     log_message(sprintf("game started %s", @id))
1820     @sente.write_safe(sprintf("START:%s\n", @id))
1821     @gote.write_safe(sprintf("START:%s\n", @id))
1822     @sente.mytime = @total_time
1823     @gote.mytime = @total_time
1824     @start_time = Time::new
1825   end
1826
1827   def propose
1828     begin
1829       @fh = open(@logfile, "w")
1830       @fh.sync = true
1831
1832       @fh.printf("V2\n")
1833       @fh.printf("N+%s\n", @sente.name)
1834       @fh.printf("N-%s\n", @gote.name)
1835       @fh.printf("$EVENT:%s\n", @id)
1836
1837       @sente.write_safe(propose_message("+"))
1838       @gote.write_safe(propose_message("-"))
1839
1840       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1841       @fh.print <<EOM
1842 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1843 P2 * -HI *  *  *  *  * -KA * 
1844 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1845 P4 *  *  *  *  *  *  *  *  * 
1846 P5 *  *  *  *  *  *  *  *  * 
1847 P6 *  *  *  *  *  *  *  *  * 
1848 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1849 P8 * +KA *  *  *  *  * +HI * 
1850 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1851 +
1852 EOM
1853     end
1854   end
1855
1856   def show()
1857     str0 = <<EOM
1858 BEGIN Game_Summary
1859 Protocol_Version:1.1
1860 Protocol_Mode:Server
1861 Format:Shogi 1.0
1862 Declaration:Jishogi 1.1
1863 Game_ID:#{@id}
1864 Name+:#{@sente.name}
1865 Name-:#{@gote.name}
1866 Rematch_On_Draw:NO
1867 To_Move:+
1868 BEGIN Time
1869 Time_Unit:1sec
1870 Total_Time:#{@total_time}
1871 Byoyomi:#{@byoyomi}
1872 Least_Time_Per_Move:#{Least_Time_Per_Move}
1873 Remaining_Time+:#{@sente.mytime}
1874 Remaining_Time-:#{@gote.mytime}
1875 Last_Move:#{@last_move}
1876 Current_Turn:#{@current_turn}
1877 END Time
1878 BEGIN Position
1879 EOM
1880
1881     str1 = <<EOM
1882 END Position
1883 END Game_Summary
1884 EOM
1885
1886     return str0 + @board.to_s + str1
1887   end
1888
1889   def propose_message(sg_flag)
1890     str = <<EOM
1891 BEGIN Game_Summary
1892 Protocol_Version:1.1
1893 Protocol_Mode:Server
1894 Format:Shogi 1.0
1895 Declaration:Jishogi 1.1
1896 Game_ID:#{@id}
1897 Name+:#{@sente.name}
1898 Name-:#{@gote.name}
1899 Your_Turn:#{sg_flag}
1900 Rematch_On_Draw:NO
1901 To_Move:+
1902 BEGIN Time
1903 Time_Unit:1sec
1904 Total_Time:#{@total_time}
1905 Byoyomi:#{@byoyomi}
1906 Least_Time_Per_Move:#{Least_Time_Per_Move}
1907 END Time
1908 BEGIN Position
1909 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1910 P2 * -HI *  *  *  *  * -KA * 
1911 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1912 P4 *  *  *  *  *  *  *  *  * 
1913 P5 *  *  *  *  *  *  *  *  * 
1914 P6 *  *  *  *  *  *  *  *  * 
1915 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1916 P8 * +KA *  *  *  *  * +HI * 
1917 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1918 P+
1919 P-
1920 +
1921 END Position
1922 END Game_Summary
1923 EOM
1924     return str
1925   end
1926   
1927   private
1928   
1929   def issue_current_time
1930     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1931     @@mutex.synchronize do
1932       while time <= @@time do
1933         time += 1
1934       end
1935       @@time = time
1936     end
1937   end
1938 end
1939 end # module ShogiServer
1940
1941 #################################################
1942 # MAIN
1943 #
1944
1945 def usage
1946     print <<EOM
1947 NAME
1948         shogi-server - server for CSA server protocol
1949
1950 SYNOPSIS
1951         shogi-server [OPTIONS] event_name port_number
1952
1953 DESCRIPTION
1954         server for CSA server protocol
1955
1956 OPTIONS
1957         --pid-file file
1958                 specify filename for logging process ID
1959     --daemon dir
1960         run as a daemon. Log files will be put in dir.
1961
1962 LICENSE
1963         this file is distributed under GPL version2 and might be compiled by Exerb
1964
1965 SEE ALSO
1966
1967 RELEASE
1968         #{ShogiServer::Release}
1969
1970 REVISION
1971         #{ShogiServer::Revision}
1972 EOM
1973 end
1974
1975 def log_message(str)
1976   $logger.info(str)
1977 end
1978
1979 def log_warning(str)
1980   $logger.warn(str)
1981 end
1982
1983 def log_error(str)
1984   $logger.error(str)
1985 end
1986
1987
1988 def parse_command_line
1989   options = Hash::new
1990   parser = GetoptLong.new( ["--daemon",         GetoptLong::REQUIRED_ARGUMENT],
1991                            ["--pid-file",       GetoptLong::REQUIRED_ARGUMENT]
1992                          )
1993   parser.quiet = true
1994   begin
1995     parser.each_option do |name, arg|
1996       name.sub!(/^--/, '')
1997       options[name] = arg.dup
1998     end
1999   rescue
2000     usage
2001     raise parser.error_message
2002   end
2003   return options
2004 end
2005
2006 def write_pid_file(file)
2007   open(file, "w") do |fh|
2008     fh.print Process::pid, "\n"
2009   end
2010 end
2011
2012 def mutex_watchdog(mutex, sec)
2013   while true
2014     begin
2015       timeout(sec) do
2016         begin
2017           mutex.lock
2018         ensure
2019           mutex.unlock
2020         end
2021       end
2022       sleep(sec)
2023     rescue TimeoutError
2024       log_error("mutex watchdog timeout")
2025       exit(1)
2026     end
2027   end
2028 end
2029
2030 def main
2031
2032   $mutex = Mutex::new
2033   Thread::start do
2034     Thread.pass
2035     mutex_watchdog($mutex, 10)
2036   end
2037
2038   $options = parse_command_line
2039   if (ARGV.length != 2)
2040     usage
2041     exit 2
2042   end
2043
2044   LEAGUE.event = ARGV.shift
2045   port = ARGV.shift
2046
2047   write_pid_file($options["pid-file"]) if ($options["pid-file"])
2048
2049   dir = $options["daemon"] || nil
2050   if dir && ! File.exist?(dir)
2051     FileUtils.mkdir(dir)
2052   end
2053   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
2054   $logger = WEBrick::Log.new(log_file)
2055
2056   LEAGUE.dir = dir || File.dirname(__FILE__)
2057   LEAGUE.setup_players_database
2058
2059   config = {}
2060   config[:Port]       = port
2061   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
2062   config[:Logger]     = $logger
2063
2064   server = WEBrick::GenericServer.new(config)
2065   ["INT", "TERM"].each {|signal| trap(signal){ server.shutdown } }
2066   $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
2067   log_message("server started [Revision: #{ShogiServer::Revision}]")
2068
2069   server.start do |client|
2070       # client.sync = true # this is already set in WEBrick 
2071       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
2072         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
2073       player = nil
2074       login  = nil
2075       while (str = client.gets_timeout(ShogiServer::Login_Time))
2076         begin
2077           $mutex.lock
2078           str =~ /([\r\n]*)$/
2079           eol = $1
2080           if (ShogiServer::Login::good_login?(str))
2081             player = ShogiServer::Player::new(str, client)
2082             player.eol = eol
2083             login  = ShogiServer::Login::factory(str, player)
2084             if (LEAGUE.players[player.name])
2085               if ((LEAGUE.players[player.name].password == player.password) &&
2086                   (LEAGUE.players[player.name].status != "game"))
2087                 log_message(sprintf("user %s login forcely", player.name))
2088                 LEAGUE.players[player.name].kill
2089               else
2090                 login.incorrect_duplicated_player(str)
2091                 #Thread::exit
2092                 #return
2093                 # TODO
2094                 player = nil
2095                 break
2096               end
2097             end
2098             LEAGUE.add(player)
2099             break
2100           else
2101             client.write_safe("LOGIN:incorrect" + eol)
2102             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2103           end
2104         ensure
2105           $mutex.unlock
2106         end
2107       end                       # login loop
2108       if (! player)
2109         #client.close
2110         #Thread::exit
2111         #return
2112         next
2113       end
2114       log_message(sprintf("user %s login", player.name))
2115       login.process
2116       player.run(login.csa_1st_str)
2117       begin
2118         $mutex.lock
2119         if (player.game)
2120           player.game.kill(player)
2121         end
2122         player.finish # socket has been closed
2123         LEAGUE.delete(player)
2124         log_message(sprintf("user %s logout", player.name))
2125       ensure
2126         $mutex.unlock
2127       end
2128   end
2129 end
2130
2131
2132 if ($0 == __FILE__)
2133   STDOUT.sync = true
2134   STDERR.sync = true
2135   TCPSocket.do_not_reverse_lookup = true
2136   Thread.abort_on_exception = true
2137
2138   LEAGUE = ShogiServer::League::new
2139   main
2140 end