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)))
122 @players.each_value do |p|
124 @db[p.id] = {'name' => p.name, 'rate' => p.rate}
130 hash = search(player.id)
133 player.rate = hash['rate']
147 def initialize(str, socket)
150 @id = nil, @rate = nil # used by rating
152 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
154 @protocol = nil # CSA or x1
155 @eol = "\m" # favorite eol code
158 @mytime = 0 # set in start method also
162 @write_queue = Queue::new
166 attr_accessor :name, :password, :socket, :status, :rate
167 attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
168 attr_accessor :main_thread, :writer_thread, :write_queue
172 log_message(sprintf("user %s killed", @name))
177 Thread::kill(@main_thread) if @main_thread
181 if (@status != "finished")
183 log_message(sprintf("user %s finish", @name))
184 Thread::kill(@writer_thread) if @writer_thread
186 @socket.close if (! @socket.closed?)
188 log_message(sprintf("user %s finish failed", @name))
194 @write_queue.push(str.gsub(/[\r\n]+/, @eol))
198 while (str = @write_queue.pop)
199 @socket.write_safe(str)
204 if ((status == "game_waiting") ||
205 (status == "start_waiting") ||
206 (status == "agree_waiting") ||
209 return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
210 elsif (@sente == false)
211 return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
212 elsif (@sente == nil)
213 return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
216 return sprintf("%s %s %s", @name, @protocol, @status)
221 @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
228 (login, @name, @password, ext) = str.split
229 @name, trip = @name.split(",") # used by rating
230 @id = trip ? Digest::MD5.hexdigest("#{@name},#{@trip}") : nil
236 @main_thread = Thread::current
237 @writer_thread = Thread::start do
243 write_safe(sprintf("LOGIN:%s OK\n", @name))
244 if (@protocol != "CSA")
245 log_message(sprintf("user %s run in %s mode", @name, @protocol))
246 write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
248 log_message(sprintf("user %s run in CSA mode", @name))
249 if (good_game_name?(@password))
250 csa_1st_str = "%%GAME #{@password} *"
252 csa_1st_str = "%%GAME #{Default_Game_Name} *"
256 while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
263 if (@write_queue.size > Max_Write_Queue_Size)
264 log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
268 if (@status == "finished")
271 str.chomp! if (str.class == String)
274 if (@status == "game")
275 array_str = str.split(",")
276 move = array_str.shift
277 additional = array_str.shift
278 if /^'(.*)/ =~ additional
279 comment = array_str.unshift("'*#{$1}")
281 s = @game.handle_one_move(move, self)
282 @game.fh.print("#{comment}\n") if (comment && !s)
283 return if (s && @protocol == "CSA")
285 when /^%[^%]/, :timeout
286 if (@status == "game")
287 s = @game.handle_one_move(str, self)
288 return if (s && @protocol == "CSA")
291 if (@status == "agree_waiting")
293 return if (@protocol == "CSA")
295 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
298 if (@status == "agree_waiting")
299 @status = "start_waiting"
300 if ((@game.sente.status == "start_waiting") &&
301 (@game.gote.status == "start_waiting"))
303 @game.sente.status = "game"
304 @game.gote.status = "game"
307 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
309 when /^%%SHOW\s+(\S+)/
311 if (LEAGUE.games[game_id])
312 write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
314 write_safe("##[SHOW] +OK\n")
315 when /^%%MONITORON\s+(\S+)/
317 if (LEAGUE.games[game_id])
318 LEAGUE.games[game_id].monitoron(self)
319 write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
320 write_safe("##[MONITOR][#{game_id}] +OK\n")
322 when /^%%MONITOROFF\s+(\S+)/
324 if (LEAGUE.games[game_id])
325 LEAGUE.games[game_id].monitoroff(self)
330 if ((@status == "connected") || (@status == "game_waiting"))
331 @status = "connected"
334 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
336 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
340 if (! good_game_name?(game_name))
341 write_safe(sprintf("##[ERROR] bad game name\n"))
343 elsif ((@status == "connected") || (@status == "game_waiting"))
346 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
349 if ((my_sente_str == "*") ||
350 (my_sente_str == "+") ||
351 (my_sente_str == "-"))
354 write_safe(sprintf("##[ERROR] bad game option\n"))
358 if (my_sente_str == "*")
359 rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
360 elsif (my_sente_str == "+")
361 rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
362 elsif (my_sente_str == "-")
363 rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
368 @game_name = game_name
369 if ((my_sente_str == "*") && (rival.sente == nil))
377 elsif (rival.sente == true) # rival has higher priority
379 elsif (rival.sente == false)
381 elsif (my_sente_str == "+")
384 elsif (my_sente_str == "-")
390 Game::new(@game_name, self, rival)
391 self.status = "agree_waiting"
392 rival.status = "agree_waiting"
393 else # rival not found
394 if (command_name == "GAME")
395 @status = "game_waiting"
396 @game_name = game_name
397 if (my_sente_str == "+")
399 elsif (my_sente_str == "-")
405 write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
406 @status = "connected"
411 when /^%%CHAT\s+(.+)/
413 LEAGUE.players.each do |name, player|
414 if (player.protocol != "CSA")
415 player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message))
420 LEAGUE.games.each do |id, game|
421 buf.push(sprintf("##[LIST] %s\n", id))
423 buf.push("##[LIST] +OK\n")
427 LEAGUE.players.each do |name, player|
428 buf.push(sprintf("##[WHO] %s\n", player.to_s))
430 buf.push("##[WHO] +OK\n")
433 @status = "connected"
434 write_safe("LOGOUT:completed\n")
437 ## ignore null string
439 write_safe(sprintf("##[ERROR] unknown command %s\n", str))
449 PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
450 def initialize(board, x, y, sente, promoted=false)
457 if ((x == 0) || (y == 0))
459 hands = board.sente_hands
461 hands = board.gote_hands
468 @board.array[x][y] = self
471 attr_accessor :promoted, :sente, :x, :y, :board
473 def room_of_head?(x, y, name)
478 return adjacent_movable_grids + far_movable_grids
481 def far_movable_grids
486 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
487 if ((@board.array[x][y] == nil) || # dst is empty
488 (@board.array[x][y].sente != @sente)) # dst is enemy
496 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
497 if (@board.array[x][y] == nil) # dst is empty?
504 def adjacent_movable_grids
507 moves = @promoted_moves
509 moves = @normal_moves
511 moves.each do |(dx, dy)|
518 if (jump_to?(cand_x, cand_y))
519 grids.push([cand_x, cand_y])
525 def move_to?(x, y, name)
526 return false if (! room_of_head?(x, y, name))
527 return false if ((name != @name) && (name != @promoted_name))
528 return false if (@promoted && (name != @promoted_name)) # can't un-promote
531 return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
533 return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
535 return false if ((6 >= @y) && (6 >= y) && (name != @name))
539 if ((@x == 0) || (@y == 0))
540 return jump_to?(x, y)
542 return movable_grids.include?([x, y])
547 if ((@x == 0) || (@y == 0))
549 @board.sente_hands.delete(self)
551 @board.gote_hands.delete(self)
553 @board.array[x][y] = self
554 elsif ((x == 0) || (y == 0))
555 @promoted = false # clear promoted flag before moving to hands
557 @board.sente_hands.push(self)
559 @board.gote_hands.push(self)
561 @board.array[@x][@y] = nil
563 @board.array[@x][@y] = nil
564 @board.array[x][y] = self
597 class PieceFU < Piece
600 @normal_moves = [[0, +1]]
601 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
603 @promoted_name = "TO"
606 def room_of_head?(x, y, name)
609 return false if (y == 1)
611 return false if (y == 9)
617 if ((iy != @y) && # not source position
618 @board.array[x][iy] &&
619 (@board.array[x][iy].sente == @sente) && # mine
620 (@board.array[x][iy].name == "FU") &&
621 (@board.array[x][iy].promoted == false))
631 class PieceKY < Piece
635 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
637 @promoted_name = "NY"
640 def room_of_head?(x, y, name)
643 return false if (y == 1)
645 return false if (y == 9)
650 def far_movable_grids
658 while (jump_to?(cand_x, cand_y))
659 grids.push([cand_x, cand_y])
660 break if (! put_to?(cand_x, cand_y))
666 while (jump_to?(cand_x, cand_y))
667 grids.push([cand_x, cand_y])
668 break if (! put_to?(cand_x, cand_y))
676 class PieceKE < Piece
679 @normal_moves = [[+1, +2], [-1, +2]]
680 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
682 @promoted_name = "NK"
685 def room_of_head?(x, y, name)
688 return false if ((y == 1) || (y == 2))
690 return false if ((y == 9) || (y == 8))
696 class PieceGI < Piece
699 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
700 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
702 @promoted_name = "NG"
706 class PieceKI < Piece
709 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
716 class PieceKA < Piece
720 @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
722 @promoted_name = "UM"
725 def far_movable_grids
730 while (jump_to?(cand_x, cand_y))
731 grids.push([cand_x, cand_y])
732 break if (! put_to?(cand_x, cand_y))
739 while (jump_to?(cand_x, cand_y))
740 grids.push([cand_x, cand_y])
741 break if (! put_to?(cand_x, cand_y))
748 while (jump_to?(cand_x, cand_y))
749 grids.push([cand_x, cand_y])
750 break if (! put_to?(cand_x, cand_y))
757 while (jump_to?(cand_x, cand_y))
758 grids.push([cand_x, cand_y])
759 break if (! put_to?(cand_x, cand_y))
766 class PieceHI < Piece
770 @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
772 @promoted_name = "RY"
775 def far_movable_grids
780 while (jump_to?(cand_x, cand_y))
781 grids.push([cand_x, cand_y])
782 break if (! put_to?(cand_x, cand_y))
788 while (jump_to?(cand_x, cand_y))
789 grids.push([cand_x, cand_y])
790 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))
804 while (jump_to?(cand_x, cand_y))
805 grids.push([cand_x, cand_y])
806 break if (! put_to?(cand_x, cand_y))
812 class PieceOU < Piece
815 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
825 @sente_hands = Array::new
826 @gote_hands = Array::new
828 @sente_history = Hash::new
829 @gote_history = Hash::new
830 @array = [[], [], [], [], [], [], [], [], [], []]
833 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
834 attr_reader :move_count
837 PieceKY::new(self, 1, 1, false)
838 PieceKE::new(self, 2, 1, false)
839 PieceGI::new(self, 3, 1, false)
840 PieceKI::new(self, 4, 1, false)
841 PieceOU::new(self, 5, 1, false)
842 PieceKI::new(self, 6, 1, false)
843 PieceGI::new(self, 7, 1, false)
844 PieceKE::new(self, 8, 1, false)
845 PieceKY::new(self, 9, 1, false)
846 PieceKA::new(self, 2, 2, false)
847 PieceHI::new(self, 8, 2, false)
848 PieceFU::new(self, 1, 3, false)
849 PieceFU::new(self, 2, 3, false)
850 PieceFU::new(self, 3, 3, false)
851 PieceFU::new(self, 4, 3, false)
852 PieceFU::new(self, 5, 3, false)
853 PieceFU::new(self, 6, 3, false)
854 PieceFU::new(self, 7, 3, false)
855 PieceFU::new(self, 8, 3, false)
856 PieceFU::new(self, 9, 3, false)
858 PieceKY::new(self, 1, 9, true)
859 PieceKE::new(self, 2, 9, true)
860 PieceGI::new(self, 3, 9, true)
861 PieceKI::new(self, 4, 9, true)
862 PieceOU::new(self, 5, 9, true)
863 PieceKI::new(self, 6, 9, true)
864 PieceGI::new(self, 7, 9, true)
865 PieceKE::new(self, 8, 9, true)
866 PieceKY::new(self, 9, 9, true)
867 PieceKA::new(self, 8, 8, true)
868 PieceHI::new(self, 2, 8, true)
869 PieceFU::new(self, 1, 7, true)
870 PieceFU::new(self, 2, 7, true)
871 PieceFU::new(self, 3, 7, true)
872 PieceFU::new(self, 4, 7, true)
873 PieceFU::new(self, 5, 7, true)
874 PieceFU::new(self, 6, 7, true)
875 PieceFU::new(self, 7, 7, true)
876 PieceFU::new(self, 8, 7, true)
877 PieceFU::new(self, 9, 7, true)
880 def have_piece?(hands, name)
881 piece = hands.find { |i|
887 def move_to(x0, y0, x1, y1, name, sente)
894 if ((x0 == 0) || (y0 == 0))
895 piece = have_piece?(hands, name)
896 return :illegal if (! piece.move_to?(x1, y1, name))
897 piece.move_to(x1, y1)
899 return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))
900 if (@array[x0][y0].name != name) # promoted ?
901 @array[x0][y0].promoted = true
904 if (@array[x1][y1].name == "OU")
905 return :outori # return board update
907 @array[x1][y1].sente = @array[x0][y0].sente
908 @array[x1][y1].move_to(0, 0)
913 @array[x0][y0].move_to(x1, y1)
919 def look_for_ou(sente)
925 (@array[x][y].name == "OU") &&
926 (@array[x][y].sente == sente))
933 raise "can't find ou"
936 def checkmated?(sente) # sente is loosing
937 ou = look_for_ou(sente)
943 (@array[x][y].sente != sente))
944 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
955 def uchifuzume?(sente)
956 rival_ou = look_for_ou(! sente) # rival's ou
957 if (sente) # rival is gote
958 if ((rival_ou.y != 9) &&
959 (@array[rival_ou.x][rival_ou.y + 1]) &&
960 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
961 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
963 fu_y = rival_ou.y + 1
968 if ((rival_ou.y != 0) &&
969 (@array[rival_ou.x][rival_ou.y - 1]) &&
970 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
971 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
973 fu_y = rival_ou.y - 1
979 ## case: rival_ou is moving
981 rival_ou.movable_grids.each do |(cand_x, cand_y)|
982 tmp_board = Marshal.load(Marshal.dump(self))
983 s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
984 raise "internal error" if (s != true)
985 if (! tmp_board.checkmated?(! sente)) # good move
990 ## case: rival is capturing fu
996 (@array[x][y].sente != sente) &&
997 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
998 if (@array[x][y].promoted)
999 name = @array[x][y].promoted_name
1001 name = @array[x][y].name
1003 tmp_board = Marshal.load(Marshal.dump(self))
1004 s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1005 raise "internal error" if (s != true)
1006 if (! tmp_board.checkmated?(! sente)) # good move
1017 def oute_sennichite?(sente)
1018 if (checkmated?(! sente))
1021 if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
1025 if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
1033 def sennichite?(sente)
1035 if (@history[str] && (@history[str] >= 3)) # already 3 times
1041 def good_kachi?(sente)
1042 if (checkmated?(sente))
1043 puts "'NG: Checkmating." if $DEBUG
1047 ou = look_for_ou(sente)
1048 if (sente && (ou.y >= 4))
1049 puts "'NG: Black's OU does not enter yet." if $DEBUG
1052 if (! sente && (ou.y <= 6))
1053 puts "'NG: White's OU does not enter yet." if $DEBUG
1061 hands = @sente_hands
1071 (@array[x][y].sente == sente) &&
1072 (@array[x][y].point > 0))
1073 point = point + @array[x][y].point
1079 hands.each do |piece|
1080 point = point + piece.point
1084 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1089 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1094 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1099 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1103 def handle_one_move(str, sente)
1104 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1111 elsif (str =~ /^%KACHI/)
1112 if (good_kachi?(sente))
1117 elsif (str =~ /^%TORYO/)
1123 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1124 ((x0 != 0) || (y0 != 0)))
1126 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1132 hands = @sente_hands
1139 if ((x0 == 0) && (y0 == 0))
1140 return :illegal if (! have_piece?(hands, name))
1141 elsif (! @array[x0][y0])
1142 return :illegal # no piece
1143 elsif (@array[x0][y0].sente != sente)
1144 return :illegal # this is not mine
1145 elsif (@array[x0][y0].name != name)
1146 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1149 ## destination check
1150 if (@array[x1][y1] &&
1151 (@array[x1][y1].sente == sente)) # can't capture mine
1153 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1154 return :illegal # can't put on existing piece
1157 tmp_board = Marshal.load(Marshal.dump(self))
1158 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1159 return :oute_kaihimore if (tmp_board.checkmated?(sente))
1160 return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1161 return :sennichite if tmp_board.sennichite?(sente)
1163 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1167 move_to(x0, y0, x1, y1, name, sente)
1170 if (checkmated?(! sente))
1172 @sente_history[str] = (@sente_history[str] || 0) + 1
1174 @gote_history[str] = (@gote_history[str] || 0) + 1
1178 @sente_history.clear
1183 @history[str] = (@history[str] || 0) + 1
1191 a.push(sprintf("P%d", y))
1194 piece = @array[x][y]
1203 a.push(sprintf("\n"))
1206 if (! sente_hands.empty?)
1208 sente_hands.each do |p|
1209 a.push("00" + p.name)
1213 if (! gote_hands.empty?)
1215 gote_hands.each do |p|
1216 a.push("00" + p.name)
1225 #################################################
1228 # http://www2.saganet.ne.jp/a-sim/mhp0726.html
1229 # http://www10.plala.or.jp/greenstone/content1_3.html
1233 def new_rate(me, you, win)
1236 if me == nil && you != nil
1237 return (you + w*400).to_i
1238 elsif me == nil && you == nil
1239 return (1100 + w*400).to_i
1243 score = me + K*(w - we(me, you))
1252 1.0 / ( 10**(-1.0*dr/400) + 1 )
1259 def initialize(p1, p2)
1266 class GameResultWin < GameResult
1267 attr_reader :winner, :loser
1269 def initialize(winner, loser)
1271 @winner, @loser = winner, loser
1281 new_winner = rating.new_rate(@winner.rate, @loser.rate, true)
1282 new_loser = rating.new_rate(@loser.rate, @winner.rate, false)
1283 @winner.rate = new_winner if new_winner
1284 @loser.rate = new_loser if new_loser
1288 class GameResultDraw < GameResult
1296 def initialize(game_name, player0, player1)
1297 @monitors = Array::new
1298 @game_name = game_name
1299 if (@game_name =~ /-(\d+)-(\d+)$/)
1300 @total_time = $1.to_i
1311 @current_player = @sente
1312 @next_player = @gote
1320 @sente.status = "agree_waiting"
1321 @gote.status = "agree_waiting"
1323 @id = sprintf("%s+%s+%s+%s+%s",
1324 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1325 @logfile = @id + ".csa"
1327 LEAGUE.games[@id] = self
1329 log_message(sprintf("game created %s", @id))
1339 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1340 attr_accessor :last_move, :current_turn
1343 def monitoron(monitor)
1344 @monitors.delete(monitor)
1345 @monitors.push(monitor)
1348 def monitoroff(monitor)
1349 @monitors.delete(monitor)
1352 def reject(rejector)
1353 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1354 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1359 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1361 elsif (@current_player == killer)
1368 log_message(sprintf("game finished %s", @id))
1369 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1374 @sente.status = "connected"
1375 @gote.status = "connected"
1377 if (@current_player.protocol == "CSA")
1378 @current_player.finish
1380 if (@next_player.protocol == "CSA")
1383 @monitors = Array::new
1386 @current_player = nil
1388 LEAGUE.games.delete(@id)
1392 def handle_one_move(str, player)
1394 if (@current_player == player)
1395 @end_time = Time::new
1396 t = (@end_time - @start_time).floor
1397 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1400 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1402 elsif (str == :timeout)
1403 return false # time isn't expired. players aren't swapped. continue game
1405 @current_player.mytime = @current_player.mytime - t
1406 if (@current_player.mytime < 0)
1407 @current_player.mytime = 0
1411 move_status = @board.handle_one_move(str, @sente == @current_player)
1413 # log_error("handle_one_move raise exception for #{str}")
1414 # move_status = :illegal
1417 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1418 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1420 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1421 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1422 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1423 @fh.printf("%s\nT%d\n", str, t)
1424 @last_move = sprintf("%s,T%d", str, t)
1425 @current_turn = @current_turn + 1
1428 @monitors.each do |monitor|
1429 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1430 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1435 if (@next_player.status != "game") # rival is logout or disconnected
1437 elsif (status == :timeout)
1439 elsif (move_status == :illegal)
1441 elsif (move_status == :kachi_win)
1443 elsif (move_status == :kachi_lose)
1445 elsif (move_status == :toryo)
1447 elsif (move_status == :outori)
1449 elsif (move_status == :sennichite)
1451 elsif (move_status == :oute_sennichite)
1452 oute_sennichite_lose()
1453 elsif (move_status == :uchifuzume)
1455 elsif (move_status == :oute_kaihimore)
1456 oute_kaihimore_lose()
1460 finish() if finish_flag
1461 (@current_player, @next_player) = [@next_player, @current_player]
1462 @start_time = Time::new
1468 @current_player.status = "connected"
1469 @next_player.status = "connected"
1470 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1471 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1472 @result = GameResultWin.new(@current_player, @next_player)
1473 @fh.printf("%%TORYO\n")
1474 @fh.print(@board.to_s.gsub(/^/, "\'"))
1475 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1476 @monitors.each do |monitor|
1477 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1482 @current_player.status = "connected"
1483 @next_player.status = "connected"
1484 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1485 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1486 @result = GameResultWin.new(@next_player, @current_player)
1487 @fh.printf("%%TORYO\n")
1488 @fh.print(@board.to_s.gsub(/^/, "\'"))
1489 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1490 @monitors.each do |monitor|
1491 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1496 @current_player.status = "connected"
1497 @next_player.status = "connected"
1498 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1499 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1500 @result = GameResultDraw.new(@current_player, @next_player)
1501 @fh.print(@board.to_s.gsub(/^/, "\'"))
1502 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1503 @monitors.each do |monitor|
1504 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1508 def oute_sennichite_lose
1509 @current_player.status = "connected"
1510 @next_player.status = "connected"
1511 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1512 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1513 @result = GameResultWin.new(@next_player, @current_player)
1514 @fh.print(@board.to_s.gsub(/^/, "\'"))
1515 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1516 @monitors.each do |monitor|
1517 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1522 @current_player.status = "connected"
1523 @next_player.status = "connected"
1524 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1525 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1526 @result = GameResultWin.new(@next_player, @current_player)
1527 @fh.print(@board.to_s.gsub(/^/, "\'"))
1528 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1529 @monitors.each do |monitor|
1530 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1535 @current_player.status = "connected"
1536 @next_player.status = "connected"
1537 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1538 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1539 @result = GameResultWin.new(@next_player, @current_player)
1540 @fh.print(@board.to_s.gsub(/^/, "\'"))
1541 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1542 @monitors.each do |monitor|
1543 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1547 def oute_kaihimore_lose
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 @result = GameResultWin.new(@next_player, @current_player)
1553 @fh.print(@board.to_s.gsub(/^/, "\'"))
1554 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1555 @monitors.each do |monitor|
1556 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1561 @current_player.status = "connected"
1562 @next_player.status = "connected"
1563 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1564 @next_player.write_safe("#TIME_UP\n#WIN\n")
1565 @result = GameResultWin.new(@next_player, @current_player)
1566 @fh.print(@board.to_s.gsub(/^/, "\'"))
1567 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1568 @monitors.each do |monitor|
1569 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1574 @current_player.status = "connected"
1575 @next_player.status = "connected"
1576 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1577 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1578 @result = GameResultWin.new(@current_player, @next_player)
1579 @fh.printf("%%KACHI\n")
1580 @fh.print(@board.to_s.gsub(/^/, "\'"))
1581 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1582 @monitors.each do |monitor|
1583 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1588 @current_player.status = "connected"
1589 @next_player.status = "connected"
1590 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1591 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1592 @result = GameResultWin(@next_player, @current_player)
1593 @fh.printf("%%KACHI\n")
1594 @fh.print(@board.to_s.gsub(/^/, "\'"))
1595 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1596 @monitors.each do |monitor|
1597 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1602 @current_player.status = "connected"
1603 @next_player.status = "connected"
1604 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1605 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1606 @result = GameResultWin.new(@next_player, @current_player)
1607 @fh.printf("%%TORYO\n")
1608 @fh.print(@board.to_s.gsub(/^/, "\'"))
1609 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1610 @monitors.each do |monitor|
1611 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1616 @current_player.status = "connected"
1617 @next_player.status = "connected"
1618 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1619 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1620 @result = GameResultWin.new(@current_player, @next_player)
1621 @fh.print(@board.to_s.gsub(/^/, "\'"))
1622 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1623 @monitors.each do |monitor|
1624 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1629 log_message(sprintf("game started %s", @id))
1630 @sente.write_safe(sprintf("START:%s\n", @id))
1631 @gote.write_safe(sprintf("START:%s\n", @id))
1632 @sente.mytime = @total_time
1633 @gote.mytime = @total_time
1634 @start_time = Time::new
1639 @fh = open(@logfile, "w")
1643 @fh.printf("N+%s\n", @sente.name)
1644 @fh.printf("N-%s\n", @gote.name)
1645 @fh.printf("$EVENT:%s\n", @id)
1647 @sente.write_safe(propose_message("+"))
1648 @gote.write_safe(propose_message("-"))
1650 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1652 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1653 P2 * -HI * * * * * -KA *
1654 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1655 P4 * * * * * * * * *
1656 P5 * * * * * * * * *
1657 P6 * * * * * * * * *
1658 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1659 P8 * +KA * * * * * +HI *
1660 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1669 Protocol_Version:1.1
1670 Protocol_Mode:Server
1672 Declaration:Jishogi 1.1
1674 Name+:#{@sente.name}
1680 Total_Time:#{@total_time}
1682 Least_Time_Per_Move:#{Least_Time_Per_Move}
1683 Remaining_Time+:#{@sente.mytime}
1684 Remaining_Time-:#{@gote.mytime}
1685 Last_Move:#{@last_move}
1686 Current_Turn:#{@current_turn}
1696 return str0 + @board.to_s + str1
1699 def propose_message(sg_flag)
1702 Protocol_Version:1.1
1703 Protocol_Mode:Server
1705 Declaration:Jishogi 1.1
1707 Name+:#{@sente.name}
1709 Your_Turn:#{sg_flag}
1714 Total_Time:#{@total_time}
1716 Least_Time_Per_Move:#{Least_Time_Per_Move}
1719 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1720 P2 * -HI * * * * * -KA *
1721 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1722 P4 * * * * * * * * *
1723 P5 * * * * * * * * *
1724 P6 * * * * * * * * *
1725 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1726 P8 * +KA * * * * * +HI *
1727 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1739 def issue_current_time
1740 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1741 @@mutex.synchronize do
1742 while time <= @@time do
1750 #################################################
1757 shogi-server - server for CSA server protocol
1760 shogi-server event_name port_number
1763 server for CSA server protocol
1767 specify filename for logging process ID
1770 this file is distributed under GPL version2 and might be compiled by Exerb
1782 def log_message(str)
1783 printf("%s message: %s\n", Time::new.to_s, str)
1786 def log_warning(str)
1787 printf("%s warning: %s\n", Time::new.to_s, str)
1791 printf("%s error: %s\n", Time::new.to_s, str)
1795 def parse_command_line
1797 parser = GetoptLong.new
1798 parser.ordering = GetoptLong::REQUIRE_ORDER
1800 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1804 parser.each_option do |name, arg|
1805 name.sub!(/^--/, '')
1806 options[name] = arg.dup
1810 raise parser.error_message
1815 def good_game_name?(str)
1816 if ((str =~ /^(.+)-\d+-\d+$/) &&
1817 (good_identifier?($1)))
1824 def good_identifier?(str)
1825 if ( ((str =~ /\A[\w\d_@\-\.]+\z/) ||
1826 (str =~ /\A[\w\d_@\-\.]+,[\w\d_@\-\.]+\z/)) &&
1827 (str.length < Max_Identifier_Length))
1834 def good_login?(str)
1836 if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1837 (tokens[0] == "LOGIN") &&
1838 (good_identifier?(tokens[1])))
1845 def write_pid_file(file)
1846 open(file, "w") do |fh|
1847 fh.print Process::pid, "\n"
1851 def mutex_watchdog(mutex, sec)
1863 log_error("mutex watchdog timeout")
1873 mutex_watchdog($mutex, 10)
1876 $options = parse_command_line
1877 if (ARGV.length != 2)
1882 LEAGUE.event = ARGV.shift
1885 write_pid_file($options["pid-file"]) if ($options["pid-file"])
1887 server = TCPserver.open(port)
1888 log_message("server started")
1891 Thread::start(server.accept) do |client|
1895 while (str = client.gets_timeout(Login_Time))
1900 if (good_login?(str))
1901 player = Player::new(str, client)
1902 if (LEAGUE.players[player.name])
1903 if ((LEAGUE.players[player.name].password == player.password) &&
1904 (LEAGUE.players[player.name].status != "game"))
1905 log_message(sprintf("user %s login forcely", player.name))
1906 LEAGUE.players[player.name].kill
1908 client.write_safe("LOGIN:incorrect" + eol)
1909 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1918 client.write_safe("LOGIN:incorrect" + eol)
1919 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1930 log_message(sprintf("user %s login", player.name))
1935 player.game.kill(player)
1937 player.finish # socket has been closed
1938 LEAGUE.delete(player)
1939 log_message(sprintf("user %s logout", player.name))
1948 LEAGUE = League::new