#! /usr/bin/env ruby ## $Id$ ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch) ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA require 'getoptlong' require 'thread' require 'timeout' require 'socket' require 'yaml' require 'yaml/store' require 'digest/md5' class TCPSocket def gets_timeout(t = Default_Timeout) begin timeout(t) do return self.gets end rescue TimeoutError return nil rescue return nil end end def gets_safe(t = nil) if (t && t > 0) begin timeout(t) do return self.gets end rescue TimeoutError return :timeout rescue return nil end else begin return self.gets rescue return nil end end end def write_safe(str) begin return self.write(str) rescue return nil end end end module ShogiServer # for a namespace Max_Write_Queue_Size = 1000 Max_Identifier_Length = 32 Default_Timeout = 60 # for single socket operation Default_Game_Name = "default-1500-0" One_Time = 10 Least_Time_Per_Move = 1 Login_Time = 300 # time for LOGIN Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.') Release.concat("-") if (Release == "") Revision = "$Revision$".gsub(/[^\.\d]/, '') class League def initialize @games = Hash::new @players = Hash::new @event = nil @db = YAML::Store.new( File.join(File.dirname(__FILE__), "players.yaml") ) end attr_accessor :players, :games, :event def add(player) self.load(player) if player.id @players[player.name] = player end def delete(player) @players.delete(player.name) end def get_player(status, game_name, sente, searcher=nil) @players.each do |name, player| if ((player.status == status) && (player.game_name == game_name) && ((sente == nil) || (player.sente == nil) || (player.sente == sente)) && ((searcher == nil) || (player != searcher))) return player end end return nil end def load(player) hash = search(player.id) if hash # a current user player.name = hash['name'] player.rate = hash['rate'] player.modified_at = hash['last_modified'] player.rating_group = hash['rating_group'] end end def search(id) hash = nil @db.transaction do @db["players"].each do |group, players| hash = players[id] break if hash end end hash end def rated_players players = [] @db.transaction(true) do @db["players"].each do |group, players_hash| players << players_hash.keys end end return players.flatten.collect do |id| p = BasicPlayer.new p.id = id self.load(p) p end end end ###################################################### # Processes the LOGIN command. # class Login def Login.good_login?(str) tokens = str.split if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) && (tokens[0] == "LOGIN") && (good_identifier?(tokens[1]))) return true else return false end end def Login.good_game_name?(str) if ((str =~ /^(.+)-\d+-\d+$/) && (good_identifier?($1))) return true else return false end end def Login.good_identifier?(str) if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/ return true else return false end end def Login.factory(str, player) (login, player.name, password, ext) = str.chomp.split if (ext) return Loginx1.new(player, password) else return LoginCSA.new(player, password) end end attr_reader :player # the first command that will be executed just after LOGIN. # If it is nil, the default process will be started. attr_reader :csa_1st_str def initialize(player, password) @player = player @csa_1st_str = nil parse_password(password) end def process @player.write_safe(sprintf("LOGIN:%s OK\n", @player.name)) log_message(sprintf("user %s run in %s mode", @player.name, @player.protocol)) end def incorrect_duplicated_player(str) @player.write_safe("LOGIN:incorrect\n") @player.write_safe(sprintf("username %s is already connected\n", @player.name)) if (str.split.length >= 4) sleep 3 # wait for sending the above messages. @player.name = "%s [duplicated]" % [@player.name] @player.finish end end ###################################################### # Processes LOGIN for the CSA standard mode. # class LoginCSA < Login PROTOCOL = "CSA" def initialize(player, password) @gamename = nil super @player.protocol = PROTOCOL end def parse_password(password) if Login.good_game_name?(password) @gamename = password @player.set_password(nil) elsif password.split(",").size > 1 @gamename, *trip = password.split(",") @player.set_password(trip.join(",")) else @player.set_password(password) @gamename = Default_Game_Name end @gamename = self.class.good_game_name?(@gamename) ? @gamename : Default_Game_Name end def process super @csa_1st_str = "%%GAME #{@gamename} *" end end ###################################################### # Processes LOGIN for the extented mode. # class Loginx1 < Login PROTOCOL = "x1" def initialize(player, password) super @player.protocol = PROTOCOL end def parse_password(password) @player.set_password(password) end def process super @player.write_safe(sprintf("##[LOGIN] +OK %s\n", PROTOCOL)) end end class BasicPlayer # Idetifier of the player in the rating system attr_accessor :id # Name of the player attr_accessor :name # Password of the player, which does not include a trip attr_accessor :password # Score in the rating sysem attr_accessor :rate # Group in the rating system attr_accessor :rating_group # Last timestamp when the rate was modified attr_accessor :modified_at def initialize @name = nil @password = nil end def modified_at @modified_at || Time.now end def rate=(new_rate) if @rate != new_rate @rate = new_rate @modified_at = Time.now end end def rated? @id != nil end def simple_id if @trip simple_name = @name.gsub(/@.*?$/, '') "%s+%s" % [simple_name, @trip[0..8]] else @name end end ## # Parses str in the LOGIN command, sets up @id and @trip # def set_password(str) if str && !str.empty? @password = str.strip @id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)] else @id = @password = nil end end end class Player < BasicPlayer def initialize(str, socket) super() @socket = socket @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished @protocol = nil # CSA or x1 @eol = "\m" # favorite eol code @game = nil @game_name = "" @mytime = 0 # set in start method also @sente = nil @write_queue = Queue::new @main_thread = Thread::current @writer_thread = Thread::start do Thread.pass writer() end end attr_accessor :socket, :status attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente attr_accessor :main_thread, :writer_thread, :write_queue def kill log_message(sprintf("user %s killed", @name)) if (@game) @game.kill(self) end finish Thread::kill(@main_thread) if @main_thread end def finish if (@status != "finished") @status = "finished" log_message(sprintf("user %s finish", @name)) # TODO you should confirm that there is no message in the queue. Thread::kill(@writer_thread) if @writer_thread begin @socket.close if (! @socket.closed?) rescue log_message(sprintf("user %s finish failed", @name)) end end end def write_safe(str) @write_queue.push(str.gsub(/[\r\n]+/, @eol)) end def writer while (str = @write_queue.pop) @socket.write_safe(str) end end def to_s if ((status == "game_waiting") || (status == "start_waiting") || (status == "agree_waiting") || (status == "game")) if (@sente) return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name) elsif (@sente == false) return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name) elsif (@sente == nil) return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name) end else return sprintf("%s %s %s", @name, @protocol, @status) end end def write_help @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"') end def run(csa_1st_str=nil) while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout))) begin $mutex.lock if (csa_1st_str) str = csa_1st_str csa_1st_str = nil end if (@write_queue.size > Max_Write_Queue_Size) log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size)) return end if (@status == "finished") return end str.chomp! if (str.class == String) case str when /^[\+\-][^%]/ if (@status == "game") array_str = str.split(",") move = array_str.shift additional = array_str.shift if /^'(.*)/ =~ additional comment = array_str.unshift("'*#{$1}") end s = @game.handle_one_move(move, self) @game.fh.print("#{comment}\n") if (comment && !s) return if (s && @protocol == LoginCSA::PROTOCOL) end when /^%[^%]/, :timeout if (@status == "game") s = @game.handle_one_move(str, self) return if (s && @protocol == LoginCSA::PROTOCOL) end when /^REJECT/ if (@status == "agree_waiting") @game.reject(@name) return if (@protocol == LoginCSA::PROTOCOL) else write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status)) end when /^AGREE/ if (@status == "agree_waiting") @status = "start_waiting" if ((@game.sente.status == "start_waiting") && (@game.gote.status == "start_waiting")) @game.start @game.sente.status = "game" @game.gote.status = "game" end else write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status)) end when /^%%SHOW\s+(\S+)/ game_id = $1 if (LEAGUE.games[game_id]) write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] ')) end write_safe("##[SHOW] +OK\n") when /^%%MONITORON\s+(\S+)/ game_id = $1 if (LEAGUE.games[game_id]) LEAGUE.games[game_id].monitoron(self) write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] ")) write_safe("##[MONITOR][#{game_id}] +OK\n") end when /^%%MONITOROFF\s+(\S+)/ game_id = $1 if (LEAGUE.games[game_id]) LEAGUE.games[game_id].monitoroff(self) end when /^%%HELP/ write_help when /^%%RATING/ players = LEAGUE.rated_players players.sort {|a,b| b.rate <=> a.rate}.each do |p| write_safe("##[RATING] %s \t %4d @%s\n" % [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")]) end write_safe("##[RATING] +OK\n") when /^%%VERSION/ write_safe "##[VERSION] Shogi Server revision #{Revision}\n" write_safe("##[VERSION] +OK\n") when /^%%GAME\s*$/ if ((@status == "connected") || (@status == "game_waiting")) @status = "connected" @game_name = "" else write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status)) end when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/ command_name = $1 game_name = $2 my_sente_str = $3 if (! Login::good_game_name?(game_name)) write_safe(sprintf("##[ERROR] bad game name\n")) next elsif ((@status == "connected") || (@status == "game_waiting")) ## continue else write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status)) next end if ((my_sente_str == "*") || (my_sente_str == "+") || (my_sente_str == "-")) ## ok else write_safe(sprintf("##[ERROR] bad game option\n")) next end if (my_sente_str == "*") rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference elsif (my_sente_str == "+") rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote elsif (my_sente_str == "-") rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente else ## never reached end if (rival) @game_name = game_name if ((my_sente_str == "*") && (rival.sente == nil)) if (rand(2) == 0) @sente = true rival.sente = false else @sente = false rival.sente = true end elsif (rival.sente == true) # rival has higher priority @sente = false elsif (rival.sente == false) @sente = true elsif (my_sente_str == "+") @sente = true rival.sente = false elsif (my_sente_str == "-") @sente = false rival.sente = true else ## never reached end Game::new(@game_name, self, rival) self.status = "agree_waiting" rival.status = "agree_waiting" else # rival not found if (command_name == "GAME") @status = "game_waiting" @game_name = game_name if (my_sente_str == "+") @sente = true elsif (my_sente_str == "-") @sente = false else @sente = nil end else # challenge write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name)) @status = "connected" @game_name = "" @sente = nil end end when /^%%CHAT\s+(.+)/ message = $1 LEAGUE.players.each do |name, player| if (player.protocol != LoginCSA::PROTOCOL) player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) end end when /^%%LIST/ buf = Array::new LEAGUE.games.each do |id, game| buf.push(sprintf("##[LIST] %s\n", id)) end buf.push("##[LIST] +OK\n") write_safe(buf.join) when /^%%WHO/ buf = Array::new LEAGUE.players.each do |name, player| buf.push(sprintf("##[WHO] %s\n", player.to_s)) end buf.push("##[WHO] +OK\n") write_safe(buf.join) when /^LOGOUT/ @status = "connected" write_safe("LOGOUT:completed\n") return when /^\s*$/ ## ignore null string else write_safe(sprintf("##[ERROR] unknown command %s\n", str)) end ensure $mutex.unlock end end # enf of while end end class Piece PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"} def initialize(board, x, y, sente, promoted=false) @board = board @x = x @y = y @sente = sente @promoted = promoted if ((x == 0) || (y == 0)) if (sente) hands = board.sente_hands else hands = board.gote_hands end hands.push(self) hands.sort! {|a, b| a.name <=> b.name } else @board.array[x][y] = self end end attr_accessor :promoted, :sente, :x, :y, :board def room_of_head?(x, y, name) true end def movable_grids return adjacent_movable_grids + far_movable_grids end def far_movable_grids return [] end def jump_to?(x, y) if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9)) if ((@board.array[x][y] == nil) || # dst is empty (@board.array[x][y].sente != @sente)) # dst is enemy return true end end return false end def put_to?(x, y) if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9)) if (@board.array[x][y] == nil) # dst is empty? return true end end return false end def adjacent_movable_grids grids = Array::new if (@promoted) moves = @promoted_moves else moves = @normal_moves end moves.each do |(dx, dy)| if (@sente) cand_y = @y - dy else cand_y = @y + dy end cand_x = @x + dx if (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) end end return grids end def move_to?(x, y, name) return false if (! room_of_head?(x, y, name)) return false if ((name != @name) && (name != @promoted_name)) return false if (@promoted && (name != @promoted_name)) # can't un-promote if (! @promoted) return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece if (@sente) return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote else return false if ((6 >= @y) && (6 >= y) && (name != @name)) end end if ((@x == 0) || (@y == 0)) return jump_to?(x, y) else return movable_grids.include?([x, y]) end end def move_to(x, y) if ((@x == 0) || (@y == 0)) if (@sente) @board.sente_hands.delete(self) else @board.gote_hands.delete(self) end @board.array[x][y] = self elsif ((x == 0) || (y == 0)) @promoted = false # clear promoted flag before moving to hands if (@sente) @board.sente_hands.push(self) else @board.gote_hands.push(self) end @board.array[@x][@y] = nil else @board.array[@x][@y] = nil @board.array[x][y] = self end @x = x @y = y end def point @point end def name @name end def promoted_name @promoted_name end def to_s if (@sente) sg = "+" else sg = "-" end if (@promoted) n = @promoted_name else n = @name end return sg + n end end class PieceFU < Piece def initialize(*arg) @point = 1 @normal_moves = [[0, +1]] @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @name = "FU" @promoted_name = "TO" super end def room_of_head?(x, y, name) if (name == "FU") if (@sente) return false if (y == 1) else return false if (y == 9) end ## 2fu check c = 0 iy = 1 while (iy <= 9) if ((iy != @y) && # not source position @board.array[x][iy] && (@board.array[x][iy].sente == @sente) && # mine (@board.array[x][iy].name == "FU") && (@board.array[x][iy].promoted == false)) return false end iy = iy + 1 end end return true end end class PieceKY < Piece def initialize(*arg) @point = 1 @normal_moves = [] @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @name = "KY" @promoted_name = "NY" super end def room_of_head?(x, y, name) if (name == "KY") if (@sente) return false if (y == 1) else return false if (y == 9) end end return true end def far_movable_grids grids = Array::new if (@promoted) return [] else if (@sente) # up cand_x = @x cand_y = @y - 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_y = cand_y - 1 end else # down cand_x = @x cand_y = @y + 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_y = cand_y + 1 end end return grids end end end class PieceKE < Piece def initialize(*arg) @point = 1 @normal_moves = [[+1, +2], [-1, +2]] @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @name = "KE" @promoted_name = "NK" super end def room_of_head?(x, y, name) if (name == "KE") if (@sente) return false if ((y == 1) || (y == 2)) else return false if ((y == 9) || (y == 8)) end end return true end end class PieceGI < Piece def initialize(*arg) @point = 1 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]] @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @name = "GI" @promoted_name = "NG" super end end class PieceKI < Piece def initialize(*arg) @point = 1 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]] @promoted_moves = [] @name = "KI" @promoted_name = nil super end end class PieceKA < Piece def initialize(*arg) @point = 5 @normal_moves = [] @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]] @name = "KA" @promoted_name = "UM" super end def far_movable_grids grids = Array::new ## up right cand_x = @x - 1 cand_y = @y - 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x - 1 cand_y = cand_y - 1 end ## down right cand_x = @x - 1 cand_y = @y + 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x - 1 cand_y = cand_y + 1 end ## up left cand_x = @x + 1 cand_y = @y - 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x + 1 cand_y = cand_y - 1 end ## down left cand_x = @x + 1 cand_y = @y + 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x + 1 cand_y = cand_y + 1 end return grids end end class PieceHI < Piece def initialize(*arg) @point = 5 @normal_moves = [] @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]] @name = "HI" @promoted_name = "RY" super end def far_movable_grids grids = Array::new ## up cand_x = @x cand_y = @y - 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_y = cand_y - 1 end ## down cand_x = @x cand_y = @y + 1 while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_y = cand_y + 1 end ## right cand_x = @x - 1 cand_y = @y while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x - 1 end ## down cand_x = @x + 1 cand_y = @y while (jump_to?(cand_x, cand_y)) grids.push([cand_x, cand_y]) break if (! put_to?(cand_x, cand_y)) cand_x = cand_x + 1 end return grids end end class PieceOU < Piece def initialize(*arg) @point = 0 @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]] @promoted_moves = [] @name = "OU" @promoted_name = nil super end end class Board def initialize @sente_hands = Array::new @gote_hands = Array::new @history = Hash::new @sente_history = Hash::new @gote_history = Hash::new @array = [[], [], [], [], [], [], [], [], [], []] @move_count = 0 end attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history attr_reader :move_count def initial PieceKY::new(self, 1, 1, false) PieceKE::new(self, 2, 1, false) PieceGI::new(self, 3, 1, false) PieceKI::new(self, 4, 1, false) PieceOU::new(self, 5, 1, false) PieceKI::new(self, 6, 1, false) PieceGI::new(self, 7, 1, false) PieceKE::new(self, 8, 1, false) PieceKY::new(self, 9, 1, false) PieceKA::new(self, 2, 2, false) PieceHI::new(self, 8, 2, false) PieceFU::new(self, 1, 3, false) PieceFU::new(self, 2, 3, false) PieceFU::new(self, 3, 3, false) PieceFU::new(self, 4, 3, false) PieceFU::new(self, 5, 3, false) PieceFU::new(self, 6, 3, false) PieceFU::new(self, 7, 3, false) PieceFU::new(self, 8, 3, false) PieceFU::new(self, 9, 3, false) PieceKY::new(self, 1, 9, true) PieceKE::new(self, 2, 9, true) PieceGI::new(self, 3, 9, true) PieceKI::new(self, 4, 9, true) PieceOU::new(self, 5, 9, true) PieceKI::new(self, 6, 9, true) PieceGI::new(self, 7, 9, true) PieceKE::new(self, 8, 9, true) PieceKY::new(self, 9, 9, true) PieceKA::new(self, 8, 8, true) PieceHI::new(self, 2, 8, true) PieceFU::new(self, 1, 7, true) PieceFU::new(self, 2, 7, true) PieceFU::new(self, 3, 7, true) PieceFU::new(self, 4, 7, true) PieceFU::new(self, 5, 7, true) PieceFU::new(self, 6, 7, true) PieceFU::new(self, 7, 7, true) PieceFU::new(self, 8, 7, true) PieceFU::new(self, 9, 7, true) end def have_piece?(hands, name) piece = hands.find { |i| i.name == name } return piece end def move_to(x0, y0, x1, y1, name, sente) if (sente) hands = @sente_hands else hands = @gote_hands end if ((x0 == 0) || (y0 == 0)) piece = have_piece?(hands, name) return :illegal if (! piece.move_to?(x1, y1, name)) piece.move_to(x1, y1) else return :illegal if (! @array[x0][y0].move_to?(x1, y1, name)) if (@array[x0][y0].name != name) # promoted ? @array[x0][y0].promoted = true end if (@array[x1][y1]) if (@array[x1][y1].name == "OU") return :outori # return board update end @array[x1][y1].sente = @array[x0][y0].sente @array[x1][y1].move_to(0, 0) hands.sort! {|a, b| a.name <=> b.name } end @array[x0][y0].move_to(x1, y1) end @move_count += 1 return true end def look_for_ou(sente) x = 1 while (x <= 9) y = 1 while (y <= 9) if (@array[x][y] && (@array[x][y].name == "OU") && (@array[x][y].sente == sente)) return @array[x][y] end y = y + 1 end x = x + 1 end raise "can't find ou" end def checkmated?(sente) # sente is loosing ou = look_for_ou(sente) x = 1 while (x <= 9) y = 1 while (y <= 9) if (@array[x][y] && (@array[x][y].sente != sente)) if (@array[x][y].movable_grids.include?([ou.x, ou.y])) return true end end y = y + 1 end x = x + 1 end return false end def uchifuzume?(sente) rival_ou = look_for_ou(! sente) # rival's ou if (sente) # rival is gote if ((rival_ou.y != 9) && (@array[rival_ou.x][rival_ou.y + 1]) && (@array[rival_ou.x][rival_ou.y + 1].name == "FU") && (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true fu_x = rival_ou.x fu_y = rival_ou.y + 1 else return false end else # gote if ((rival_ou.y != 0) && (@array[rival_ou.x][rival_ou.y - 1]) && (@array[rival_ou.x][rival_ou.y - 1].name == "FU") && (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true fu_x = rival_ou.x fu_y = rival_ou.y - 1 else return false end end ## case: rival_ou is moving escaped = false rival_ou.movable_grids.each do |(cand_x, cand_y)| tmp_board = Marshal.load(Marshal.dump(self)) s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente) raise "internal error" if (s != true) if (! tmp_board.checkmated?(! sente)) # good move return false end end ## case: rival is capturing fu x = 1 while (x <= 9) y = 1 while (y <= 9) if (@array[x][y] && (@array[x][y].sente != sente) && @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable if (@array[x][y].promoted) name = @array[x][y].promoted_name else name = @array[x][y].name end tmp_board = Marshal.load(Marshal.dump(self)) s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente) raise "internal error" if (s != true) if (! tmp_board.checkmated?(! sente)) # good move return false end end y = y + 1 end x = x + 1 end return true end def oute_sennichite?(sente) if (checkmated?(! sente)) str = to_s if (sente) if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times return true end else if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times return true end end end return false end def sennichite?(sente) str = to_s if (@history[str] && (@history[str] >= 3)) # already 3 times return true end return false end def good_kachi?(sente) if (checkmated?(sente)) puts "'NG: Checkmating." if $DEBUG return false end ou = look_for_ou(sente) if (sente && (ou.y >= 4)) puts "'NG: Black's OU does not enter yet." if $DEBUG return false end if (! sente && (ou.y <= 6)) puts "'NG: White's OU does not enter yet." if $DEBUG return false end number = 0 point = 0 if (sente) hands = @sente_hands r = [1, 2, 3] else hands = @gote_hands r = [7, 8, 9] end r.each do |y| x = 1 while (x <= 9) if (@array[x][y] && (@array[x][y].sente == sente) && (@array[x][y].point > 0)) point = point + @array[x][y].point number = number + 1 end x = x + 1 end end hands.each do |piece| point = point + piece.point end if (number < 10) puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG return false end if (sente) if (point < 28) puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG return false end else if (point < 27) puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG return false end end puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG return true end # sente is nil only if tests in test_board run def handle_one_move(str, sente=nil) if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/) sg = $1 x0 = $2.to_i y0 = $3.to_i x1 = $4.to_i y1 = $5.to_i name = $6 elsif (str =~ /^%KACHI/) raise ArgumentError, "sente is null", caller if sente == nil if (good_kachi?(sente)) return :kachi_win else return :kachi_lose end elsif (str =~ /^%TORYO/) return :toryo else return :illegal end if (((x0 == 0) || (y0 == 0)) && # source is not from hand ((x0 != 0) || (y0 != 0))) return :illegal elsif ((x1 == 0) || (y1 == 0)) # destination is out of board return :illegal end if (sg == "+") sente = true if sente == nil # deprecated return :illegal unless sente == true # black player's move must be black hands = @sente_hands else sente = false if sente == nil # deprecated return :illegal unless sente == false # white player's move must be white hands = @gote_hands end ## source check if ((x0 == 0) && (y0 == 0)) return :illegal if (! have_piece?(hands, name)) elsif (! @array[x0][y0]) return :illegal # no piece elsif (@array[x0][y0].sente != sente) return :illegal # this is not mine elsif (@array[x0][y0].name != name) return :illegal if (@array[x0][y0].promoted_name != name) # can't promote end ## destination check if (@array[x1][y1] && (@array[x1][y1].sente == sente)) # can't capture mine return :illegal elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1]) return :illegal # can't put on existing piece end tmp_board = Marshal.load(Marshal.dump(self)) return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal) return :oute_kaihimore if (tmp_board.checkmated?(sente)) return :oute_sennichite if tmp_board.oute_sennichite?(sente) return :sennichite if tmp_board.sennichite?(sente) if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente)) return :uchifuzume end move_to(x0, y0, x1, y1, name, sente) str = to_s if (checkmated?(! sente)) if (sente) @sente_history[str] = (@sente_history[str] || 0) + 1 else @gote_history[str] = (@gote_history[str] || 0) + 1 end else if (sente) @sente_history.clear else @gote_history.clear end end @history[str] = (@history[str] || 0) + 1 return :normal end def to_s a = Array::new y = 1 while (y <= 9) a.push(sprintf("P%d", y)) x = 9 while (x >= 1) piece = @array[x][y] if (piece) s = piece.to_s else s = " * " end a.push(s) x = x - 1 end a.push(sprintf("\n")) y = y + 1 end if (! sente_hands.empty?) a.push("P+") sente_hands.each do |p| a.push("00" + p.name) end a.push("\n") end if (! gote_hands.empty?) a.push("P-") gote_hands.each do |p| a.push("00" + p.name) end a.push("\n") end a.push("+\n") return a.join end end class GameResult attr_reader :players, :black, :white def initialize(p1, p2) @players = [] @players << p1 @players << p2 if p1.sente && !p2.sente @black, @white = p1, p2 elsif !p1.sente && p2.sente @black, @white = p2, p1 else raise "Never reached!" end end end class GameResultWin < GameResult attr_reader :winner, :loser def initialize(winner, loser) super @winner, @loser = winner, loser end def to_s black_name = @black.id || @black.name white_name = @white.id || @white.name "%s:%s" % [black_name, white_name] end end class GameResultDraw < GameResult end class Game @@mutex = Mutex.new @@time = 0 def initialize(game_name, player0, player1) @monitors = Array::new @game_name = game_name if (@game_name =~ /-(\d+)-(\d+)$/) @total_time = $1.to_i @byoyomi = $2.to_i end if (player0.sente) @sente = player0 @gote = player1 else @sente = player1 @gote = player0 end @current_player = @sente @next_player = @gote @sente.game = self @gote.game = self @last_move = "" @current_turn = 0 @sente.status = "agree_waiting" @gote.status = "agree_waiting" @id = sprintf("%s+%s+%s+%s+%s", LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time) @logfile = @id + ".csa" LEAGUE.games[@id] = self log_message(sprintf("game created %s", @id)) @board = Board::new @board.initial @start_time = nil @fh = nil @result = nil propose end attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors attr_accessor :last_move, :current_turn attr_reader :result def rated? @sente.rated? && @gote.rated? end def monitoron(monitor) @monitors.delete(monitor) @monitors.push(monitor) end def monitoroff(monitor) @monitors.delete(monitor) end def reject(rejector) @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector)) @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector)) finish end def kill(killer) if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting")) reject(killer.name) elsif (@current_player == killer) abnormal_lose() finish end end def finish log_message(sprintf("game finished %s", @id)) @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S")) @fh.close @sente.game = nil @gote.game = nil @sente.status = "connected" @gote.status = "connected" if (@current_player.protocol == LoginCSA::PROTOCOL) @current_player.finish end if (@next_player.protocol == LoginCSA::PROTOCOL) @next_player.finish end @monitors = Array::new @sente = nil @gote = nil @current_player = nil @next_player = nil LEAGUE.games.delete(@id) end def handle_one_move(str, player) finish_flag = true if (@current_player == player) @end_time = Time::new t = (@end_time - @start_time).floor t = Least_Time_Per_Move if (t < Least_Time_Per_Move) move_status = nil if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0))) status = :timeout elsif (str == :timeout) return false # time isn't expired. players aren't swapped. continue game else @current_player.mytime = @current_player.mytime - t if (@current_player.mytime < 0) @current_player.mytime = 0 end # begin move_status = @board.handle_one_move(str, @sente == @current_player) # rescue # log_error("handle_one_move raise exception for #{str}") # move_status = :illegal # end if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore)) @fh.printf("'ILLEGAL_MOVE(%s)\n", str) else if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite)) @sente.write_safe(sprintf("%s,T%d\n", str, t)) @gote.write_safe(sprintf("%s,T%d\n", str, t)) @fh.printf("%s\nT%d\n", str, t) @last_move = sprintf("%s,T%d", str, t) @current_turn = @current_turn + 1 end @monitors.each do |monitor| monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] ")) monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id)) end end end if (@next_player.status != "game") # rival is logout or disconnected abnormal_win() elsif (status == :timeout) timeout_lose() elsif (move_status == :illegal) illegal_lose() elsif (move_status == :kachi_win) kachi_win() elsif (move_status == :kachi_lose) kachi_lose() elsif (move_status == :toryo) toryo_lose() elsif (move_status == :outori) outori_win() elsif (move_status == :sennichite) sennichite_draw() elsif (move_status == :oute_sennichite) oute_sennichite_lose() elsif (move_status == :uchifuzume) uchifuzume_lose() elsif (move_status == :oute_kaihimore) oute_kaihimore_lose() else finish_flag = false end finish() if finish_flag (@current_player, @next_player) = [@next_player, @current_player] @start_time = Time::new return finish_flag end end def abnormal_win @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n") @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n") @fh.printf("%%TORYO\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@current_player, @next_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id)) end end def abnormal_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n") @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n") @fh.printf("%%TORYO\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id)) end end def sennichite_draw @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#SENNICHITE\n#DRAW\n") @next_player.write_safe("#SENNICHITE\n#DRAW\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name) @result = GameResultDraw.new(@current_player, @next_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id)) end end def oute_sennichite_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n") @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id)) end end def illegal_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n") @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id)) end end def uchifuzume_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n") @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id)) end end def oute_kaihimore_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n") @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id)) end end def timeout_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#TIME_UP\n#LOSE\n") @next_player.write_safe("#TIME_UP\n#WIN\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id)) end end def kachi_win @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n") @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n") @fh.printf("%%KACHI\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@current_player, @next_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id)) end end def kachi_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n") @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n") @fh.printf("%%KACHI\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id)) end end def toryo_lose @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n") @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n") @fh.printf("%%TORYO\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@next_player, @current_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id)) end end def outori_win @current_player.status = "connected" @next_player.status = "connected" @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n") @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n") @fh.print(@board.to_s.gsub(/^/, "\'")) @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name) @result = GameResultWin.new(@current_player, @next_player) @fh.printf("'rating:#{@result.to_s}\n") if rated? @monitors.each do |monitor| monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id)) end end def start log_message(sprintf("game started %s", @id)) @sente.write_safe(sprintf("START:%s\n", @id)) @gote.write_safe(sprintf("START:%s\n", @id)) @sente.mytime = @total_time @gote.mytime = @total_time @start_time = Time::new end def propose begin @fh = open(@logfile, "w") @fh.sync = true @fh.printf("V2\n") @fh.printf("N+%s\n", @sente.name) @fh.printf("N-%s\n", @gote.name) @fh.printf("$EVENT:%s\n", @id) @sente.write_safe(propose_message("+")) @gote.write_safe(propose_message("-")) @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S")) @fh.print <= 4) end ensure $mutex.unlock end end # login loop if (! player) client.close Thread::exit return end log_message(sprintf("user %s login", player.name)) login.process player.run(login.csa_1st_str) begin $mutex.lock if (player.game) player.game.kill(player) end player.finish # socket has been closed LEAGUE.delete(player) log_message(sprintf("user %s logout", player.name)) ensure $mutex.unlock end end end end if ($0 == __FILE__) STDOUT.sync = true STDERR.sync = true TCPSocket.do_not_reverse_lookup = true Thread.abort_on_exception = true LEAGUE = ShogiServer::League::new main end