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)
1143 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1150 elsif (str =~ /^%KACHI/)
1151 if (good_kachi?(sente))
1156 elsif (str =~ /^%TORYO/)
1162 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1163 ((x0 != 0) || (y0 != 0)))
1165 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1171 hands = @sente_hands
1178 if ((x0 == 0) && (y0 == 0))
1179 return :illegal if (! have_piece?(hands, name))
1180 elsif (! @array[x0][y0])
1181 return :illegal # no piece
1182 elsif (@array[x0][y0].sente != sente)
1183 return :illegal # this is not mine
1184 elsif (@array[x0][y0].name != name)
1185 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1188 ## destination check
1189 if (@array[x1][y1] &&
1190 (@array[x1][y1].sente == sente)) # can't capture mine
1192 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1193 return :illegal # can't put on existing piece
1196 tmp_board = Marshal.load(Marshal.dump(self))
1197 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1198 return :oute_kaihimore if (tmp_board.checkmated?(sente))
1199 return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1200 return :sennichite if tmp_board.sennichite?(sente)
1202 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1206 move_to(x0, y0, x1, y1, name, sente)
1209 if (checkmated?(! sente))
1211 @sente_history[str] = (@sente_history[str] || 0) + 1
1213 @gote_history[str] = (@gote_history[str] || 0) + 1
1217 @sente_history.clear
1222 @history[str] = (@history[str] || 0) + 1
1230 a.push(sprintf("P%d", y))
1233 piece = @array[x][y]
1242 a.push(sprintf("\n"))
1245 if (! sente_hands.empty?)
1247 sente_hands.each do |p|
1248 a.push("00" + p.name)
1252 if (! gote_hands.empty?)
1254 gote_hands.each do |p|
1255 a.push("00" + p.name)
1265 attr_reader :players, :black, :white
1267 def initialize(p1, p2)
1271 if p1.sente && !p2.sente
1272 @black, @white = p1, p2
1273 elsif !p1.sente && p2.sente
1274 @black, @white = p2, p1
1276 raise "Never reached!"
1281 class GameResultWin < GameResult
1284 attr_reader :winner, :loser
1286 def initialize(winner, loser)
1288 @winner, @loser = winner, loser
1292 if @black == @winner
1293 black_mark, white_mark = WIN_MARK, LOSS_MARK
1294 elsif @white == @winner
1295 black_mark, white_mark = LOSS_MARK, WIN_MARK
1297 raise "Never reached! Nobody is the winner."
1299 black_name = @black.id || @black.name
1300 white_name = @white.id || @white.name
1301 "%s : %s %d : %s %d : %s" %
1302 [black_mark, black_name, @black.rate,
1303 white_name, @white.rate, white_mark]
1307 class GameResultDraw < GameResult
1315 def initialize(game_name, player0, player1)
1316 @monitors = Array::new
1317 @game_name = game_name
1318 if (@game_name =~ /-(\d+)-(\d+)$/)
1319 @total_time = $1.to_i
1330 @current_player = @sente
1331 @next_player = @gote
1339 @sente.status = "agree_waiting"
1340 @gote.status = "agree_waiting"
1342 @id = sprintf("%s+%s+%s+%s+%s",
1343 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1344 @logfile = @id + ".csa"
1346 LEAGUE.games[@id] = self
1348 log_message(sprintf("game created %s", @id))
1358 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1359 attr_accessor :last_move, :current_turn
1363 @sente.rated? && @gote.rated?
1366 def monitoron(monitor)
1367 @monitors.delete(monitor)
1368 @monitors.push(monitor)
1371 def monitoroff(monitor)
1372 @monitors.delete(monitor)
1375 def reject(rejector)
1376 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1377 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1382 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1384 elsif (@current_player == killer)
1391 log_message(sprintf("game finished %s", @id))
1392 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1397 @sente.status = "connected"
1398 @gote.status = "connected"
1400 if (@current_player.protocol == "CSA")
1401 @current_player.finish
1403 if (@next_player.protocol == "CSA")
1406 @monitors = Array::new
1409 @current_player = nil
1411 LEAGUE.games.delete(@id)
1414 def handle_one_move(str, player)
1416 if (@current_player == player)
1417 @end_time = Time::new
1418 t = (@end_time - @start_time).floor
1419 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1422 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1424 elsif (str == :timeout)
1425 return false # time isn't expired. players aren't swapped. continue game
1427 @current_player.mytime = @current_player.mytime - t
1428 if (@current_player.mytime < 0)
1429 @current_player.mytime = 0
1433 move_status = @board.handle_one_move(str, @sente == @current_player)
1435 # log_error("handle_one_move raise exception for #{str}")
1436 # move_status = :illegal
1439 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1440 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1442 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1443 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1444 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1445 @fh.printf("%s\nT%d\n", str, t)
1446 @last_move = sprintf("%s,T%d", str, t)
1447 @current_turn = @current_turn + 1
1450 @monitors.each do |monitor|
1451 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1452 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1457 if (@next_player.status != "game") # rival is logout or disconnected
1459 elsif (status == :timeout)
1461 elsif (move_status == :illegal)
1463 elsif (move_status == :kachi_win)
1465 elsif (move_status == :kachi_lose)
1467 elsif (move_status == :toryo)
1469 elsif (move_status == :outori)
1471 elsif (move_status == :sennichite)
1473 elsif (move_status == :oute_sennichite)
1474 oute_sennichite_lose()
1475 elsif (move_status == :uchifuzume)
1477 elsif (move_status == :oute_kaihimore)
1478 oute_kaihimore_lose()
1482 finish() if finish_flag
1483 (@current_player, @next_player) = [@next_player, @current_player]
1484 @start_time = Time::new
1490 @current_player.status = "connected"
1491 @next_player.status = "connected"
1492 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1493 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1494 @fh.printf("%%TORYO\n")
1495 @fh.print(@board.to_s.gsub(/^/, "\'"))
1496 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1497 @result = GameResultWin.new(@current_player, @next_player)
1498 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1499 @monitors.each do |monitor|
1500 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1505 @current_player.status = "connected"
1506 @next_player.status = "connected"
1507 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1508 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1509 @fh.printf("%%TORYO\n")
1510 @fh.print(@board.to_s.gsub(/^/, "\'"))
1511 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1512 @result = GameResultWin.new(@next_player, @current_player)
1513 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1514 @monitors.each do |monitor|
1515 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1520 @current_player.status = "connected"
1521 @next_player.status = "connected"
1522 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1523 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1524 @fh.print(@board.to_s.gsub(/^/, "\'"))
1525 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1526 @result = GameResultDraw.new(@current_player, @next_player)
1527 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1528 @monitors.each do |monitor|
1529 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1533 def oute_sennichite_lose
1534 @current_player.status = "connected"
1535 @next_player.status = "connected"
1536 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1537 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1538 @fh.print(@board.to_s.gsub(/^/, "\'"))
1539 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1540 @result = GameResultWin.new(@next_player, @current_player)
1541 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1542 @monitors.each do |monitor|
1543 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1548 @current_player.status = "connected"
1549 @next_player.status = "connected"
1550 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1551 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1552 @fh.print(@board.to_s.gsub(/^/, "\'"))
1553 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1554 @result = GameResultWin.new(@next_player, @current_player)
1555 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1556 @monitors.each do |monitor|
1557 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1562 @current_player.status = "connected"
1563 @next_player.status = "connected"
1564 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1565 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1566 @fh.print(@board.to_s.gsub(/^/, "\'"))
1567 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1568 @result = GameResultWin.new(@next_player, @current_player)
1569 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1570 @monitors.each do |monitor|
1571 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1575 def oute_kaihimore_lose
1576 @current_player.status = "connected"
1577 @next_player.status = "connected"
1578 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1579 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1580 @fh.print(@board.to_s.gsub(/^/, "\'"))
1581 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1582 @result = GameResultWin.new(@next_player, @current_player)
1583 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1584 @monitors.each do |monitor|
1585 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1590 @current_player.status = "connected"
1591 @next_player.status = "connected"
1592 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1593 @next_player.write_safe("#TIME_UP\n#WIN\n")
1594 @fh.print(@board.to_s.gsub(/^/, "\'"))
1595 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1596 @result = GameResultWin.new(@next_player, @current_player)
1597 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1598 @monitors.each do |monitor|
1599 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1604 @current_player.status = "connected"
1605 @next_player.status = "connected"
1606 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1607 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1608 @fh.printf("%%KACHI\n")
1609 @fh.print(@board.to_s.gsub(/^/, "\'"))
1610 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1611 @result = GameResultWin.new(@current_player, @next_player)
1612 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1613 @monitors.each do |monitor|
1614 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1619 @current_player.status = "connected"
1620 @next_player.status = "connected"
1621 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1622 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1623 @fh.printf("%%KACHI\n")
1624 @fh.print(@board.to_s.gsub(/^/, "\'"))
1625 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1626 @result = GameResultWin.new(@next_player, @current_player)
1627 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1628 @monitors.each do |monitor|
1629 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1634 @current_player.status = "connected"
1635 @next_player.status = "connected"
1636 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1637 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1638 @fh.printf("%%TORYO\n")
1639 @fh.print(@board.to_s.gsub(/^/, "\'"))
1640 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1641 @result = GameResultWin.new(@next_player, @current_player)
1642 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1643 @monitors.each do |monitor|
1644 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1649 @current_player.status = "connected"
1650 @next_player.status = "connected"
1651 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1652 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1653 @fh.print(@board.to_s.gsub(/^/, "\'"))
1654 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1655 @result = GameResultWin.new(@current_player, @next_player)
1656 @fh.printf("'rating: #{@result.to_s}\n") if rated?
1657 @monitors.each do |monitor|
1658 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1663 log_message(sprintf("game started %s", @id))
1664 @sente.write_safe(sprintf("START:%s\n", @id))
1665 @gote.write_safe(sprintf("START:%s\n", @id))
1666 @sente.mytime = @total_time
1667 @gote.mytime = @total_time
1668 @start_time = Time::new
1673 @fh = open(@logfile, "w")
1677 @fh.printf("N+%s\n", @sente.name)
1678 @fh.printf("N-%s\n", @gote.name)
1679 @fh.printf("$EVENT:%s\n", @id)
1681 @sente.write_safe(propose_message("+"))
1682 @gote.write_safe(propose_message("-"))
1684 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1686 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1687 P2 * -HI * * * * * -KA *
1688 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1689 P4 * * * * * * * * *
1690 P5 * * * * * * * * *
1691 P6 * * * * * * * * *
1692 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1693 P8 * +KA * * * * * +HI *
1694 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1703 Protocol_Version:1.1
1704 Protocol_Mode:Server
1706 Declaration:Jishogi 1.1
1708 Name+:#{@sente.name}
1714 Total_Time:#{@total_time}
1716 Least_Time_Per_Move:#{Least_Time_Per_Move}
1717 Remaining_Time+:#{@sente.mytime}
1718 Remaining_Time-:#{@gote.mytime}
1719 Last_Move:#{@last_move}
1720 Current_Turn:#{@current_turn}
1730 return str0 + @board.to_s + str1
1733 def propose_message(sg_flag)
1736 Protocol_Version:1.1
1737 Protocol_Mode:Server
1739 Declaration:Jishogi 1.1
1741 Name+:#{@sente.name}
1743 Your_Turn:#{sg_flag}
1748 Total_Time:#{@total_time}
1750 Least_Time_Per_Move:#{Least_Time_Per_Move}
1753 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1754 P2 * -HI * * * * * -KA *
1755 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1756 P4 * * * * * * * * *
1757 P5 * * * * * * * * *
1758 P6 * * * * * * * * *
1759 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1760 P8 * +KA * * * * * +HI *
1761 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1773 def issue_current_time
1774 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1775 @@mutex.synchronize do
1776 while time <= @@time do
1784 #################################################
1791 shogi-server - server for CSA server protocol
1794 shogi-server event_name port_number
1797 server for CSA server protocol
1801 specify filename for logging process ID
1804 this file is distributed under GPL version2 and might be compiled by Exerb
1816 def log_message(str)
1817 printf("%s message: %s\n", Time::new.to_s, str)
1820 def log_warning(str)
1821 printf("%s warning: %s\n", Time::new.to_s, str)
1825 printf("%s error: %s\n", Time::new.to_s, str)
1829 def parse_command_line
1831 parser = GetoptLong.new
1832 parser.ordering = GetoptLong::REQUIRE_ORDER
1834 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1838 parser.each_option do |name, arg|
1839 name.sub!(/^--/, '')
1840 options[name] = arg.dup
1844 raise parser.error_message
1849 def good_game_name?(str)
1850 if ((str =~ /^(.+)-\d+-\d+$/) &&
1851 (good_identifier?($1)))
1858 def good_identifier?(str)
1859 if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1861 elsif str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}},[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1868 def good_login?(str)
1870 if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1871 (tokens[0] == "LOGIN") &&
1872 (good_identifier?(tokens[1])))
1879 def write_pid_file(file)
1880 open(file, "w") do |fh|
1881 fh.print Process::pid, "\n"
1885 def mutex_watchdog(mutex, sec)
1897 log_error("mutex watchdog timeout")
1907 mutex_watchdog($mutex, 10)
1910 $options = parse_command_line
1911 if (ARGV.length != 2)
1916 LEAGUE.event = ARGV.shift
1919 write_pid_file($options["pid-file"]) if ($options["pid-file"])
1921 server = TCPserver.open(port)
1922 log_message("server started")
1925 Thread::start(server.accept) do |client|
1929 while (str = client.gets_timeout(Login_Time))
1934 if (good_login?(str))
1935 player = Player::new(str, client)
1936 if (LEAGUE.players[player.name])
1937 if ((LEAGUE.players[player.name].password == player.password) &&
1938 (LEAGUE.players[player.name].status != "game"))
1939 log_message(sprintf("user %s login forcely", player.name))
1940 LEAGUE.players[player.name].kill
1942 client.write_safe("LOGIN:incorrect" + eol)
1943 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1952 client.write_safe("LOGIN:incorrect" + eol)
1953 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1964 log_message(sprintf("user %s login", player.name))
1969 player.game.kill(player)
1971 player.finish # socket has been closed
1972 LEAGUE.delete(player)
1973 log_message(sprintf("user %s logout", player.name))
1982 LEAGUE = League::new