X-Git-Url: http://git.sourceforge.jp/view?p=shogi-server%2Fshogi-server.git;a=blobdiff_plain;f=shogi-server;h=c4573c658d08dc813bc5a0e370229e7e6893cd47;hp=f236f9cd9af5ff6c74014dbab6276dacfc00d3fa;hb=440981d016ebd22a8b8c9912b9d19f5087f0cb39;hpb=288f9facce889620046bc6934bb673984452dac0 diff --git a/shogi-server b/shogi-server index f236f9c..c4573c6 100755 --- a/shogi-server +++ b/shogi-server @@ -1,1990 +1,168 @@ #! /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' -require 'webrick' -require 'fileutils' - - -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 Exception => ex - log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") - return :exception - 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 - @dir = File.dirname(__FILE__) - end - attr_accessor :players, :games, :event, :dir - - # this should be called just after instanciating a League object. - def setup_players_database - @db = YAML::Store.new(File.join(@dir, "players.yaml")) - end - - 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 - break unless @db["players"] - @db["players"].each do |group, players| - hash = players[id] - break if hash - end - end - hash - end - - def rated_players - players = [] - @db.transaction(true) do - break unless @db["players"] - @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) - begin - @socket.write(str) - rescue Exception => ex - log_error("Failed to send a message to #{@name}.") - log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") - return - end - 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 run(csa_1st_str=nil) - while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout))) - begin - if (@writer_thread == nil || @writer_thread.status == false) - # The writer_thread has been killed because of socket errors. - return - end - - $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) # may be strip! ? - case str - when "" - # Application-level protocol for Keep-Alive - # If the server gets LF, it sends back LF. - # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet. - write_safe("\n") - 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) - # else - # begin - # @socket.write("##[KEEPALIVE] #{Time.now}\n") - # rescue Exception => ex - # log_error("Failed to send a keepalive to #{@name}.") - # log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") - # return - # end - end - when :exception - log_error("Failed to receive a message from #{@name}.") - return - 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_safe( - %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!) - 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 /^CHALLENGE/ - # This command is only available for CSA's official testing server. - # So, this means nothing for this program. - write_safe("CHALLENGE ACCEPTED\n") - when /^\s*$/ - ## ignore null string - else - msg = "##[ERROR] unknown command %s\n" % [str] - write_safe(msg) - log_error(msg) - 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 = File.join(LEAGUE.dir, @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 < ex + log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") + return :exception +end + def usage print <.conf". The file will be re-read once just after a + game starts. + + For example, a floodgate-3600-30 game group requires + floodgate-3600-30.conf. However, for floodgate-900-0 and + floodgate-3600-0, which were default enabled in previous + versions, configuration files are optional if you are happy with + default time settings. + File format is: + Line format: + # This is a comment line + DoW Time + ... + where + DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | + "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | + "Friday" | "Saturday" + Time := HH:MM + + For example, + Sat 13:00 + Sat 22:00 + Sun 13:00 LICENSE - this file is distributed under GPL version2 and might be compiled by Exerb + GPL versoin 2 or later SEE ALSO RELEASE - #{ShogiServer::Release} + #{ShogiServer::Release} REVISION - #{ShogiServer::Revision} + #{ShogiServer::Revision} + EOM end + +def log_debug(str) + $logger.debug(str) +end + def log_message(str) $logger.info(str) end +def log_info(str) + log_message(str) +end def log_warning(str) $logger.warn(str) @@ -1995,11 +173,17 @@ def log_error(str) end +# Parse command line options. Return a hash containing the option strings +# where a key is the option name without the first two slashes. For example, +# {"pid-file" => "foo.pid"}. +# def parse_command_line options = Hash::new - parser = GetoptLong.new( ["--daemon", GetoptLong::REQUIRED_ARGUMENT], - ["--pid-file", GetoptLong::REQUIRED_ARGUMENT] - ) + parser = GetoptLong.new( + ["--daemon", GetoptLong::REQUIRED_ARGUMENT], + ["--floodgate-games", GetoptLong::REQUIRED_ARGUMENT], + ["--pid-file", GetoptLong::REQUIRED_ARGUMENT], + ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT]) parser.quiet = true begin parser.each_option do |name, arg| @@ -2013,128 +197,249 @@ def parse_command_line return options end +# Check command line options. +# If any of them is invalid, exit the process. +# +def check_command_line + if (ARGV.length != 2) + usage + exit 2 + end + + if $options["daemon"] + $options["daemon"] = File.expand_path($options["daemon"], File.dirname(__FILE__)) + unless is_writable_dir? $options["daemon"] + usage + $stderr.puts "Can not create a file in the daemon directory: %s" % [$options["daemon"]] + exit 5 + end + end + + $topdir = $options["daemon"] || File.expand_path(File.dirname(__FILE__)) + + if $options["player-log-dir"] + $options["player-log-dir"] = File.expand_path($options["player-log-dir"], $topdir) + unless is_writable_dir?($options["player-log-dir"]) + usage + $stderr.puts "Can not write a file in the player log dir: %s" % [$options["player-log-dir"]] + exit 3 + end + end + + if $options["pid-file"] + $options["pid-file"] = File.expand_path($options["pid-file"], $topdir) + unless ShogiServer::is_writable_file? $options["pid-file"] + usage + $stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]] + exit 4 + end + end + + if $options["floodgate-games"] + names = $options["floodgate-games"].split(",") + new_names = + names.select do |name| + ShogiServer::League::Floodgate::game_name?(name) + end + if names.size != new_names.size + $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")] + exit 6 + end + $options["floodgate-games"] = new_names + end + + if $options["floodgate-history"] + $stderr.puts "WARNING: --floodgate-history has been deprecated." + $options["floodgate-history"] = nil + end +end + +# See if a file can be created in the directory. +# Return true if a file is writable in the directory, otherwise false. +# +def is_writable_dir?(dir) + unless File.directory? dir + return false + end + + result = true + + begin + temp_file = Tempfile.new("dummy-shogi-server", dir) + temp_file.close true + rescue + result = false + end + + return result +end + def write_pid_file(file) open(file, "w") do |fh| - fh.print Process::pid, "\n" + fh.puts "#{$$}" end end def mutex_watchdog(mutex, sec) + sec = 1 if sec < 1 + queue = [] while true + if mutex.try_lock + queue.clear + mutex.unlock + else + queue.push(Object.new) + if queue.size > sec + # timeout + log_error("mutex watchdog timeout: %d sec" % [sec]) + queue.clear + end + end + sleep(1) + end +end + +def login_loop(client) + player = login = nil + + while r = select([client], nil, nil, ShogiServer::Login_Time) do + str = nil + begin + break unless str = r[0].first.gets + rescue Exception => ex + # It is posssible that the socket causes an error (ex. Errno::ECONNRESET) + log_error("login_loop: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") + break + end + $mutex.lock # guards $league begin - timeout(sec) do - begin - mutex.lock - ensure - mutex.unlock + str =~ /([\r\n]*)$/ + eol = $1 + if (ShogiServer::Login::good_login?(str)) + player = ShogiServer::Player::new(str, client, eol) + login = ShogiServer::Login::factory(str, player) + if (current_player = $league.find(player.name)) + if (current_player.password == player.password && + current_player.status != "game") + log_message(sprintf("user %s login forcely", player.name)) + current_player.kill + else + login.incorrect_duplicated_player(str) + player = nil + break + end end + $league.add(player) + break + else + client.write("LOGIN:incorrect" + eol) + client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4) end - sleep(sec) - rescue TimeoutError - log_error("mutex watchdog timeout") - exit(1) + ensure + $mutex.unlock end - end + end # login loop + return [player, login] end -def main +def setup_logger(log_file) + logger = ShogiServer::Logger.new(log_file, 'daily') + logger.formatter = ShogiServer::Formatter.new + logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO + logger.datetime_format = "%Y-%m-%d %H:%M:%S" + return logger +end +def setup_watchdog_for_giant_lock $mutex = Mutex::new Thread::start do Thread.pass mutex_watchdog($mutex, 10) end +end +def main + $options = parse_command_line - if (ARGV.length != 2) - usage - exit 2 - end + check_command_line + $config = ShogiServer::Config.new $options - LEAGUE.event = ARGV.shift - port = ARGV.shift + $league = ShogiServer::League.new($topdir) - write_pid_file($options["pid-file"]) if ($options["pid-file"]) + $league.event = ARGV.shift + port = ARGV.shift - dir = $options["daemon"] || nil - if dir && ! File.exist?(dir) - FileUtils.mkdir(dir) - end - log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT - $logger = WEBrick::Log.new(log_file) + log_file = $options["daemon"] ? File.join($options["daemon"], "shogi-server.log") : STDOUT + $logger = setup_logger(log_file) - LEAGUE.dir = dir || File.dirname(__FILE__) - LEAGUE.setup_players_database + $league.dir = $topdir config = {} config[:Port] = port config[:ServerType] = WEBrick::Daemon if $options["daemon"] config[:Logger] = $logger + setup_floodgate = nil + + config[:StartCallback] = Proc.new do + srand + if $options["pid-file"] + write_pid_file($options["pid-file"]) + end + setup_watchdog_for_giant_lock + $league.setup_players_database + setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"]) + setup_floodgate.start + end + + config[:StopCallback] = Proc.new do + if $options["pid-file"] + FileUtils.rm($options["pid-file"], :force => true) + end + end + + srand server = WEBrick::GenericServer.new(config) - ["INT", "TERM"].each {|signal| trap(signal){ server.shutdown } } - $stderr.puts("server started as a deamon") if $options["daemon"] - log_message("server started") + ["INT", "TERM"].each do |signal| + trap(signal) do + server.shutdown + setup_floodgate.kill + end + end + unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/) + trap("HUP") do + Dependencies.clear + end + end + $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] + log_message("server started [Revision: #{ShogiServer::Revision}]") server.start do |client| + begin # client.sync = true # this is already set in WEBrick client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time - player = nil - login = nil - while (str = client.gets_timeout(ShogiServer::Login_Time)) - begin - $mutex.lock - str =~ /([\r\n]*)$/ - eol = $1 - if (ShogiServer::Login::good_login?(str)) - player = ShogiServer::Player::new(str, client) - player.eol = eol - login = ShogiServer::Login::factory(str, player) - if (LEAGUE.players[player.name]) - if ((LEAGUE.players[player.name].password == player.password) && - (LEAGUE.players[player.name].status != "game")) - log_message(sprintf("user %s login forcely", player.name)) - LEAGUE.players[player.name].kill - else - login.incorrect_duplicated_player(str) - #Thread::exit - #return - # TODO - player = nil - break - end - end - LEAGUE.add(player) - break - else - client.write_safe("LOGIN:incorrect" + eol) - client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4) - end - ensure - $mutex.unlock - end - end # login loop - if (! player) - #client.close - #Thread::exit - #return - next - end + player, login = login_loop(client) # loop + next unless player + log_message(sprintf("user %s login", player.name)) login.process - player.run(login.csa_1st_str) + player.setup_logger($options["player-log-dir"]) if $options["player-log-dir"] + player.run(login.csa_1st_str) # loop + $mutex.lock begin - $mutex.lock if (player.game) player.game.kill(player) end player.finish # socket has been closed - LEAGUE.delete(player) + $league.delete(player) log_message(sprintf("user %s logout", player.name)) ensure $mutex.unlock end + rescue Exception => ex + log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") + end end end @@ -2143,8 +448,15 @@ if ($0 == __FILE__) STDOUT.sync = true STDERR.sync = true TCPSocket.do_not_reverse_lookup = true - Thread.abort_on_exception = true + Thread.abort_on_exception = $DEBUG ? true : false - LEAGUE = ShogiServer::League::new - main + begin + main + rescue Exception => ex + if $logger + log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") + else + $stderr.puts "main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" + end + end end