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']
127 player.rating_group = hash['rating_group']
134 @db["players"].each do |group, players|
144 @db.transaction(true) do
145 @db["players"].each do |group, players_hash|
146 players << players_hash.keys
149 return players.flatten.collect do |id|
160 # Idetifier of the player in the rating system
166 # Password of the player, which does not include a trip
167 attr_accessor :password
169 # Score in the rating sysem
172 # Group in the rating system
173 attr_accessor :rating_group
175 # Last timestamp when the rate was modified
176 attr_accessor :modified_at
186 @modified_at || Time.now
192 @modified_at = Time.now
202 simple_name = @name.gsub(/@.*?$/, '')
203 "%s+%s" % [simple_name, @trip[0..8]]
210 # Parses str in the LOGIN command, sets up @id and @trip
212 def set_password(str)
213 if str && !str.empty?
214 @password = str.strip
215 @id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
217 @id = @password = nil
224 def Login.good_login?(str)
226 if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
227 (tokens[0] == "LOGIN") &&
228 (good_identifier?(tokens[1])))
235 def Login.good_game_name?(str)
236 if ((str =~ /^(.+)-\d+-\d+$/) && (good_identifier?($1)))
243 def Login.good_identifier?(str)
244 if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
251 def Login.factory(str, player)
252 (login, player.name, password, ext) = str.chomp.split
254 return Loginx1.new(player, password)
256 return LoginCSA.new(player, password)
260 attr_reader :player, :csa_1st_str
262 def initialize(player, password)
265 parse_password(password)
269 @player.write_safe(sprintf("LOGIN:%s OK\n", @player.name))
272 def incorrect_duplicated_player(str)
273 @player.write_safe("LOGIN:incorrect\n")
274 @player.write_safe(sprintf("username %s is already connected\n", @player.name)) if (str.split.length >= 4)
275 sleep 3 # wait for sending the above messages.
276 @player.name = "%s [duplicated]" % [@player.name]
281 class LoginCSA < Login
284 def initialize(player, password)
287 @player.protocol = PROTOCOL
290 def parse_password(password)
291 if Login.good_game_name?(password)
293 @player.set_password(nil)
294 elsif password.split(",").size > 1
295 @gamename, *trip = password.split(",")
296 @player.set_password(trip.join(","))
298 @player.set_password(password)
299 @gamename = Default_Game_Name
301 @gamename = self.class.good_game_name?(@gamename) ? @gamename : Default_Game_Name
306 log_message(sprintf("user %s run in CSA mode", @player.name))
307 @csa_1st_str = "%%GAME #{@gamename} *"
311 class Loginx1 < Login
314 def initialize(player, password)
316 @player.protocol = PROTOCOL
319 def parse_password(password)
320 @player.set_password(password)
325 log_message(sprintf("user %s run in %s mode", @player.name, PROTOCOL))
326 @player.write_safe(sprintf("##[LOGIN] +OK %s\n", PROTOCOL))
331 class Player < BasicPlayer
332 def initialize(str, socket)
335 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
337 @protocol = nil # CSA or x1
338 @eol = "\m" # favorite eol code
341 @mytime = 0 # set in start method also
343 @write_queue = Queue::new
344 @main_thread = Thread::current
345 @writer_thread = Thread::start do
351 attr_accessor :socket, :status
352 attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
353 attr_accessor :main_thread, :writer_thread, :write_queue
356 log_message(sprintf("user %s killed", @name))
361 Thread::kill(@main_thread) if @main_thread
365 if (@status != "finished")
367 log_message(sprintf("user %s finish", @name))
368 # TODO you should confirm that there is no message in the queue.
369 Thread::kill(@writer_thread) if @writer_thread
371 @socket.close if (! @socket.closed?)
373 log_message(sprintf("user %s finish failed", @name))
379 @write_queue.push(str.gsub(/[\r\n]+/, @eol))
383 while (str = @write_queue.pop)
384 @socket.write_safe(str)
389 if ((status == "game_waiting") ||
390 (status == "start_waiting") ||
391 (status == "agree_waiting") ||
394 return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
395 elsif (@sente == false)
396 return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
397 elsif (@sente == nil)
398 return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
401 return sprintf("%s %s %s", @name, @protocol, @status)
406 @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
409 def run(csa_1st_str=nil)
410 while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
417 if (@write_queue.size > Max_Write_Queue_Size)
418 log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
422 if (@status == "finished")
425 str.chomp! if (str.class == String)
428 if (@status == "game")
429 array_str = str.split(",")
430 move = array_str.shift
431 additional = array_str.shift
432 if /^'(.*)/ =~ additional
433 comment = array_str.unshift("'*#{$1}")
435 s = @game.handle_one_move(move, self)
436 @game.fh.print("#{comment}\n") if (comment && !s)
437 return if (s && @protocol == LoginCSA::PROTOCOL)
439 when /^%[^%]/, :timeout
440 if (@status == "game")
441 s = @game.handle_one_move(str, self)
442 return if (s && @protocol == LoginCSA::PROTOCOL)
445 if (@status == "agree_waiting")
447 return if (@protocol == LoginCSA::PROTOCOL)
449 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
452 if (@status == "agree_waiting")
453 @status = "start_waiting"
454 if ((@game.sente.status == "start_waiting") &&
455 (@game.gote.status == "start_waiting"))
457 @game.sente.status = "game"
458 @game.gote.status = "game"
461 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
463 when /^%%SHOW\s+(\S+)/
465 if (LEAGUE.games[game_id])
466 write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
468 write_safe("##[SHOW] +OK\n")
469 when /^%%MONITORON\s+(\S+)/
471 if (LEAGUE.games[game_id])
472 LEAGUE.games[game_id].monitoron(self)
473 write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
474 write_safe("##[MONITOR][#{game_id}] +OK\n")
476 when /^%%MONITOROFF\s+(\S+)/
478 if (LEAGUE.games[game_id])
479 LEAGUE.games[game_id].monitoroff(self)
484 players = LEAGUE.rated_players
485 players.sort {|a,b| b.rate <=> a.rate}.each do |p|
486 write_safe("##[RATING] %s \t %4d @%s\n" %
487 [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
489 write_safe("##[RATING] +OK\n")
491 write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
492 write_safe("##[VERSION] +OK\n")
494 if ((@status == "connected") || (@status == "game_waiting"))
495 @status = "connected"
498 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
500 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
504 if (! Login::good_game_name?(game_name))
505 write_safe(sprintf("##[ERROR] bad game name\n"))
507 elsif ((@status == "connected") || (@status == "game_waiting"))
510 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
513 if ((my_sente_str == "*") ||
514 (my_sente_str == "+") ||
515 (my_sente_str == "-"))
518 write_safe(sprintf("##[ERROR] bad game option\n"))
522 if (my_sente_str == "*")
523 rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
524 elsif (my_sente_str == "+")
525 rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
526 elsif (my_sente_str == "-")
527 rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
532 @game_name = game_name
533 if ((my_sente_str == "*") && (rival.sente == nil))
541 elsif (rival.sente == true) # rival has higher priority
543 elsif (rival.sente == false)
545 elsif (my_sente_str == "+")
548 elsif (my_sente_str == "-")
554 Game::new(@game_name, self, rival)
555 self.status = "agree_waiting"
556 rival.status = "agree_waiting"
557 else # rival not found
558 if (command_name == "GAME")
559 @status = "game_waiting"
560 @game_name = game_name
561 if (my_sente_str == "+")
563 elsif (my_sente_str == "-")
569 write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
570 @status = "connected"
575 when /^%%CHAT\s+(.+)/
577 LEAGUE.players.each do |name, player|
578 if (player.protocol != LoginCSA::PROTOCOL)
579 player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message))
584 LEAGUE.games.each do |id, game|
585 buf.push(sprintf("##[LIST] %s\n", id))
587 buf.push("##[LIST] +OK\n")
591 LEAGUE.players.each do |name, player|
592 buf.push(sprintf("##[WHO] %s\n", player.to_s))
594 buf.push("##[WHO] +OK\n")
597 @status = "connected"
598 write_safe("LOGOUT:completed\n")
601 ## ignore null string
603 write_safe(sprintf("##[ERROR] unknown command %s\n", str))
613 PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
614 def initialize(board, x, y, sente, promoted=false)
621 if ((x == 0) || (y == 0))
623 hands = board.sente_hands
625 hands = board.gote_hands
632 @board.array[x][y] = self
635 attr_accessor :promoted, :sente, :x, :y, :board
637 def room_of_head?(x, y, name)
642 return adjacent_movable_grids + far_movable_grids
645 def far_movable_grids
650 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
651 if ((@board.array[x][y] == nil) || # dst is empty
652 (@board.array[x][y].sente != @sente)) # dst is enemy
660 if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
661 if (@board.array[x][y] == nil) # dst is empty?
668 def adjacent_movable_grids
671 moves = @promoted_moves
673 moves = @normal_moves
675 moves.each do |(dx, dy)|
682 if (jump_to?(cand_x, cand_y))
683 grids.push([cand_x, cand_y])
689 def move_to?(x, y, name)
690 return false if (! room_of_head?(x, y, name))
691 return false if ((name != @name) && (name != @promoted_name))
692 return false if (@promoted && (name != @promoted_name)) # can't un-promote
695 return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
697 return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
699 return false if ((6 >= @y) && (6 >= y) && (name != @name))
703 if ((@x == 0) || (@y == 0))
704 return jump_to?(x, y)
706 return movable_grids.include?([x, y])
711 if ((@x == 0) || (@y == 0))
713 @board.sente_hands.delete(self)
715 @board.gote_hands.delete(self)
717 @board.array[x][y] = self
718 elsif ((x == 0) || (y == 0))
719 @promoted = false # clear promoted flag before moving to hands
721 @board.sente_hands.push(self)
723 @board.gote_hands.push(self)
725 @board.array[@x][@y] = nil
727 @board.array[@x][@y] = nil
728 @board.array[x][y] = self
761 class PieceFU < Piece
764 @normal_moves = [[0, +1]]
765 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
767 @promoted_name = "TO"
770 def room_of_head?(x, y, name)
773 return false if (y == 1)
775 return false if (y == 9)
781 if ((iy != @y) && # not source position
782 @board.array[x][iy] &&
783 (@board.array[x][iy].sente == @sente) && # mine
784 (@board.array[x][iy].name == "FU") &&
785 (@board.array[x][iy].promoted == false))
795 class PieceKY < Piece
799 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
801 @promoted_name = "NY"
804 def room_of_head?(x, y, name)
807 return false if (y == 1)
809 return false if (y == 9)
814 def far_movable_grids
822 while (jump_to?(cand_x, cand_y))
823 grids.push([cand_x, cand_y])
824 break if (! put_to?(cand_x, cand_y))
830 while (jump_to?(cand_x, cand_y))
831 grids.push([cand_x, cand_y])
832 break if (! put_to?(cand_x, cand_y))
840 class PieceKE < Piece
843 @normal_moves = [[+1, +2], [-1, +2]]
844 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
846 @promoted_name = "NK"
849 def room_of_head?(x, y, name)
852 return false if ((y == 1) || (y == 2))
854 return false if ((y == 9) || (y == 8))
860 class PieceGI < Piece
863 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
864 @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
866 @promoted_name = "NG"
870 class PieceKI < Piece
873 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
880 class PieceKA < Piece
884 @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
886 @promoted_name = "UM"
889 def far_movable_grids
894 while (jump_to?(cand_x, cand_y))
895 grids.push([cand_x, cand_y])
896 break if (! put_to?(cand_x, cand_y))
903 while (jump_to?(cand_x, cand_y))
904 grids.push([cand_x, cand_y])
905 break if (! put_to?(cand_x, cand_y))
912 while (jump_to?(cand_x, cand_y))
913 grids.push([cand_x, cand_y])
914 break if (! put_to?(cand_x, cand_y))
921 while (jump_to?(cand_x, cand_y))
922 grids.push([cand_x, cand_y])
923 break if (! put_to?(cand_x, cand_y))
930 class PieceHI < Piece
934 @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
936 @promoted_name = "RY"
939 def far_movable_grids
944 while (jump_to?(cand_x, cand_y))
945 grids.push([cand_x, cand_y])
946 break if (! put_to?(cand_x, cand_y))
952 while (jump_to?(cand_x, cand_y))
953 grids.push([cand_x, cand_y])
954 break if (! put_to?(cand_x, cand_y))
960 while (jump_to?(cand_x, cand_y))
961 grids.push([cand_x, cand_y])
962 break if (! put_to?(cand_x, cand_y))
968 while (jump_to?(cand_x, cand_y))
969 grids.push([cand_x, cand_y])
970 break if (! put_to?(cand_x, cand_y))
976 class PieceOU < Piece
979 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
989 @sente_hands = Array::new
990 @gote_hands = Array::new
992 @sente_history = Hash::new
993 @gote_history = Hash::new
994 @array = [[], [], [], [], [], [], [], [], [], []]
997 attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
998 attr_reader :move_count
1001 PieceKY::new(self, 1, 1, false)
1002 PieceKE::new(self, 2, 1, false)
1003 PieceGI::new(self, 3, 1, false)
1004 PieceKI::new(self, 4, 1, false)
1005 PieceOU::new(self, 5, 1, false)
1006 PieceKI::new(self, 6, 1, false)
1007 PieceGI::new(self, 7, 1, false)
1008 PieceKE::new(self, 8, 1, false)
1009 PieceKY::new(self, 9, 1, false)
1010 PieceKA::new(self, 2, 2, false)
1011 PieceHI::new(self, 8, 2, false)
1012 PieceFU::new(self, 1, 3, false)
1013 PieceFU::new(self, 2, 3, false)
1014 PieceFU::new(self, 3, 3, false)
1015 PieceFU::new(self, 4, 3, false)
1016 PieceFU::new(self, 5, 3, false)
1017 PieceFU::new(self, 6, 3, false)
1018 PieceFU::new(self, 7, 3, false)
1019 PieceFU::new(self, 8, 3, false)
1020 PieceFU::new(self, 9, 3, false)
1022 PieceKY::new(self, 1, 9, true)
1023 PieceKE::new(self, 2, 9, true)
1024 PieceGI::new(self, 3, 9, true)
1025 PieceKI::new(self, 4, 9, true)
1026 PieceOU::new(self, 5, 9, true)
1027 PieceKI::new(self, 6, 9, true)
1028 PieceGI::new(self, 7, 9, true)
1029 PieceKE::new(self, 8, 9, true)
1030 PieceKY::new(self, 9, 9, true)
1031 PieceKA::new(self, 8, 8, true)
1032 PieceHI::new(self, 2, 8, true)
1033 PieceFU::new(self, 1, 7, true)
1034 PieceFU::new(self, 2, 7, true)
1035 PieceFU::new(self, 3, 7, true)
1036 PieceFU::new(self, 4, 7, true)
1037 PieceFU::new(self, 5, 7, true)
1038 PieceFU::new(self, 6, 7, true)
1039 PieceFU::new(self, 7, 7, true)
1040 PieceFU::new(self, 8, 7, true)
1041 PieceFU::new(self, 9, 7, true)
1044 def have_piece?(hands, name)
1045 piece = hands.find { |i|
1051 def move_to(x0, y0, x1, y1, name, sente)
1053 hands = @sente_hands
1058 if ((x0 == 0) || (y0 == 0))
1059 piece = have_piece?(hands, name)
1060 return :illegal if (! piece.move_to?(x1, y1, name))
1061 piece.move_to(x1, y1)
1063 return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))
1064 if (@array[x0][y0].name != name) # promoted ?
1065 @array[x0][y0].promoted = true
1068 if (@array[x1][y1].name == "OU")
1069 return :outori # return board update
1071 @array[x1][y1].sente = @array[x0][y0].sente
1072 @array[x1][y1].move_to(0, 0)
1077 @array[x0][y0].move_to(x1, y1)
1083 def look_for_ou(sente)
1089 (@array[x][y].name == "OU") &&
1090 (@array[x][y].sente == sente))
1097 raise "can't find ou"
1100 def checkmated?(sente) # sente is loosing
1101 ou = look_for_ou(sente)
1107 (@array[x][y].sente != sente))
1108 if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1119 def uchifuzume?(sente)
1120 rival_ou = look_for_ou(! sente) # rival's ou
1121 if (sente) # rival is gote
1122 if ((rival_ou.y != 9) &&
1123 (@array[rival_ou.x][rival_ou.y + 1]) &&
1124 (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1125 (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1127 fu_y = rival_ou.y + 1
1132 if ((rival_ou.y != 0) &&
1133 (@array[rival_ou.x][rival_ou.y - 1]) &&
1134 (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1135 (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1137 fu_y = rival_ou.y - 1
1143 ## case: rival_ou is moving
1145 rival_ou.movable_grids.each do |(cand_x, cand_y)|
1146 tmp_board = Marshal.load(Marshal.dump(self))
1147 s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1148 raise "internal error" if (s != true)
1149 if (! tmp_board.checkmated?(! sente)) # good move
1154 ## case: rival is capturing fu
1160 (@array[x][y].sente != sente) &&
1161 @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1162 if (@array[x][y].promoted)
1163 name = @array[x][y].promoted_name
1165 name = @array[x][y].name
1167 tmp_board = Marshal.load(Marshal.dump(self))
1168 s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1169 raise "internal error" if (s != true)
1170 if (! tmp_board.checkmated?(! sente)) # good move
1181 def oute_sennichite?(sente)
1182 if (checkmated?(! sente))
1185 if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
1189 if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
1197 def sennichite?(sente)
1199 if (@history[str] && (@history[str] >= 3)) # already 3 times
1205 def good_kachi?(sente)
1206 if (checkmated?(sente))
1207 puts "'NG: Checkmating." if $DEBUG
1211 ou = look_for_ou(sente)
1212 if (sente && (ou.y >= 4))
1213 puts "'NG: Black's OU does not enter yet." if $DEBUG
1216 if (! sente && (ou.y <= 6))
1217 puts "'NG: White's OU does not enter yet." if $DEBUG
1225 hands = @sente_hands
1235 (@array[x][y].sente == sente) &&
1236 (@array[x][y].point > 0))
1237 point = point + @array[x][y].point
1243 hands.each do |piece|
1244 point = point + piece.point
1248 puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1253 puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1258 puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1263 puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1267 def handle_one_move(str, sente=nil)
1268 if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1275 elsif (str =~ /^%KACHI/)
1276 raise ArgumentError, "sente is null", caller if sente == nil
1277 if (good_kachi?(sente))
1282 elsif (str =~ /^%TORYO/)
1288 if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1289 ((x0 != 0) || (y0 != 0)))
1291 elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1297 hands = @sente_hands
1304 if ((x0 == 0) && (y0 == 0))
1305 return :illegal if (! have_piece?(hands, name))
1306 elsif (! @array[x0][y0])
1307 return :illegal # no piece
1308 elsif (@array[x0][y0].sente != sente)
1309 return :illegal # this is not mine
1310 elsif (@array[x0][y0].name != name)
1311 return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1314 ## destination check
1315 if (@array[x1][y1] &&
1316 (@array[x1][y1].sente == sente)) # can't capture mine
1318 elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1319 return :illegal # can't put on existing piece
1322 tmp_board = Marshal.load(Marshal.dump(self))
1323 return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1324 return :oute_kaihimore if (tmp_board.checkmated?(sente))
1325 return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1326 return :sennichite if tmp_board.sennichite?(sente)
1328 if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1332 move_to(x0, y0, x1, y1, name, sente)
1335 if (checkmated?(! sente))
1337 @sente_history[str] = (@sente_history[str] || 0) + 1
1339 @gote_history[str] = (@gote_history[str] || 0) + 1
1343 @sente_history.clear
1348 @history[str] = (@history[str] || 0) + 1
1356 a.push(sprintf("P%d", y))
1359 piece = @array[x][y]
1368 a.push(sprintf("\n"))
1371 if (! sente_hands.empty?)
1373 sente_hands.each do |p|
1374 a.push("00" + p.name)
1378 if (! gote_hands.empty?)
1380 gote_hands.each do |p|
1381 a.push("00" + p.name)
1391 attr_reader :players, :black, :white
1393 def initialize(p1, p2)
1397 if p1.sente && !p2.sente
1398 @black, @white = p1, p2
1399 elsif !p1.sente && p2.sente
1400 @black, @white = p2, p1
1402 raise "Never reached!"
1407 class GameResultWin < GameResult
1408 attr_reader :winner, :loser
1410 def initialize(winner, loser)
1412 @winner, @loser = winner, loser
1416 black_name = @black.id || @black.name
1417 white_name = @white.id || @white.name
1418 "%s:%s" % [black_name, white_name]
1422 class GameResultDraw < GameResult
1430 def initialize(game_name, player0, player1)
1431 @monitors = Array::new
1432 @game_name = game_name
1433 if (@game_name =~ /-(\d+)-(\d+)$/)
1434 @total_time = $1.to_i
1445 @current_player = @sente
1446 @next_player = @gote
1454 @sente.status = "agree_waiting"
1455 @gote.status = "agree_waiting"
1457 @id = sprintf("%s+%s+%s+%s+%s",
1458 LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1459 @logfile = @id + ".csa"
1461 LEAGUE.games[@id] = self
1463 log_message(sprintf("game created %s", @id))
1473 attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1474 attr_accessor :last_move, :current_turn
1478 @sente.rated? && @gote.rated?
1481 def monitoron(monitor)
1482 @monitors.delete(monitor)
1483 @monitors.push(monitor)
1486 def monitoroff(monitor)
1487 @monitors.delete(monitor)
1490 def reject(rejector)
1491 @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1492 @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1497 if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1499 elsif (@current_player == killer)
1506 log_message(sprintf("game finished %s", @id))
1507 @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1512 @sente.status = "connected"
1513 @gote.status = "connected"
1515 if (@current_player.protocol == LoginCSA::PROTOCOL)
1516 @current_player.finish
1518 if (@next_player.protocol == LoginCSA::PROTOCOL)
1521 @monitors = Array::new
1524 @current_player = nil
1526 LEAGUE.games.delete(@id)
1529 def handle_one_move(str, player)
1531 if (@current_player == player)
1532 @end_time = Time::new
1533 t = (@end_time - @start_time).floor
1534 t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1537 if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1539 elsif (str == :timeout)
1540 return false # time isn't expired. players aren't swapped. continue game
1542 @current_player.mytime = @current_player.mytime - t
1543 if (@current_player.mytime < 0)
1544 @current_player.mytime = 0
1548 move_status = @board.handle_one_move(str, @sente == @current_player)
1550 # log_error("handle_one_move raise exception for #{str}")
1551 # move_status = :illegal
1554 if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1555 @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1557 if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1558 @sente.write_safe(sprintf("%s,T%d\n", str, t))
1559 @gote.write_safe(sprintf("%s,T%d\n", str, t))
1560 @fh.printf("%s\nT%d\n", str, t)
1561 @last_move = sprintf("%s,T%d", str, t)
1562 @current_turn = @current_turn + 1
1565 @monitors.each do |monitor|
1566 monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1567 monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1572 if (@next_player.status != "game") # rival is logout or disconnected
1574 elsif (status == :timeout)
1576 elsif (move_status == :illegal)
1578 elsif (move_status == :kachi_win)
1580 elsif (move_status == :kachi_lose)
1582 elsif (move_status == :toryo)
1584 elsif (move_status == :outori)
1586 elsif (move_status == :sennichite)
1588 elsif (move_status == :oute_sennichite)
1589 oute_sennichite_lose()
1590 elsif (move_status == :uchifuzume)
1592 elsif (move_status == :oute_kaihimore)
1593 oute_kaihimore_lose()
1597 finish() if finish_flag
1598 (@current_player, @next_player) = [@next_player, @current_player]
1599 @start_time = Time::new
1605 @current_player.status = "connected"
1606 @next_player.status = "connected"
1607 @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1608 @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1609 @fh.printf("%%TORYO\n")
1610 @fh.print(@board.to_s.gsub(/^/, "\'"))
1611 @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1612 @result = GameResultWin.new(@current_player, @next_player)
1613 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1614 @monitors.each do |monitor|
1615 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1620 @current_player.status = "connected"
1621 @next_player.status = "connected"
1622 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1623 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1624 @fh.printf("%%TORYO\n")
1625 @fh.print(@board.to_s.gsub(/^/, "\'"))
1626 @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1627 @result = GameResultWin.new(@next_player, @current_player)
1628 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1629 @monitors.each do |monitor|
1630 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1635 @current_player.status = "connected"
1636 @next_player.status = "connected"
1637 @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1638 @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1639 @fh.print(@board.to_s.gsub(/^/, "\'"))
1640 @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1641 @result = GameResultDraw.new(@current_player, @next_player)
1642 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1643 @monitors.each do |monitor|
1644 monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1648 def oute_sennichite_lose
1649 @current_player.status = "connected"
1650 @next_player.status = "connected"
1651 @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1652 @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1653 @fh.print(@board.to_s.gsub(/^/, "\'"))
1654 @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1655 @result = GameResultWin.new(@next_player, @current_player)
1656 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1657 @monitors.each do |monitor|
1658 monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1663 @current_player.status = "connected"
1664 @next_player.status = "connected"
1665 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1666 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1667 @fh.print(@board.to_s.gsub(/^/, "\'"))
1668 @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1669 @result = GameResultWin.new(@next_player, @current_player)
1670 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1671 @monitors.each do |monitor|
1672 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1677 @current_player.status = "connected"
1678 @next_player.status = "connected"
1679 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1680 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1681 @fh.print(@board.to_s.gsub(/^/, "\'"))
1682 @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1683 @result = GameResultWin.new(@next_player, @current_player)
1684 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1685 @monitors.each do |monitor|
1686 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1690 def oute_kaihimore_lose
1691 @current_player.status = "connected"
1692 @next_player.status = "connected"
1693 @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1694 @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1695 @fh.print(@board.to_s.gsub(/^/, "\'"))
1696 @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1697 @result = GameResultWin.new(@next_player, @current_player)
1698 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1699 @monitors.each do |monitor|
1700 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1705 @current_player.status = "connected"
1706 @next_player.status = "connected"
1707 @current_player.write_safe("#TIME_UP\n#LOSE\n")
1708 @next_player.write_safe("#TIME_UP\n#WIN\n")
1709 @fh.print(@board.to_s.gsub(/^/, "\'"))
1710 @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1711 @result = GameResultWin.new(@next_player, @current_player)
1712 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1713 @monitors.each do |monitor|
1714 monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1719 @current_player.status = "connected"
1720 @next_player.status = "connected"
1721 @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1722 @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1723 @fh.printf("%%KACHI\n")
1724 @fh.print(@board.to_s.gsub(/^/, "\'"))
1725 @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1726 @result = GameResultWin.new(@current_player, @next_player)
1727 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1728 @monitors.each do |monitor|
1729 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1734 @current_player.status = "connected"
1735 @next_player.status = "connected"
1736 @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1737 @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1738 @fh.printf("%%KACHI\n")
1739 @fh.print(@board.to_s.gsub(/^/, "\'"))
1740 @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1741 @result = GameResultWin.new(@next_player, @current_player)
1742 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1743 @monitors.each do |monitor|
1744 monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1749 @current_player.status = "connected"
1750 @next_player.status = "connected"
1751 @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1752 @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1753 @fh.printf("%%TORYO\n")
1754 @fh.print(@board.to_s.gsub(/^/, "\'"))
1755 @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1756 @result = GameResultWin.new(@next_player, @current_player)
1757 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1758 @monitors.each do |monitor|
1759 monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1764 @current_player.status = "connected"
1765 @next_player.status = "connected"
1766 @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1767 @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1768 @fh.print(@board.to_s.gsub(/^/, "\'"))
1769 @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1770 @result = GameResultWin.new(@current_player, @next_player)
1771 @fh.printf("'rating:#{@result.to_s}\n") if rated?
1772 @monitors.each do |monitor|
1773 monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1778 log_message(sprintf("game started %s", @id))
1779 @sente.write_safe(sprintf("START:%s\n", @id))
1780 @gote.write_safe(sprintf("START:%s\n", @id))
1781 @sente.mytime = @total_time
1782 @gote.mytime = @total_time
1783 @start_time = Time::new
1788 @fh = open(@logfile, "w")
1792 @fh.printf("N+%s\n", @sente.name)
1793 @fh.printf("N-%s\n", @gote.name)
1794 @fh.printf("$EVENT:%s\n", @id)
1796 @sente.write_safe(propose_message("+"))
1797 @gote.write_safe(propose_message("-"))
1799 @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1801 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1802 P2 * -HI * * * * * -KA *
1803 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1804 P4 * * * * * * * * *
1805 P5 * * * * * * * * *
1806 P6 * * * * * * * * *
1807 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1808 P8 * +KA * * * * * +HI *
1809 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1818 Protocol_Version:1.1
1819 Protocol_Mode:Server
1821 Declaration:Jishogi 1.1
1823 Name+:#{@sente.name}
1829 Total_Time:#{@total_time}
1831 Least_Time_Per_Move:#{Least_Time_Per_Move}
1832 Remaining_Time+:#{@sente.mytime}
1833 Remaining_Time-:#{@gote.mytime}
1834 Last_Move:#{@last_move}
1835 Current_Turn:#{@current_turn}
1845 return str0 + @board.to_s + str1
1848 def propose_message(sg_flag)
1851 Protocol_Version:1.1
1852 Protocol_Mode:Server
1854 Declaration:Jishogi 1.1
1856 Name+:#{@sente.name}
1858 Your_Turn:#{sg_flag}
1863 Total_Time:#{@total_time}
1865 Least_Time_Per_Move:#{Least_Time_Per_Move}
1868 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1869 P2 * -HI * * * * * -KA *
1870 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1871 P4 * * * * * * * * *
1872 P5 * * * * * * * * *
1873 P6 * * * * * * * * *
1874 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1875 P8 * +KA * * * * * +HI *
1876 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1888 def issue_current_time
1889 time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1890 @@mutex.synchronize do
1891 while time <= @@time do
1899 #################################################
1906 shogi-server - server for CSA server protocol
1909 shogi-server event_name port_number
1912 server for CSA server protocol
1916 specify filename for logging process ID
1919 this file is distributed under GPL version2 and might be compiled by Exerb
1931 def log_message(str)
1932 printf("%s message: %s\n", Time::new.to_s, str)
1935 def log_warning(str)
1936 printf("%s warning: %s\n", Time::new.to_s, str)
1940 printf("%s error: %s\n", Time::new.to_s, str)
1944 def parse_command_line
1946 parser = GetoptLong.new
1947 parser.ordering = GetoptLong::REQUIRE_ORDER
1949 ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1953 parser.each_option do |name, arg|
1954 name.sub!(/^--/, '')
1955 options[name] = arg.dup
1959 raise parser.error_message
1964 def write_pid_file(file)
1965 open(file, "w") do |fh|
1966 fh.print Process::pid, "\n"
1970 def mutex_watchdog(mutex, sec)
1982 log_error("mutex watchdog timeout")
1992 mutex_watchdog($mutex, 10)
1995 $options = parse_command_line
1996 if (ARGV.length != 2)
2001 LEAGUE.event = ARGV.shift
2004 write_pid_file($options["pid-file"]) if ($options["pid-file"])
2006 server = TCPserver.open(port)
2007 log_message("server started")
2010 Thread::start(server.accept) do |client|
2015 while (str = client.gets_timeout(Login_Time))
2020 if (Login::good_login?(str))
2021 player = Player::new(str, client)
2023 login = Login::factory(str, player)
2024 if (LEAGUE.players[player.name])
2025 if ((LEAGUE.players[player.name].password == player.password) &&
2026 (LEAGUE.players[player.name].status != "game"))
2027 log_message(sprintf("user %s login forcely", player.name))
2028 LEAGUE.players[player.name].kill
2030 login.incorrect_duplicated_player(str)
2038 client.write_safe("LOGIN:incorrect" + eol)
2039 client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2050 log_message(sprintf("user %s login", player.name))
2052 player.run(login.csa_1st_str)
2056 player.game.kill(player)
2058 player.finish # socket has been closed
2059 LEAGUE.delete(player)
2060 log_message(sprintf("user %s logout", player.name))
2069 LEAGUE = League::new