4 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
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.
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.
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
20 Max_Write_Queue_Size = 1000
21 Max_Identifier_Length = 32
22 Default_Timeout = 60 # for single socket operation
24 Default_Game_Name = "default-1500-0"
27 Least_Time_Per_Move = 1
28 Login_Time = 300 # time for LOGIN
30 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
31 Release.concat("-") if (Release == "")
32 Revision = "$Revision$".gsub(/[^\.\d]/, '')
45 TCPSocket.do_not_reverse_lookup = true
46 Thread.abort_on_exception = true
50 def gets_timeout(t = Default_Timeout)
61 def gets_safe(t = nil)
82 return self.write(str)
95 @db = YAML::Store.new( File.join(File.dirname(__FILE__), "players.yaml") )
97 attr_accessor :players, :games, :event
100 self.load(player) if player.id
101 @players[player.name] = player
105 @players.delete(player.name)
108 def get_player(status, game_name, sente, searcher=nil)
109 @players.each do |name, player|
110 if ((player.status == status) &&
111 (player.game_name == game_name) &&
112 ((sente == nil) || (player.sente == nil) || (player.sente == sente)) &&
113 ((searcher == nil) || (player != searcher)))
121 hash = search(player.id)
124 player.name = hash['name']
125 player.rate = hash['rate']
126 player.modified_at = hash['modified_at']
140 @db.transaction(true) do
141 @db.roots.each do |id|
145 return players.collect do |id|
156 # Idetifier of the player in the rating system
160 # Score in the rating sysem
162 # Last timestamp when the rate was modified
163 attr_accessor :modified_at
166 @modified_at || Time.now
172 @modified_at = Time.now
181 class Player < RatedPlayer
182 def initialize(str, socket)
186 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
188 @protocol = nil # CSA or x1
189 @eol = "\m" # favorite eol code
192 @mytime = 0 # set in start method also
196 @write_queue = Queue::new
200 attr_accessor :password, :socket, :status
201 attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
202 attr_accessor :main_thread, :writer_thread, :write_queue
205 log_message(sprintf("user %s killed", @name))
210 Thread::kill(@main_thread) if @main_thread
214 if (@status != "finished")
216 log_message(sprintf("user %s finish", @name))
217 Thread::kill(@writer_thread) if @writer_thread
219 @socket.close if (! @socket.closed?)
221 log_message(sprintf("user %s finish failed", @name))
227 @write_queue.push(str.gsub(/[\r\n]+/, @eol))
231 while (str = @write_queue.pop)
232 @socket.write_safe(str)
237 if ((status == "game_waiting") ||
238 (status == "start_waiting") ||
239 (status == "agree_waiting") ||
242 return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
243 elsif (@sente == false)
244 return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
245 elsif (@sente == nil)
246 return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
249 return sprintf("%s %s %s", @name, @protocol, @status)
254 @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
261 (login, @name, @password, ext) = str.split
262 @name, trip = @name.split(",") # used by rating
263 @id = trip ? "%s+%s" % [@name, Digest::MD5.hexdigest(trip)] : nil
269 @main_thread = Thread::current
270 @writer_thread = Thread::start do
276 write_safe(sprintf("LOGIN:%s OK\n", @name))
277 if (@protocol != "CSA")
278 log_message(sprintf("user %s run in %s mode", @name, @protocol))
279 write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
281 log_message(sprintf("user %s run in CSA mode", @name))
282 if (good_game_name?(@password))
283 csa_1st_str = "%%GAME #{@password} *"
285 csa_1st_str = "%%GAME #{Default_Game_Name} *"
289 while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
296 if (@write_queue.size > Max_Write_Queue_Size)
297 log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
301 if (@status == "finished")
304 str.chomp! if (str.class == String)
307 if (@status == "game")
308 array_str = str.split(",")
309 move = array_str.shift
310 additional = array_str.shift
311 if /^'(.*)/ =~ additional
312 comment = array_str.unshift("'*#{$1}")
314 s = @game.handle_one_move(move, self)
315 @game.fh.print("#{comment}\n") if (comment && !s)
316 return if (s && @protocol == "CSA")
318 when /^%[^%]/, :timeout
319 if (@status == "game")
320 s = @game.handle_one_move(str, self)
321 return if (s && @protocol == "CSA")
324 if (@status == "agree_waiting")
326 return if (@protocol == "CSA")
328 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
331 if (@status == "agree_waiting")
332 @status = "start_waiting"
333 if ((@game.sente.status == "start_waiting") &&
334 (@game.gote.status == "start_waiting"))
336 @game.sente.status = "game"
337 @game.gote.status = "game"
340 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
342 when /^%%SHOW\s+(\S+)/
344 if (LEAGUE.games[game_id])
345 write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
347 write_safe("##[SHOW] +OK\n")
348 when /^%%MONITORON\s+(\S+)/
350 if (LEAGUE.games[game_id])
351 LEAGUE.games[game_id].monitoron(self)
352 write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
353 write_safe("##[MONITOR][#{game_id}] +OK\n")
355 when /^%%MONITOROFF\s+(\S+)/
357 if (LEAGUE.games[game_id])
358 LEAGUE.games[game_id].monitoroff(self)
363 players = LEAGUE.rated_players
364 players.sort {|a,b| b.rate <=> a.rate}.each do |p|
365 write_safe("%s (%s) \t %4d @%s\n" %
366 [p.name, p.id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
369 if ((@status == "connected") || (@status == "game_waiting"))
370 @status = "connected"
373 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
375 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
379 if (! good_game_name?(game_name))
380 write_safe(sprintf("##[ERROR] bad game name\n"))
382 elsif ((@status == "connected") || (@status == "game_waiting"))
385 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
388 if ((my_sente_str == "*") ||
389 (my_sente_str == "+") ||
390 (my_sente_str == "-"))
393 write_safe(sprintf("##[ERROR] bad game option\n"))
397 if (my_sente_str == "*")
398 rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
399 elsif (my_sente_str == "+")
400 rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
401 elsif (my_sente_str == "-")
402 rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
407 @game_name = game_name
408 if ((my_sente_str == "*") && (rival.sente == nil))
416 elsif (rival.sente == true) # rival has higher priority
418 elsif (rival.sente == false)
420 elsif (my_sente_str == "+")
423 elsif (my_sente_str == "-")
429 Game::new(@game_name, self, rival)
430 self.status = "agree_waiting"
431 rival.status = "agree_waiting"
432 else # rival not found
433 if (command_name == "GAME")
434 @status = "game_waiting"
435 @game_name = game_name
436 if (my_sente_str == "+")
438 elsif (my_sente_str == "-")
444 write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
445 @status = "connected"
450 when /^%%CHAT\s+(.+)/
452 LEAGUE.players.each do |name, player|
453 if (player.protocol != "CSA")
454 player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message))
459 LEAGUE.games.each do |id, game|
460 buf.push(sprintf("##[LIST] %s\n", id))
462 buf.push("##[LIST] +OK\n")
466 LEAGUE.players.each do |name, player|
467 buf.push(sprintf("##[WHO] %s\n", player.to_s))
469 buf.push("##[WHO] +OK\n")
472 @status = "connected"
473 write_safe("LOGOUT:completed\n")
476 ## ignore null string
478 write_safe(sprintf("##[ERROR] unknown command %s\n", str))
488 PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
489 def initialize(board, x, y, sente, promoted=false)
496 if ((x == 0) || (y == 0))
498 hands = board.sente_hands
500 hands = board.gote_hands
507 @board.array[x][y] = self
510 attr_accessor :promoted, :sente, :x, :y, :board
512 def room_of_head?(x, y, name)
517 return adjacent_movable_grids + far_movable_grids
520 def far_movable_grids
525 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
526 if ((@board.array[x][y] == nil) || # dst is empty
527 (@board.array[x][y].sente != @sente)) # dst is enemy
535 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
536 if (@board.array[x][y] == nil) # dst is empty?
543 def adjacent_movable_grids
546 moves = @promoted_moves
548 moves = @normal_moves
550 moves.each do |(dx, dy)|
557 if (jump_to?(cand_x, cand_y))
558 grids.push([cand_x, cand_y])
564 def move_to?(x, y, name)
565 return false if (! room_of_head?(x, y, name))
566 return false if ((name != @name) && (name != @promoted_name))
567 return false if (@promoted && (name != @promoted_name)) # can't un-promote
570 return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
572 return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
574 return false if ((6 >= @y) && (6 >= y) && (name != @name))
578 if ((@x == 0) || (@y == 0))
579 return jump_to?(x, y)
581 return movable_grids.include?([x, y])
586 if ((@x == 0) || (@y == 0))
588 @board.sente_hands.delete(self)
590 @board.gote_hands.delete(self)
592 @board.array[x][y] = self
593 elsif ((x == 0) || (y == 0))
594 @promoted = false # clear promoted flag before moving to hands
596 @board.sente_hands.push(self)
598 @board.gote_hands.push(self)
600 @board.array[@x][@y] = nil
602 @board.array[@x][@y] = nil
603 @board.array[x][y] = self
636 class PieceFU < Piece
639 @normal_moves = [[0, +1]]
640 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
642 @promoted_name = "TO"
645 def room_of_head?(x, y, name)
648 return false if (y == 1)
650 return false if (y == 9)
656 if ((iy != @y) && # not source position
657 @board.array[x][iy] &&
658 (@board.array[x][iy].sente == @sente) && # mine
659 (@board.array[x][iy].name == "FU") &&
660 (@board.array[x][iy].promoted == false))
670 class PieceKY < Piece
674 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
676 @promoted_name = "NY"
679 def room_of_head?(x, y, name)
682 return false if (y == 1)
684 return false if (y == 9)
689 def far_movable_grids
697 while (jump_to?(cand_x, cand_y))
698 grids.push([cand_x, cand_y])
699 break if (! put_to?(cand_x, cand_y))
705 while (jump_to?(cand_x, cand_y))
706 grids.push([cand_x, cand_y])
707 break if (! put_to?(cand_x, cand_y))
715 class PieceKE < Piece
718 @normal_moves = [[+1, +2], [-1, +2]]
719 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
721 @promoted_name = "NK"
724 def room_of_head?(x, y, name)
727 return false if ((y == 1) || (y == 2))
729 return false if ((y == 9) || (y == 8))
735 class PieceGI < Piece
738 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
739 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
741 @promoted_name = "NG"
745 class PieceKI < Piece
748 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
755 class PieceKA < Piece
759 @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
761 @promoted_name = "UM"
764 def far_movable_grids
769 while (jump_to?(cand_x, cand_y))
770 grids.push([cand_x, cand_y])
771 break if (! put_to?(cand_x, cand_y))
778 while (jump_to?(cand_x, cand_y))
779 grids.push([cand_x, cand_y])
780 break if (! put_to?(cand_x, cand_y))
787 while (jump_to?(cand_x, cand_y))
788 grids.push([cand_x, cand_y])
789 break if (! put_to?(cand_x, cand_y))
796 while (jump_to?(cand_x, cand_y))
797 grids.push([cand_x, cand_y])
798 break if (! put_to?(cand_x, cand_y))
805 class PieceHI < Piece
809 @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
811 @promoted_name = "RY"
814 def far_movable_grids
819 while (jump_to?(cand_x, cand_y))
820 grids.push([cand_x, cand_y])
821 break if (! put_to?(cand_x, cand_y))
827 while (jump_to?(cand_x, cand_y))
828 grids.push([cand_x, cand_y])
829 break if (! put_to?(cand_x, cand_y))
835 while (jump_to?(cand_x, cand_y))
836 grids.push([cand_x, cand_y])
837 break if (! put_to?(cand_x, cand_y))
843 while (jump_to?(cand_x, cand_y))
844 grids.push([cand_x, cand_y])
845 break if (! put_to?(cand_x, cand_y))
851 class PieceOU < Piece
854 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
864 @sente_hands = Array::new
865 @gote_hands = Array::new
867 @sente_history = Hash::new
868 @gote_history = Hash::new
869 @array = [[], [], [], [], [], [], [], [], [], []]
872 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
873 attr_reader :move_count
876 PieceKY::new(self, 1, 1, false)
877 PieceKE::new(self, 2, 1, false)
878 PieceGI::new(self, 3, 1, false)
879 PieceKI::new(self, 4, 1, false)
880 PieceOU::new(self, 5, 1, false)
881 PieceKI::new(self, 6, 1, false)
882 PieceGI::new(self, 7, 1, false)
883 PieceKE::new(self, 8, 1, false)
884 PieceKY::new(self, 9, 1, false)
885 PieceKA::new(self, 2, 2, false)
886 PieceHI::new(self, 8, 2, false)
887 PieceFU::new(self, 1, 3, false)
888 PieceFU::new(self, 2, 3, false)
889 PieceFU::new(self, 3, 3, false)
890 PieceFU::new(self, 4, 3, false)
891 PieceFU::new(self, 5, 3, false)
892 PieceFU::new(self, 6, 3, false)
893 PieceFU::new(self, 7, 3, false)
894 PieceFU::new(self, 8, 3, false)
895 PieceFU::new(self, 9, 3, false)
897 PieceKY::new(self, 1, 9, true)
898 PieceKE::new(self, 2, 9, true)
899 PieceGI::new(self, 3, 9, true)
900 PieceKI::new(self, 4, 9, true)
901 PieceOU::new(self, 5, 9, true)
902 PieceKI::new(self, 6, 9, true)
903 PieceGI::new(self, 7, 9, true)
904 PieceKE::new(self, 8, 9, true)
905 PieceKY::new(self, 9, 9, true)
906 PieceKA::new(self, 8, 8, true)
907 PieceHI::new(self, 2, 8, true)
908 PieceFU::new(self, 1, 7, true)
909 PieceFU::new(self, 2, 7, true)
910 PieceFU::new(self, 3, 7, true)
911 PieceFU::new(self, 4, 7, true)
912 PieceFU::new(self, 5, 7, true)
913 PieceFU::new(self, 6, 7, true)
914 PieceFU::new(self, 7, 7, true)
915 PieceFU::new(self, 8, 7, true)
916 PieceFU::new(self, 9, 7, true)
919 def have_piece?(hands, name)
920 piece = hands.find { |i|
926 def move_to(x0, y0, x1, y1, name, sente)
933 if ((x0 == 0) || (y0 == 0))
934 piece = have_piece?(hands, name)
935 return :illegal if (! piece.move_to?(x1, y1, name))
936 piece.move_to(x1, y1)
938 return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))
939 if (@array[x0][y0].name != name) # promoted ?
940 @array[x0][y0].promoted = true
943 if (@array[x1][y1].name == "OU")
944 return :outori # return board update
946 @array[x1][y1].sente = @array[x0][y0].sente
947 @array[x1][y1].move_to(0, 0)
952 @array[x0][y0].move_to(x1, y1)
958 def look_for_ou(sente)
964 (@array[x][y].name == "OU") &&
965 (@array[x][y].sente == sente))
972 raise "can't find ou"
975 def checkmated?(sente) # sente is loosing
976 ou = look_for_ou(sente)
982 (@array[x][y].sente != sente))
983 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
994 def uchifuzume?(sente)
995 rival_ou = look_for_ou(! sente) # rival's ou
996 if (sente) # rival is gote
997 if ((rival_ou.y != 9) &&
998 (@array[rival_ou.x][rival_ou.y + 1]) &&
999 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1000 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1002 fu_y = rival_ou.y + 1
1007 if ((rival_ou.y != 0) &&
1008 (@array[rival_ou.x][rival_ou.y - 1]) &&
1009 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1010 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1012 fu_y = rival_ou.y - 1
1018 ## case: rival_ou is moving
1020 rival_ou.movable_grids.each do |(cand_x, cand_y)|
1021 tmp_board = Marshal.load(Marshal.dump(self))
1022 s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1023 raise "internal error" if (s != true)
1024 if (! tmp_board.checkmated?(! sente)) # good move
1029 ## case: rival is capturing fu
1035 (@array[x][y].sente != sente) &&
1036 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1037 if (@array[x][y].promoted)
1038 name = @array[x][y].promoted_name
1040 name = @array[x][y].name
1042 tmp_board = Marshal.load(Marshal.dump(self))
1043 s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1044 raise "internal error" if (s != true)
1045 if (! tmp_board.checkmated?(! sente)) # good move
1056 def oute_sennichite?(sente)
1057 if (checkmated?(! sente))
1060 if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
1064 if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
1072 def sennichite?(sente)
1074 if (@history[str] && (@history[str] >= 3)) # already 3 times
1080 def good_kachi?(sente)
1081 if (checkmated?(sente))
1082 puts "'NG: Checkmating." if $DEBUG
1086 ou = look_for_ou(sente)
1087 if (sente && (ou.y >= 4))
1088 puts "'NG: Black's OU does not enter yet." if $DEBUG
1091 if (! sente && (ou.y <= 6))
1092 puts "'NG: White's OU does not enter yet." if $DEBUG
1100 hands = @sente_hands
1110 (@array[x][y].sente == sente) &&
1111 (@array[x][y].point > 0))
1112 point = point + @array[x][y].point
1118 hands.each do |piece|
1119 point = point + piece.point
1123 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1128 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1133 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1138 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1142 def handle_one_move(str, sente=nil)
1143 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1150 elsif (str =~ /^%KACHI/)
1151 raise ArgumentError, "sente is null", callser unless sente
1152 if (good_kachi?(sente))
1157 elsif (str =~ /^%TORYO/)
1163 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1164 ((x0 != 0) || (y0 != 0)))
1166 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1172 hands = @sente_hands
1179 if ((x0 == 0) && (y0 == 0))
1180 return :illegal if (! have_piece?(hands, name))
1181 elsif (! @array[x0][y0])
1182 return :illegal # no piece
1183 elsif (@array[x0][y0].sente != sente)
1184 return :illegal # this is not mine
1185 elsif (@array[x0][y0].name != name)
1186 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1189 ## destination check
1190 if (@array[x1][y1] &&
1191 (@array[x1][y1].sente == sente)) # can't capture mine
1193 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1194 return :illegal # can't put on existing piece
1197 tmp_board = Marshal.load(Marshal.dump(self))
1198 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1199 return :oute_kaihimore if (tmp_board.checkmated?(sente))
1200 return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1201 return :sennichite if tmp_board.sennichite?(sente)
1203 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1207 move_to(x0, y0, x1, y1, name, sente)
1210 if (checkmated?(! sente))
1212 @sente_history[str] = (@sente_history[str] || 0) + 1
1214 @gote_history[str] = (@gote_history[str] || 0) + 1
1218 @sente_history.clear
1223 @history[str] = (@history[str] || 0) + 1
1231 a.push(sprintf("P%d", y))
1234 piece = @array[x][y]
1243 a.push(sprintf("\n"))
1246 if (! sente_hands.empty?)
1248 sente_hands.each do |p|
1249 a.push("00" + p.name)
1253 if (! gote_hands.empty?)
1255 gote_hands.each do |p|
1256 a.push("00" + p.name)
1266 attr_reader :players, :black, :white
1268 def initialize(p1, p2)
1272 if p1.sente && !p2.sente
1273 @black, @white = p1, p2
1274 elsif !p1.sente && p2.sente
1275 @black, @white = p2, p1
1277 raise "Never reached!"
1282 class GameResultWin < GameResult
1283 attr_reader :winner, :loser
1285 def initialize(winner, loser)
1287 @winner, @loser = winner, loser
1291 black_name = @black.id || @black.name
1292 white_name = @white.id || @white.name
1293 "%s:%s" % [black_name, white_name]
1297 class GameResultDraw < GameResult
1305 def initialize(game_name, player0, player1)
1306 @monitors = Array::new
1307 @game_name = game_name
1308 if (@game_name =~ /-(\d+)-(\d+)$/)
1309 @total_time = $1.to_i
1320 @current_player = @sente
1321 @next_player = @gote
1329 @sente.status = "agree_waiting"
1330 @gote.status = "agree_waiting"
1332 @id = sprintf("%s+%s+%s+%s+%s",
1333 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1334 @logfile = @id + ".csa"
1336 LEAGUE.games[@id] = self
1338 log_message(sprintf("game created %s", @id))
1348 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1349 attr_accessor :last_move, :current_turn
1353 @sente.rated? && @gote.rated?
1356 def monitoron(monitor)
1357 @monitors.delete(monitor)
1358 @monitors.push(monitor)
1361 def monitoroff(monitor)
1362 @monitors.delete(monitor)
1365 def reject(rejector)
1366 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1367 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1372 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1374 elsif (@current_player == killer)
1381 log_message(sprintf("game finished %s", @id))
1382 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1387 @sente.status = "connected"
1388 @gote.status = "connected"
1390 if (@current_player.protocol == "CSA")
1391 @current_player.finish
1393 if (@next_player.protocol == "CSA")
1396 @monitors = Array::new
1399 @current_player = nil
1401 LEAGUE.games.delete(@id)
1404 def handle_one_move(str, player)
1406 if (@current_player == player)
1407 @end_time = Time::new
1408 t = (@end_time - @start_time).floor
1409 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1412 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1414 elsif (str == :timeout)
1415 return false # time isn't expired. players aren't swapped. continue game
1417 @current_player.mytime = @current_player.mytime - t
1418 if (@current_player.mytime < 0)
1419 @current_player.mytime = 0
1423 move_status = @board.handle_one_move(str, @sente == @current_player)
1425 # log_error("handle_one_move raise exception for #{str}")
1426 # move_status = :illegal
1429 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1430 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1432 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1433 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1434 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1435 @fh.printf("%s\nT%d\n", str, t)
1436 @last_move = sprintf("%s,T%d", str, t)
1437 @current_turn = @current_turn + 1
1440 @monitors.each do |monitor|
1441 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1442 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1447 if (@next_player.status != "game") # rival is logout or disconnected
1449 elsif (status == :timeout)
1451 elsif (move_status == :illegal)
1453 elsif (move_status == :kachi_win)
1455 elsif (move_status == :kachi_lose)
1457 elsif (move_status == :toryo)
1459 elsif (move_status == :outori)
1461 elsif (move_status == :sennichite)
1463 elsif (move_status == :oute_sennichite)
1464 oute_sennichite_lose()
1465 elsif (move_status == :uchifuzume)
1467 elsif (move_status == :oute_kaihimore)
1468 oute_kaihimore_lose()
1472 finish() if finish_flag
1473 (@current_player, @next_player) = [@next_player, @current_player]
1474 @start_time = Time::new
1480 @current_player.status = "connected"
1481 @next_player.status = "connected"
1482 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1483 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1484 @fh.printf("%%TORYO\n")
1485 @fh.print(@board.to_s.gsub(/^/, "\'"))
1486 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1487 @result = GameResultWin.new(@current_player, @next_player)
1488 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1489 @monitors.each do |monitor|
1490 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1495 @current_player.status = "connected"
1496 @next_player.status = "connected"
1497 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1498 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1499 @fh.printf("%%TORYO\n")
1500 @fh.print(@board.to_s.gsub(/^/, "\'"))
1501 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1502 @result = GameResultWin.new(@next_player, @current_player)
1503 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1504 @monitors.each do |monitor|
1505 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1510 @current_player.status = "connected"
1511 @next_player.status = "connected"
1512 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1513 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1514 @fh.print(@board.to_s.gsub(/^/, "\'"))
1515 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1516 @result = GameResultDraw.new(@current_player, @next_player)
1517 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1518 @monitors.each do |monitor|
1519 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1523 def oute_sennichite_lose
1524 @current_player.status = "connected"
1525 @next_player.status = "connected"
1526 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1527 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1528 @fh.print(@board.to_s.gsub(/^/, "\'"))
1529 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1530 @result = GameResultWin.new(@next_player, @current_player)
1531 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1532 @monitors.each do |monitor|
1533 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1538 @current_player.status = "connected"
1539 @next_player.status = "connected"
1540 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1541 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1542 @fh.print(@board.to_s.gsub(/^/, "\'"))
1543 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1544 @result = GameResultWin.new(@next_player, @current_player)
1545 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1546 @monitors.each do |monitor|
1547 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1552 @current_player.status = "connected"
1553 @next_player.status = "connected"
1554 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1555 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1556 @fh.print(@board.to_s.gsub(/^/, "\'"))
1557 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1558 @result = GameResultWin.new(@next_player, @current_player)
1559 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1560 @monitors.each do |monitor|
1561 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1565 def oute_kaihimore_lose
1566 @current_player.status = "connected"
1567 @next_player.status = "connected"
1568 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1569 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1570 @fh.print(@board.to_s.gsub(/^/, "\'"))
1571 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1572 @result = GameResultWin.new(@next_player, @current_player)
1573 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1574 @monitors.each do |monitor|
1575 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1580 @current_player.status = "connected"
1581 @next_player.status = "connected"
1582 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1583 @next_player.write_safe("#TIME_UP\n#WIN\n")
1584 @fh.print(@board.to_s.gsub(/^/, "\'"))
1585 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1586 @result = GameResultWin.new(@next_player, @current_player)
1587 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1588 @monitors.each do |monitor|
1589 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1594 @current_player.status = "connected"
1595 @next_player.status = "connected"
1596 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1597 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1598 @fh.printf("%%KACHI\n")
1599 @fh.print(@board.to_s.gsub(/^/, "\'"))
1600 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1601 @result = GameResultWin.new(@current_player, @next_player)
1602 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1603 @monitors.each do |monitor|
1604 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1609 @current_player.status = "connected"
1610 @next_player.status = "connected"
1611 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1612 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1613 @fh.printf("%%KACHI\n")
1614 @fh.print(@board.to_s.gsub(/^/, "\'"))
1615 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1616 @result = GameResultWin.new(@next_player, @current_player)
1617 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1618 @monitors.each do |monitor|
1619 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1624 @current_player.status = "connected"
1625 @next_player.status = "connected"
1626 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1627 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1628 @fh.printf("%%TORYO\n")
1629 @fh.print(@board.to_s.gsub(/^/, "\'"))
1630 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1631 @result = GameResultWin.new(@next_player, @current_player)
1632 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1633 @monitors.each do |monitor|
1634 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1639 @current_player.status = "connected"
1640 @next_player.status = "connected"
1641 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1642 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1643 @fh.print(@board.to_s.gsub(/^/, "\'"))
1644 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1645 @result = GameResultWin.new(@current_player, @next_player)
1646 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1647 @monitors.each do |monitor|
1648 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1653 log_message(sprintf("game started %s", @id))
1654 @sente.write_safe(sprintf("START:%s\n", @id))
1655 @gote.write_safe(sprintf("START:%s\n", @id))
1656 @sente.mytime = @total_time
1657 @gote.mytime = @total_time
1658 @start_time = Time::new
1663 @fh = open(@logfile, "w")
1667 @fh.printf("N+%s\n", @sente.name)
1668 @fh.printf("N-%s\n", @gote.name)
1669 @fh.printf("$EVENT:%s\n", @id)
1671 @sente.write_safe(propose_message("+"))
1672 @gote.write_safe(propose_message("-"))
1674 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1676 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1677 P2 * -HI * * * * * -KA *
1678 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1679 P4 * * * * * * * * *
1680 P5 * * * * * * * * *
1681 P6 * * * * * * * * *
1682 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1683 P8 * +KA * * * * * +HI *
1684 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1693 Protocol_Version:1.1
1694 Protocol_Mode:Server
1696 Declaration:Jishogi 1.1
1698 Name+:#{@sente.name}
1704 Total_Time:#{@total_time}
1706 Least_Time_Per_Move:#{Least_Time_Per_Move}
1707 Remaining_Time+:#{@sente.mytime}
1708 Remaining_Time-:#{@gote.mytime}
1709 Last_Move:#{@last_move}
1710 Current_Turn:#{@current_turn}
1720 return str0 + @board.to_s + str1
1723 def propose_message(sg_flag)
1726 Protocol_Version:1.1
1727 Protocol_Mode:Server
1729 Declaration:Jishogi 1.1
1731 Name+:#{@sente.name}
1733 Your_Turn:#{sg_flag}
1738 Total_Time:#{@total_time}
1740 Least_Time_Per_Move:#{Least_Time_Per_Move}
1743 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1744 P2 * -HI * * * * * -KA *
1745 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1746 P4 * * * * * * * * *
1747 P5 * * * * * * * * *
1748 P6 * * * * * * * * *
1749 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1750 P8 * +KA * * * * * +HI *
1751 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1763 def issue_current_time
1764 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1765 @@mutex.synchronize do
1766 while time <= @@time do
1774 #################################################
1781 shogi-server - server for CSA server protocol
1784 shogi-server event_name port_number
1787 server for CSA server protocol
1791 specify filename for logging process ID
1794 this file is distributed under GPL version2 and might be compiled by Exerb
1806 def log_message(str)
1807 printf("%s message: %s\n", Time::new.to_s, str)
1810 def log_warning(str)
1811 printf("%s warning: %s\n", Time::new.to_s, str)
1815 printf("%s error: %s\n", Time::new.to_s, str)
1819 def parse_command_line
1821 parser = GetoptLong.new
1822 parser.ordering = GetoptLong::REQUIRE_ORDER
1824 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1828 parser.each_option do |name, arg|
1829 name.sub!(/^--/, '')
1830 options[name] = arg.dup
1834 raise parser.error_message
1839 def good_game_name?(str)
1840 if ((str =~ /^(.+)-\d+-\d+$/) &&
1841 (good_identifier?($1)))
1848 # TODO This is also checked by good_game_name?().
1849 def good_identifier?(str)
1850 if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1852 elsif str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}},[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1859 def good_login?(str)
1861 if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1862 (tokens[0] == "LOGIN") &&
1863 (good_identifier?(tokens[1])))
1870 def write_pid_file(file)
1871 open(file, "w") do |fh|
1872 fh.print Process::pid, "\n"
1876 def mutex_watchdog(mutex, sec)
1888 log_error("mutex watchdog timeout")
1898 mutex_watchdog($mutex, 10)
1901 $options = parse_command_line
1902 if (ARGV.length != 2)
1907 LEAGUE.event = ARGV.shift
1910 write_pid_file($options["pid-file"]) if ($options["pid-file"])
1912 server = TCPserver.open(port)
1913 log_message("server started")
1916 Thread::start(server.accept) do |client|
1920 while (str = client.gets_timeout(Login_Time))
1925 if (good_login?(str))
1926 player = Player::new(str, client)
1927 if (LEAGUE.players[player.name])
1928 if ((LEAGUE.players[player.name].password == player.password) &&
1929 (LEAGUE.players[player.name].status != "game"))
1930 log_message(sprintf("user %s login forcely", player.name))
1931 LEAGUE.players[player.name].kill
1933 client.write_safe("LOGIN:incorrect" + eol)
1934 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1943 client.write_safe("LOGIN:incorrect" + eol)
1944 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1955 log_message(sprintf("user %s login", player.name))
1960 player.game.kill(player)
1962 player.finish # socket has been closed
1963 LEAGUE.delete(player)
1964 log_message(sprintf("user %s logout", player.name))
1973 LEAGUE = League::new