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
1282 attr_reader :winner, :loser
1284 def initialize(winner, loser)
1286 @winner, @loser = winner, loser
1290 black_name = @black.id || @black.name
1291 white_name = @white.id || @white.name
1292 "%s:%s" % [black_name, white_name]
1296 class GameResultDraw < GameResult
1304 def initialize(game_name, player0, player1)
1305 @monitors = Array::new
1306 @game_name = game_name
1307 if (@game_name =~ /-(\d+)-(\d+)$/)
1308 @total_time = $1.to_i
1319 @current_player = @sente
1320 @next_player = @gote
1328 @sente.status = "agree_waiting"
1329 @gote.status = "agree_waiting"
1331 @id = sprintf("%s+%s+%s+%s+%s",
1332 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1333 @logfile = @id + ".csa"
1335 LEAGUE.games[@id] = self
1337 log_message(sprintf("game created %s", @id))
1347 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1348 attr_accessor :last_move, :current_turn
1352 @sente.rated? && @gote.rated?
1355 def monitoron(monitor)
1356 @monitors.delete(monitor)
1357 @monitors.push(monitor)
1360 def monitoroff(monitor)
1361 @monitors.delete(monitor)
1364 def reject(rejector)
1365 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1366 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1371 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1373 elsif (@current_player == killer)
1380 log_message(sprintf("game finished %s", @id))
1381 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1386 @sente.status = "connected"
1387 @gote.status = "connected"
1389 if (@current_player.protocol == "CSA")
1390 @current_player.finish
1392 if (@next_player.protocol == "CSA")
1395 @monitors = Array::new
1398 @current_player = nil
1400 LEAGUE.games.delete(@id)
1403 def handle_one_move(str, player)
1405 if (@current_player == player)
1406 @end_time = Time::new
1407 t = (@end_time - @start_time).floor
1408 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1411 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1413 elsif (str == :timeout)
1414 return false # time isn't expired. players aren't swapped. continue game
1416 @current_player.mytime = @current_player.mytime - t
1417 if (@current_player.mytime < 0)
1418 @current_player.mytime = 0
1422 move_status = @board.handle_one_move(str, @sente == @current_player)
1424 # log_error("handle_one_move raise exception for #{str}")
1425 # move_status = :illegal
1428 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1429 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1431 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1432 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1433 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1434 @fh.printf("%s\nT%d\n", str, t)
1435 @last_move = sprintf("%s,T%d", str, t)
1436 @current_turn = @current_turn + 1
1439 @monitors.each do |monitor|
1440 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1441 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1446 if (@next_player.status != "game") # rival is logout or disconnected
1448 elsif (status == :timeout)
1450 elsif (move_status == :illegal)
1452 elsif (move_status == :kachi_win)
1454 elsif (move_status == :kachi_lose)
1456 elsif (move_status == :toryo)
1458 elsif (move_status == :outori)
1460 elsif (move_status == :sennichite)
1462 elsif (move_status == :oute_sennichite)
1463 oute_sennichite_lose()
1464 elsif (move_status == :uchifuzume)
1466 elsif (move_status == :oute_kaihimore)
1467 oute_kaihimore_lose()
1471 finish() if finish_flag
1472 (@current_player, @next_player) = [@next_player, @current_player]
1473 @start_time = Time::new
1479 @current_player.status = "connected"
1480 @next_player.status = "connected"
1481 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1482 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1483 @fh.printf("%%TORYO\n")
1484 @fh.print(@board.to_s.gsub(/^/, "\'"))
1485 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1486 @result = GameResultWin.new(@current_player, @next_player)
1487 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1488 @monitors.each do |monitor|
1489 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1494 @current_player.status = "connected"
1495 @next_player.status = "connected"
1496 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1497 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1498 @fh.printf("%%TORYO\n")
1499 @fh.print(@board.to_s.gsub(/^/, "\'"))
1500 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1501 @result = GameResultWin.new(@next_player, @current_player)
1502 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1503 @monitors.each do |monitor|
1504 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1509 @current_player.status = "connected"
1510 @next_player.status = "connected"
1511 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1512 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1513 @fh.print(@board.to_s.gsub(/^/, "\'"))
1514 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1515 @result = GameResultDraw.new(@current_player, @next_player)
1516 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1517 @monitors.each do |monitor|
1518 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1522 def oute_sennichite_lose
1523 @current_player.status = "connected"
1524 @next_player.status = "connected"
1525 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1526 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1527 @fh.print(@board.to_s.gsub(/^/, "\'"))
1528 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1529 @result = GameResultWin.new(@next_player, @current_player)
1530 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1531 @monitors.each do |monitor|
1532 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1537 @current_player.status = "connected"
1538 @next_player.status = "connected"
1539 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1540 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1541 @fh.print(@board.to_s.gsub(/^/, "\'"))
1542 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1543 @result = GameResultWin.new(@next_player, @current_player)
1544 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1545 @monitors.each do |monitor|
1546 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1551 @current_player.status = "connected"
1552 @next_player.status = "connected"
1553 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1554 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1555 @fh.print(@board.to_s.gsub(/^/, "\'"))
1556 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1557 @result = GameResultWin.new(@next_player, @current_player)
1558 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1559 @monitors.each do |monitor|
1560 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1564 def oute_kaihimore_lose
1565 @current_player.status = "connected"
1566 @next_player.status = "connected"
1567 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1568 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1569 @fh.print(@board.to_s.gsub(/^/, "\'"))
1570 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1571 @result = GameResultWin.new(@next_player, @current_player)
1572 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1573 @monitors.each do |monitor|
1574 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1579 @current_player.status = "connected"
1580 @next_player.status = "connected"
1581 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1582 @next_player.write_safe("#TIME_UP\n#WIN\n")
1583 @fh.print(@board.to_s.gsub(/^/, "\'"))
1584 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1585 @result = GameResultWin.new(@next_player, @current_player)
1586 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1587 @monitors.each do |monitor|
1588 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1593 @current_player.status = "connected"
1594 @next_player.status = "connected"
1595 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1596 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1597 @fh.printf("%%KACHI\n")
1598 @fh.print(@board.to_s.gsub(/^/, "\'"))
1599 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1600 @result = GameResultWin.new(@current_player, @next_player)
1601 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1602 @monitors.each do |monitor|
1603 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1608 @current_player.status = "connected"
1609 @next_player.status = "connected"
1610 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1611 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1612 @fh.printf("%%KACHI\n")
1613 @fh.print(@board.to_s.gsub(/^/, "\'"))
1614 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1615 @result = GameResultWin.new(@next_player, @current_player)
1616 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1617 @monitors.each do |monitor|
1618 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1623 @current_player.status = "connected"
1624 @next_player.status = "connected"
1625 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1626 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1627 @fh.printf("%%TORYO\n")
1628 @fh.print(@board.to_s.gsub(/^/, "\'"))
1629 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1630 @result = GameResultWin.new(@next_player, @current_player)
1631 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1632 @monitors.each do |monitor|
1633 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1638 @current_player.status = "connected"
1639 @next_player.status = "connected"
1640 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1641 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1642 @fh.print(@board.to_s.gsub(/^/, "\'"))
1643 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1644 @result = GameResultWin.new(@current_player, @next_player)
1645 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1646 @monitors.each do |monitor|
1647 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1652 log_message(sprintf("game started %s", @id))
1653 @sente.write_safe(sprintf("START:%s\n", @id))
1654 @gote.write_safe(sprintf("START:%s\n", @id))
1655 @sente.mytime = @total_time
1656 @gote.mytime = @total_time
1657 @start_time = Time::new
1662 @fh = open(@logfile, "w")
1666 @fh.printf("N+%s\n", @sente.name)
1667 @fh.printf("N-%s\n", @gote.name)
1668 @fh.printf("$EVENT:%s\n", @id)
1670 @sente.write_safe(propose_message("+"))
1671 @gote.write_safe(propose_message("-"))
1673 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1675 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1676 P2 * -HI * * * * * -KA *
1677 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1678 P4 * * * * * * * * *
1679 P5 * * * * * * * * *
1680 P6 * * * * * * * * *
1681 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1682 P8 * +KA * * * * * +HI *
1683 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1692 Protocol_Version:1.1
1693 Protocol_Mode:Server
1695 Declaration:Jishogi 1.1
1697 Name+:#{@sente.name}
1703 Total_Time:#{@total_time}
1705 Least_Time_Per_Move:#{Least_Time_Per_Move}
1706 Remaining_Time+:#{@sente.mytime}
1707 Remaining_Time-:#{@gote.mytime}
1708 Last_Move:#{@last_move}
1709 Current_Turn:#{@current_turn}
1719 return str0 + @board.to_s + str1
1722 def propose_message(sg_flag)
1725 Protocol_Version:1.1
1726 Protocol_Mode:Server
1728 Declaration:Jishogi 1.1
1730 Name+:#{@sente.name}
1732 Your_Turn:#{sg_flag}
1737 Total_Time:#{@total_time}
1739 Least_Time_Per_Move:#{Least_Time_Per_Move}
1742 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1743 P2 * -HI * * * * * -KA *
1744 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1745 P4 * * * * * * * * *
1746 P5 * * * * * * * * *
1747 P6 * * * * * * * * *
1748 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1749 P8 * +KA * * * * * +HI *
1750 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1762 def issue_current_time
1763 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1764 @@mutex.synchronize do
1765 while time <= @@time do
1773 #################################################
1780 shogi-server - server for CSA server protocol
1783 shogi-server event_name port_number
1786 server for CSA server protocol
1790 specify filename for logging process ID
1793 this file is distributed under GPL version2 and might be compiled by Exerb
1805 def log_message(str)
1806 printf("%s message: %s\n", Time::new.to_s, str)
1809 def log_warning(str)
1810 printf("%s warning: %s\n", Time::new.to_s, str)
1814 printf("%s error: %s\n", Time::new.to_s, str)
1818 def parse_command_line
1820 parser = GetoptLong.new
1821 parser.ordering = GetoptLong::REQUIRE_ORDER
1823 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1827 parser.each_option do |name, arg|
1828 name.sub!(/^--/, '')
1829 options[name] = arg.dup
1833 raise parser.error_message
1838 def good_game_name?(str)
1839 if ((str =~ /^(.+)-\d+-\d+$/) &&
1840 (good_identifier?($1)))
1847 def good_identifier?(str)
1848 if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1850 elsif str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}},[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1857 def good_login?(str)
1859 if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1860 (tokens[0] == "LOGIN") &&
1861 (good_identifier?(tokens[1])))
1868 def write_pid_file(file)
1869 open(file, "w") do |fh|
1870 fh.print Process::pid, "\n"
1874 def mutex_watchdog(mutex, sec)
1886 log_error("mutex watchdog timeout")
1896 mutex_watchdog($mutex, 10)
1899 $options = parse_command_line
1900 if (ARGV.length != 2)
1905 LEAGUE.event = ARGV.shift
1908 write_pid_file($options["pid-file"]) if ($options["pid-file"])
1910 server = TCPserver.open(port)
1911 log_message("server started")
1914 Thread::start(server.accept) do |client|
1918 while (str = client.gets_timeout(Login_Time))
1923 if (good_login?(str))
1924 player = Player::new(str, client)
1925 if (LEAGUE.players[player.name])
1926 if ((LEAGUE.players[player.name].password == player.password) &&
1927 (LEAGUE.players[player.name].status != "game"))
1928 log_message(sprintf("user %s login forcely", player.name))
1929 LEAGUE.players[player.name].kill
1931 client.write_safe("LOGIN:incorrect" + eol)
1932 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1941 client.write_safe("LOGIN:incorrect" + eol)
1942 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1953 log_message(sprintf("user %s login", player.name))
1958 player.game.kill(player)
1960 player.finish # socket has been closed
1961 LEAGUE.delete(player)
1962 log_message(sprintf("user %s logout", player.name))
1971 LEAGUE = League::new