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['last_modified']
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
162 # Password of the player, which does not include a trip
163 attr_accessor :password
165 # Score in the rating sysem
168 # Last timestamp when the rate was modified
169 attr_accessor :modified_at
179 @modified_at || Time.now
185 @modified_at = Time.now
195 simple_name = @name.gsub(/@.*?$/, '')
196 "%s+%s" % [simple_name, @trip[0..8]]
203 # Parses str in the LOGIN command, sets up @id and @trip
205 def set_password(str)
206 if str && !str.empty?
207 @password = str.strip
208 @id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
210 @id = @password = nil
215 class Player < BasicPlayer
216 def initialize(str, socket)
219 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
221 @protocol = nil # CSA or x1
222 @eol = "\m" # favorite eol code
225 @mytime = 0 # set in start method also
229 @write_queue = Queue::new
233 attr_accessor :socket, :status
234 attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
235 attr_accessor :main_thread, :writer_thread, :write_queue
238 log_message(sprintf("user %s killed", @name))
243 Thread::kill(@main_thread) if @main_thread
247 if (@status != "finished")
249 log_message(sprintf("user %s finish", @name))
250 Thread::kill(@writer_thread) if @writer_thread
252 @socket.close if (! @socket.closed?)
254 log_message(sprintf("user %s finish failed", @name))
260 @write_queue.push(str.gsub(/[\r\n]+/, @eol))
264 while (str = @write_queue.pop)
265 @socket.write_safe(str)
270 if ((status == "game_waiting") ||
271 (status == "start_waiting") ||
272 (status == "agree_waiting") ||
275 return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
276 elsif (@sente == false)
277 return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
278 elsif (@sente == nil)
279 return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
282 return sprintf("%s %s %s", @name, @protocol, @status)
287 @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
294 (login, @name, password, ext) = str.split
295 set_password(password)
301 @main_thread = Thread::current
302 @writer_thread = Thread::start do
308 write_safe(sprintf("LOGIN:%s OK\n", @name))
309 if (@protocol != "CSA")
310 log_message(sprintf("user %s run in %s mode", @name, @protocol))
311 write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
313 log_message(sprintf("user %s run in CSA mode", @name))
314 if (good_game_name?(@password))
315 csa_1st_str = "%%GAME #{@password} *"
317 csa_1st_str = "%%GAME #{Default_Game_Name} *"
321 while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
328 if (@write_queue.size > Max_Write_Queue_Size)
329 log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
333 if (@status == "finished")
336 str.chomp! if (str.class == String)
339 if (@status == "game")
340 array_str = str.split(",")
341 move = array_str.shift
342 additional = array_str.shift
343 if /^'(.*)/ =~ additional
344 comment = array_str.unshift("'*#{$1}")
346 s = @game.handle_one_move(move, self)
347 @game.fh.print("#{comment}\n") if (comment && !s)
348 return if (s && @protocol == "CSA")
350 when /^%[^%]/, :timeout
351 if (@status == "game")
352 s = @game.handle_one_move(str, self)
353 return if (s && @protocol == "CSA")
356 if (@status == "agree_waiting")
358 return if (@protocol == "CSA")
360 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
363 if (@status == "agree_waiting")
364 @status = "start_waiting"
365 if ((@game.sente.status == "start_waiting") &&
366 (@game.gote.status == "start_waiting"))
368 @game.sente.status = "game"
369 @game.gote.status = "game"
372 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
374 when /^%%SHOW\s+(\S+)/
376 if (LEAGUE.games[game_id])
377 write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
379 write_safe("##[SHOW] +OK\n")
380 when /^%%MONITORON\s+(\S+)/
382 if (LEAGUE.games[game_id])
383 LEAGUE.games[game_id].monitoron(self)
384 write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
385 write_safe("##[MONITOR][#{game_id}] +OK\n")
387 when /^%%MONITOROFF\s+(\S+)/
389 if (LEAGUE.games[game_id])
390 LEAGUE.games[game_id].monitoroff(self)
395 players = LEAGUE.rated_players
396 players.sort {|a,b| b.rate <=> a.rate}.each do |p|
397 write_safe("##[RATING] %s \t %4d @%s\n" %
398 [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
400 write_safe("##[RATING] +OK\n")
402 write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
403 write_safe("##[VERSION] +OK\n")
405 if ((@status == "connected") || (@status == "game_waiting"))
406 @status = "connected"
409 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
411 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
415 if (! good_game_name?(game_name))
416 write_safe(sprintf("##[ERROR] bad game name\n"))
418 elsif ((@status == "connected") || (@status == "game_waiting"))
421 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
424 if ((my_sente_str == "*") ||
425 (my_sente_str == "+") ||
426 (my_sente_str == "-"))
429 write_safe(sprintf("##[ERROR] bad game option\n"))
433 if (my_sente_str == "*")
434 rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
435 elsif (my_sente_str == "+")
436 rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
437 elsif (my_sente_str == "-")
438 rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
443 @game_name = game_name
444 if ((my_sente_str == "*") && (rival.sente == nil))
452 elsif (rival.sente == true) # rival has higher priority
454 elsif (rival.sente == false)
456 elsif (my_sente_str == "+")
459 elsif (my_sente_str == "-")
465 Game::new(@game_name, self, rival)
466 self.status = "agree_waiting"
467 rival.status = "agree_waiting"
468 else # rival not found
469 if (command_name == "GAME")
470 @status = "game_waiting"
471 @game_name = game_name
472 if (my_sente_str == "+")
474 elsif (my_sente_str == "-")
480 write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
481 @status = "connected"
486 when /^%%CHAT\s+(.+)/
488 LEAGUE.players.each do |name, player|
489 if (player.protocol != "CSA")
490 player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message))
495 LEAGUE.games.each do |id, game|
496 buf.push(sprintf("##[LIST] %s\n", id))
498 buf.push("##[LIST] +OK\n")
502 LEAGUE.players.each do |name, player|
503 buf.push(sprintf("##[WHO] %s\n", player.to_s))
505 buf.push("##[WHO] +OK\n")
508 @status = "connected"
509 write_safe("LOGOUT:completed\n")
512 ## ignore null string
514 write_safe(sprintf("##[ERROR] unknown command %s\n", str))
524 PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
525 def initialize(board, x, y, sente, promoted=false)
532 if ((x == 0) || (y == 0))
534 hands = board.sente_hands
536 hands = board.gote_hands
543 @board.array[x][y] = self
546 attr_accessor :promoted, :sente, :x, :y, :board
548 def room_of_head?(x, y, name)
553 return adjacent_movable_grids + far_movable_grids
556 def far_movable_grids
561 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
562 if ((@board.array[x][y] == nil) || # dst is empty
563 (@board.array[x][y].sente != @sente)) # dst is enemy
571 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
572 if (@board.array[x][y] == nil) # dst is empty?
579 def adjacent_movable_grids
582 moves = @promoted_moves
584 moves = @normal_moves
586 moves.each do |(dx, dy)|
593 if (jump_to?(cand_x, cand_y))
594 grids.push([cand_x, cand_y])
600 def move_to?(x, y, name)
601 return false if (! room_of_head?(x, y, name))
602 return false if ((name != @name) && (name != @promoted_name))
603 return false if (@promoted && (name != @promoted_name)) # can't un-promote
606 return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
608 return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
610 return false if ((6 >= @y) && (6 >= y) && (name != @name))
614 if ((@x == 0) || (@y == 0))
615 return jump_to?(x, y)
617 return movable_grids.include?([x, y])
622 if ((@x == 0) || (@y == 0))
624 @board.sente_hands.delete(self)
626 @board.gote_hands.delete(self)
628 @board.array[x][y] = self
629 elsif ((x == 0) || (y == 0))
630 @promoted = false # clear promoted flag before moving to hands
632 @board.sente_hands.push(self)
634 @board.gote_hands.push(self)
636 @board.array[@x][@y] = nil
638 @board.array[@x][@y] = nil
639 @board.array[x][y] = self
672 class PieceFU < Piece
675 @normal_moves = [[0, +1]]
676 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
678 @promoted_name = "TO"
681 def room_of_head?(x, y, name)
684 return false if (y == 1)
686 return false if (y == 9)
692 if ((iy != @y) && # not source position
693 @board.array[x][iy] &&
694 (@board.array[x][iy].sente == @sente) && # mine
695 (@board.array[x][iy].name == "FU") &&
696 (@board.array[x][iy].promoted == false))
706 class PieceKY < Piece
710 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
712 @promoted_name = "NY"
715 def room_of_head?(x, y, name)
718 return false if (y == 1)
720 return false if (y == 9)
725 def far_movable_grids
733 while (jump_to?(cand_x, cand_y))
734 grids.push([cand_x, cand_y])
735 break if (! put_to?(cand_x, cand_y))
741 while (jump_to?(cand_x, cand_y))
742 grids.push([cand_x, cand_y])
743 break if (! put_to?(cand_x, cand_y))
751 class PieceKE < Piece
754 @normal_moves = [[+1, +2], [-1, +2]]
755 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
757 @promoted_name = "NK"
760 def room_of_head?(x, y, name)
763 return false if ((y == 1) || (y == 2))
765 return false if ((y == 9) || (y == 8))
771 class PieceGI < Piece
774 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
775 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
777 @promoted_name = "NG"
781 class PieceKI < Piece
784 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
791 class PieceKA < Piece
795 @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
797 @promoted_name = "UM"
800 def far_movable_grids
805 while (jump_to?(cand_x, cand_y))
806 grids.push([cand_x, cand_y])
807 break if (! put_to?(cand_x, cand_y))
814 while (jump_to?(cand_x, cand_y))
815 grids.push([cand_x, cand_y])
816 break if (! put_to?(cand_x, cand_y))
823 while (jump_to?(cand_x, cand_y))
824 grids.push([cand_x, cand_y])
825 break if (! put_to?(cand_x, cand_y))
832 while (jump_to?(cand_x, cand_y))
833 grids.push([cand_x, cand_y])
834 break if (! put_to?(cand_x, cand_y))
841 class PieceHI < Piece
845 @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
847 @promoted_name = "RY"
850 def far_movable_grids
855 while (jump_to?(cand_x, cand_y))
856 grids.push([cand_x, cand_y])
857 break if (! put_to?(cand_x, cand_y))
863 while (jump_to?(cand_x, cand_y))
864 grids.push([cand_x, cand_y])
865 break if (! put_to?(cand_x, cand_y))
871 while (jump_to?(cand_x, cand_y))
872 grids.push([cand_x, cand_y])
873 break if (! put_to?(cand_x, cand_y))
879 while (jump_to?(cand_x, cand_y))
880 grids.push([cand_x, cand_y])
881 break if (! put_to?(cand_x, cand_y))
887 class PieceOU < Piece
890 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
900 @sente_hands = Array::new
901 @gote_hands = Array::new
903 @sente_history = Hash::new
904 @gote_history = Hash::new
905 @array = [[], [], [], [], [], [], [], [], [], []]
908 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
909 attr_reader :move_count
912 PieceKY::new(self, 1, 1, false)
913 PieceKE::new(self, 2, 1, false)
914 PieceGI::new(self, 3, 1, false)
915 PieceKI::new(self, 4, 1, false)
916 PieceOU::new(self, 5, 1, false)
917 PieceKI::new(self, 6, 1, false)
918 PieceGI::new(self, 7, 1, false)
919 PieceKE::new(self, 8, 1, false)
920 PieceKY::new(self, 9, 1, false)
921 PieceKA::new(self, 2, 2, false)
922 PieceHI::new(self, 8, 2, false)
923 PieceFU::new(self, 1, 3, false)
924 PieceFU::new(self, 2, 3, false)
925 PieceFU::new(self, 3, 3, false)
926 PieceFU::new(self, 4, 3, false)
927 PieceFU::new(self, 5, 3, false)
928 PieceFU::new(self, 6, 3, false)
929 PieceFU::new(self, 7, 3, false)
930 PieceFU::new(self, 8, 3, false)
931 PieceFU::new(self, 9, 3, false)
933 PieceKY::new(self, 1, 9, true)
934 PieceKE::new(self, 2, 9, true)
935 PieceGI::new(self, 3, 9, true)
936 PieceKI::new(self, 4, 9, true)
937 PieceOU::new(self, 5, 9, true)
938 PieceKI::new(self, 6, 9, true)
939 PieceGI::new(self, 7, 9, true)
940 PieceKE::new(self, 8, 9, true)
941 PieceKY::new(self, 9, 9, true)
942 PieceKA::new(self, 8, 8, true)
943 PieceHI::new(self, 2, 8, true)
944 PieceFU::new(self, 1, 7, true)
945 PieceFU::new(self, 2, 7, true)
946 PieceFU::new(self, 3, 7, true)
947 PieceFU::new(self, 4, 7, true)
948 PieceFU::new(self, 5, 7, true)
949 PieceFU::new(self, 6, 7, true)
950 PieceFU::new(self, 7, 7, true)
951 PieceFU::new(self, 8, 7, true)
952 PieceFU::new(self, 9, 7, true)
955 def have_piece?(hands, name)
956 piece = hands.find { |i|
962 def move_to(x0, y0, x1, y1, name, sente)
969 if ((x0 == 0) || (y0 == 0))
970 piece = have_piece?(hands, name)
971 return :illegal if (! piece.move_to?(x1, y1, name))
972 piece.move_to(x1, y1)
974 return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))
975 if (@array[x0][y0].name != name) # promoted ?
976 @array[x0][y0].promoted = true
979 if (@array[x1][y1].name == "OU")
980 return :outori # return board update
982 @array[x1][y1].sente = @array[x0][y0].sente
983 @array[x1][y1].move_to(0, 0)
988 @array[x0][y0].move_to(x1, y1)
994 def look_for_ou(sente)
1000 (@array[x][y].name == "OU") &&
1001 (@array[x][y].sente == sente))
1008 raise "can't find ou"
1011 def checkmated?(sente) # sente is loosing
1012 ou = look_for_ou(sente)
1018 (@array[x][y].sente != sente))
1019 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1030 def uchifuzume?(sente)
1031 rival_ou = look_for_ou(! sente) # rival's ou
1032 if (sente) # rival is gote
1033 if ((rival_ou.y != 9) &&
1034 (@array[rival_ou.x][rival_ou.y + 1]) &&
1035 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1036 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1038 fu_y = rival_ou.y + 1
1043 if ((rival_ou.y != 0) &&
1044 (@array[rival_ou.x][rival_ou.y - 1]) &&
1045 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1046 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1048 fu_y = rival_ou.y - 1
1054 ## case: rival_ou is moving
1056 rival_ou.movable_grids.each do |(cand_x, cand_y)|
1057 tmp_board = Marshal.load(Marshal.dump(self))
1058 s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1059 raise "internal error" if (s != true)
1060 if (! tmp_board.checkmated?(! sente)) # good move
1065 ## case: rival is capturing fu
1071 (@array[x][y].sente != sente) &&
1072 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1073 if (@array[x][y].promoted)
1074 name = @array[x][y].promoted_name
1076 name = @array[x][y].name
1078 tmp_board = Marshal.load(Marshal.dump(self))
1079 s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1080 raise "internal error" if (s != true)
1081 if (! tmp_board.checkmated?(! sente)) # good move
1092 def oute_sennichite?(sente)
1093 if (checkmated?(! sente))
1096 if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
1100 if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
1108 def sennichite?(sente)
1110 if (@history[str] && (@history[str] >= 3)) # already 3 times
1116 def good_kachi?(sente)
1117 if (checkmated?(sente))
1118 puts "'NG: Checkmating." if $DEBUG
1122 ou = look_for_ou(sente)
1123 if (sente && (ou.y >= 4))
1124 puts "'NG: Black's OU does not enter yet." if $DEBUG
1127 if (! sente && (ou.y <= 6))
1128 puts "'NG: White's OU does not enter yet." if $DEBUG
1136 hands = @sente_hands
1146 (@array[x][y].sente == sente) &&
1147 (@array[x][y].point > 0))
1148 point = point + @array[x][y].point
1154 hands.each do |piece|
1155 point = point + piece.point
1159 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1164 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1169 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1174 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1178 def handle_one_move(str, sente=nil)
1179 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1186 elsif (str =~ /^%KACHI/)
1187 raise ArgumentError, "sente is null", caller if sente == nil
1188 if (good_kachi?(sente))
1193 elsif (str =~ /^%TORYO/)
1199 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1200 ((x0 != 0) || (y0 != 0)))
1202 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1208 hands = @sente_hands
1215 if ((x0 == 0) && (y0 == 0))
1216 return :illegal if (! have_piece?(hands, name))
1217 elsif (! @array[x0][y0])
1218 return :illegal # no piece
1219 elsif (@array[x0][y0].sente != sente)
1220 return :illegal # this is not mine
1221 elsif (@array[x0][y0].name != name)
1222 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1225 ## destination check
1226 if (@array[x1][y1] &&
1227 (@array[x1][y1].sente == sente)) # can't capture mine
1229 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1230 return :illegal # can't put on existing piece
1233 tmp_board = Marshal.load(Marshal.dump(self))
1234 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1235 return :oute_kaihimore if (tmp_board.checkmated?(sente))
1236 return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1237 return :sennichite if tmp_board.sennichite?(sente)
1239 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1243 move_to(x0, y0, x1, y1, name, sente)
1246 if (checkmated?(! sente))
1248 @sente_history[str] = (@sente_history[str] || 0) + 1
1250 @gote_history[str] = (@gote_history[str] || 0) + 1
1254 @sente_history.clear
1259 @history[str] = (@history[str] || 0) + 1
1267 a.push(sprintf("P%d", y))
1270 piece = @array[x][y]
1279 a.push(sprintf("\n"))
1282 if (! sente_hands.empty?)
1284 sente_hands.each do |p|
1285 a.push("00" + p.name)
1289 if (! gote_hands.empty?)
1291 gote_hands.each do |p|
1292 a.push("00" + p.name)
1302 attr_reader :players, :black, :white
1304 def initialize(p1, p2)
1308 if p1.sente && !p2.sente
1309 @black, @white = p1, p2
1310 elsif !p1.sente && p2.sente
1311 @black, @white = p2, p1
1313 raise "Never reached!"
1318 class GameResultWin < GameResult
1319 attr_reader :winner, :loser
1321 def initialize(winner, loser)
1323 @winner, @loser = winner, loser
1327 black_name = @black.id || @black.name
1328 white_name = @white.id || @white.name
1329 "%s:%s" % [black_name, white_name]
1333 class GameResultDraw < GameResult
1341 def initialize(game_name, player0, player1)
1342 @monitors = Array::new
1343 @game_name = game_name
1344 if (@game_name =~ /-(\d+)-(\d+)$/)
1345 @total_time = $1.to_i
1356 @current_player = @sente
1357 @next_player = @gote
1365 @sente.status = "agree_waiting"
1366 @gote.status = "agree_waiting"
1368 @id = sprintf("%s+%s+%s+%s+%s",
1369 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1370 @logfile = @id + ".csa"
1372 LEAGUE.games[@id] = self
1374 log_message(sprintf("game created %s", @id))
1384 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1385 attr_accessor :last_move, :current_turn
1389 @sente.rated? && @gote.rated?
1392 def monitoron(monitor)
1393 @monitors.delete(monitor)
1394 @monitors.push(monitor)
1397 def monitoroff(monitor)
1398 @monitors.delete(monitor)
1401 def reject(rejector)
1402 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1403 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1408 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1410 elsif (@current_player == killer)
1417 log_message(sprintf("game finished %s", @id))
1418 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1423 @sente.status = "connected"
1424 @gote.status = "connected"
1426 if (@current_player.protocol == "CSA")
1427 @current_player.finish
1429 if (@next_player.protocol == "CSA")
1432 @monitors = Array::new
1435 @current_player = nil
1437 LEAGUE.games.delete(@id)
1440 def handle_one_move(str, player)
1442 if (@current_player == player)
1443 @end_time = Time::new
1444 t = (@end_time - @start_time).floor
1445 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1448 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1450 elsif (str == :timeout)
1451 return false # time isn't expired. players aren't swapped. continue game
1453 @current_player.mytime = @current_player.mytime - t
1454 if (@current_player.mytime < 0)
1455 @current_player.mytime = 0
1459 move_status = @board.handle_one_move(str, @sente == @current_player)
1461 # log_error("handle_one_move raise exception for #{str}")
1462 # move_status = :illegal
1465 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1466 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1468 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1469 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1470 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1471 @fh.printf("%s\nT%d\n", str, t)
1472 @last_move = sprintf("%s,T%d", str, t)
1473 @current_turn = @current_turn + 1
1476 @monitors.each do |monitor|
1477 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1478 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1483 if (@next_player.status != "game") # rival is logout or disconnected
1485 elsif (status == :timeout)
1487 elsif (move_status == :illegal)
1489 elsif (move_status == :kachi_win)
1491 elsif (move_status == :kachi_lose)
1493 elsif (move_status == :toryo)
1495 elsif (move_status == :outori)
1497 elsif (move_status == :sennichite)
1499 elsif (move_status == :oute_sennichite)
1500 oute_sennichite_lose()
1501 elsif (move_status == :uchifuzume)
1503 elsif (move_status == :oute_kaihimore)
1504 oute_kaihimore_lose()
1508 finish() if finish_flag
1509 (@current_player, @next_player) = [@next_player, @current_player]
1510 @start_time = Time::new
1516 @current_player.status = "connected"
1517 @next_player.status = "connected"
1518 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1519 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1520 @fh.printf("%%TORYO\n")
1521 @fh.print(@board.to_s.gsub(/^/, "\'"))
1522 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1523 @result = GameResultWin.new(@current_player, @next_player)
1524 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1525 @monitors.each do |monitor|
1526 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1531 @current_player.status = "connected"
1532 @next_player.status = "connected"
1533 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1534 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1535 @fh.printf("%%TORYO\n")
1536 @fh.print(@board.to_s.gsub(/^/, "\'"))
1537 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1538 @result = GameResultWin.new(@next_player, @current_player)
1539 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1540 @monitors.each do |monitor|
1541 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1546 @current_player.status = "connected"
1547 @next_player.status = "connected"
1548 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1549 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1550 @fh.print(@board.to_s.gsub(/^/, "\'"))
1551 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1552 @result = GameResultDraw.new(@current_player, @next_player)
1553 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1554 @monitors.each do |monitor|
1555 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1559 def oute_sennichite_lose
1560 @current_player.status = "connected"
1561 @next_player.status = "connected"
1562 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1563 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1564 @fh.print(@board.to_s.gsub(/^/, "\'"))
1565 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1566 @result = GameResultWin.new(@next_player, @current_player)
1567 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1568 @monitors.each do |monitor|
1569 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1574 @current_player.status = "connected"
1575 @next_player.status = "connected"
1576 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1577 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1578 @fh.print(@board.to_s.gsub(/^/, "\'"))
1579 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1580 @result = GameResultWin.new(@next_player, @current_player)
1581 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1582 @monitors.each do |monitor|
1583 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1588 @current_player.status = "connected"
1589 @next_player.status = "connected"
1590 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1591 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1592 @fh.print(@board.to_s.gsub(/^/, "\'"))
1593 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1594 @result = GameResultWin.new(@next_player, @current_player)
1595 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1596 @monitors.each do |monitor|
1597 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1601 def oute_kaihimore_lose
1602 @current_player.status = "connected"
1603 @next_player.status = "connected"
1604 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1605 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1606 @fh.print(@board.to_s.gsub(/^/, "\'"))
1607 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1608 @result = GameResultWin.new(@next_player, @current_player)
1609 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1610 @monitors.each do |monitor|
1611 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1616 @current_player.status = "connected"
1617 @next_player.status = "connected"
1618 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1619 @next_player.write_safe("#TIME_UP\n#WIN\n")
1620 @fh.print(@board.to_s.gsub(/^/, "\'"))
1621 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1622 @result = GameResultWin.new(@next_player, @current_player)
1623 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1624 @monitors.each do |monitor|
1625 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1630 @current_player.status = "connected"
1631 @next_player.status = "connected"
1632 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1633 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1634 @fh.printf("%%KACHI\n")
1635 @fh.print(@board.to_s.gsub(/^/, "\'"))
1636 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1637 @result = GameResultWin.new(@current_player, @next_player)
1638 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1639 @monitors.each do |monitor|
1640 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1645 @current_player.status = "connected"
1646 @next_player.status = "connected"
1647 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1648 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1649 @fh.printf("%%KACHI\n")
1650 @fh.print(@board.to_s.gsub(/^/, "\'"))
1651 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1652 @result = GameResultWin.new(@next_player, @current_player)
1653 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1654 @monitors.each do |monitor|
1655 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1660 @current_player.status = "connected"
1661 @next_player.status = "connected"
1662 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1663 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1664 @fh.printf("%%TORYO\n")
1665 @fh.print(@board.to_s.gsub(/^/, "\'"))
1666 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1667 @result = GameResultWin.new(@next_player, @current_player)
1668 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1669 @monitors.each do |monitor|
1670 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1675 @current_player.status = "connected"
1676 @next_player.status = "connected"
1677 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1678 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1679 @fh.print(@board.to_s.gsub(/^/, "\'"))
1680 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1681 @result = GameResultWin.new(@current_player, @next_player)
1682 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1683 @monitors.each do |monitor|
1684 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1689 log_message(sprintf("game started %s", @id))
1690 @sente.write_safe(sprintf("START:%s\n", @id))
1691 @gote.write_safe(sprintf("START:%s\n", @id))
1692 @sente.mytime = @total_time
1693 @gote.mytime = @total_time
1694 @start_time = Time::new
1699 @fh = open(@logfile, "w")
1703 @fh.printf("N+%s\n", @sente.name)
1704 @fh.printf("N-%s\n", @gote.name)
1705 @fh.printf("$EVENT:%s\n", @id)
1707 @sente.write_safe(propose_message("+"))
1708 @gote.write_safe(propose_message("-"))
1710 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1712 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1713 P2 * -HI * * * * * -KA *
1714 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1715 P4 * * * * * * * * *
1716 P5 * * * * * * * * *
1717 P6 * * * * * * * * *
1718 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1719 P8 * +KA * * * * * +HI *
1720 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1729 Protocol_Version:1.1
1730 Protocol_Mode:Server
1732 Declaration:Jishogi 1.1
1734 Name+:#{@sente.name}
1740 Total_Time:#{@total_time}
1742 Least_Time_Per_Move:#{Least_Time_Per_Move}
1743 Remaining_Time+:#{@sente.mytime}
1744 Remaining_Time-:#{@gote.mytime}
1745 Last_Move:#{@last_move}
1746 Current_Turn:#{@current_turn}
1756 return str0 + @board.to_s + str1
1759 def propose_message(sg_flag)
1762 Protocol_Version:1.1
1763 Protocol_Mode:Server
1765 Declaration:Jishogi 1.1
1767 Name+:#{@sente.name}
1769 Your_Turn:#{sg_flag}
1774 Total_Time:#{@total_time}
1776 Least_Time_Per_Move:#{Least_Time_Per_Move}
1779 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1780 P2 * -HI * * * * * -KA *
1781 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1782 P4 * * * * * * * * *
1783 P5 * * * * * * * * *
1784 P6 * * * * * * * * *
1785 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1786 P8 * +KA * * * * * +HI *
1787 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1799 def issue_current_time
1800 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1801 @@mutex.synchronize do
1802 while time <= @@time do
1810 #################################################
1817 shogi-server - server for CSA server protocol
1820 shogi-server event_name port_number
1823 server for CSA server protocol
1827 specify filename for logging process ID
1830 this file is distributed under GPL version2 and might be compiled by Exerb
1842 def log_message(str)
1843 printf("%s message: %s\n", Time::new.to_s, str)
1846 def log_warning(str)
1847 printf("%s warning: %s\n", Time::new.to_s, str)
1851 printf("%s error: %s\n", Time::new.to_s, str)
1855 def parse_command_line
1857 parser = GetoptLong.new
1858 parser.ordering = GetoptLong::REQUIRE_ORDER
1860 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1864 parser.each_option do |name, arg|
1865 name.sub!(/^--/, '')
1866 options[name] = arg.dup
1870 raise parser.error_message
1875 def good_game_name?(str)
1876 if ((str =~ /^(.+)-\d+-\d+$/) &&
1877 (good_identifier?($1)))
1884 def good_identifier?(str)
1885 if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1892 def good_login?(str)
1894 if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1895 (tokens[0] == "LOGIN") &&
1896 (good_identifier?(tokens[1])))
1903 def write_pid_file(file)
1904 open(file, "w") do |fh|
1905 fh.print Process::pid, "\n"
1909 def mutex_watchdog(mutex, sec)
1921 log_error("mutex watchdog timeout")
1931 mutex_watchdog($mutex, 10)
1934 $options = parse_command_line
1935 if (ARGV.length != 2)
1940 LEAGUE.event = ARGV.shift
1943 write_pid_file($options["pid-file"]) if ($options["pid-file"])
1945 server = TCPserver.open(port)
1946 log_message("server started")
1949 Thread::start(server.accept) do |client|
1953 while (str = client.gets_timeout(Login_Time))
1958 if (good_login?(str))
1959 player = Player::new(str, client)
1960 if (LEAGUE.players[player.name])
1961 if ((LEAGUE.players[player.name].password == player.password) &&
1962 (LEAGUE.players[player.name].status != "game"))
1963 log_message(sprintf("user %s login forcely", player.name))
1964 LEAGUE.players[player.name].kill
1966 client.write_safe("LOGIN:incorrect" + eol)
1967 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1976 client.write_safe("LOGIN:incorrect" + eol)
1977 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1988 log_message(sprintf("user %s login", player.name))
1993 player.game.kill(player)
1995 player.finish # socket has been closed
1996 LEAGUE.delete(player)
1997 log_message(sprintf("user %s logout", player.name))
2006 LEAGUE = League::new