+## $Id$
+
+## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
+## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
+##
+## 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 'kconv'
require 'shogi_server'
class Command
# Factory method
#
- def Command.factory(str, player)
+ def Command.factory(str, player, time=Time.now)
cmd = nil
case str
when ""
when /^%%MONITOROFF\s+(\S+)/
game_id = $1
cmd = MonitorOffCommand.new(str, player, $league.games[game_id])
+ when /^%%MONITOR2ON\s+(\S+)/
+ game_id = $1
+ cmd = Monitor2OnCommand.new(str, player, $league.games[game_id])
+ when /^%%MONITOR2OFF\s+(\S+)/
+ game_id = $1
+ cmd = Monitor2OffCommand.new(str, player, $league.games[game_id])
when /^%%HELP/
cmd = HelpCommand.new(str, player)
when /^%%RATING/
my_sente_str = $3
cmd = GameChallengeCommand.new(str, player,
command_name, game_name, my_sente_str)
+ when /^%%(GAME|CHALLENGE)\s+(\S+)/
+ msg = "A turn identifier is required"
+ cmd = ErrorCommand.new(str, player, msg)
when /^%%CHAT\s+(.+)/
message = $1
cmd = ChatCommand.new(str, player, message, $league.players)
cmd = LogoutCommand.new(str, player)
when /^CHALLENGE/
cmd = ChallengeCommand.new(str, player)
+ when /^%%SETBUOY\s+(\S+)\s+(\S+)(.*)/
+ game_name = $1
+ moves = $2
+ count = 1 # default
+ if $3 && /^\s+(\d*)/ =~ $3
+ count = $1.to_i
+ end
+ cmd = SetBuoyCommand.new(str, player, game_name, moves, count)
+ when /^%%DELETEBUOY\s+(\S+)/
+ game_name = $1
+ cmd = DeleteBuoyCommand.new(str, player, game_name)
+ when /^%%GETBUOYCOUNT\s+(\S+)/
+ game_name = $1
+ cmd = GetBuoyCountCommand.new(str, player, game_name)
+ when /^%%FORK\s+(\S+)\s+(\S+)(.*)/
+ source_game = $1
+ new_buoy_game = $2
+ nth_move = nil
+ if $3 && /^\s+(\d+)/ =~ $3
+ nth_move = $3.to_i
+ end
+ cmd = ForkCommand.new(str, player, source_game, new_buoy_game, nth_move)
+ when /^%%FORK\s+(\S+)$/
+ source_game = $1
+ new_buoy_game = nil
+ nth_move = nil
+ cmd = ForkCommand.new(str, player, source_game, new_buoy_game, nth_move)
when /^\s*$/
cmd = SpaceCommand.new(str, player)
+ when /^%%%[^%]/
+ # TODO: just ignore commands specific to 81Dojo.
+ # Need to discuss with 81Dojo people.
+ cmd = VoidCommand.new(str, player)
else
cmd = ErrorCommand.new(str, player)
end
+ cmd.time = time
return cmd
end
def initialize(str, player)
@str = str
@player = player
+ @time = Time.now # this should be replaced later with a real time
+ end
+ attr_accessor :time
+ end
+
+ # Dummy command which does nothing.
+ #
+ class VoidCommand < Command
+ def initialize(str, player)
+ super
+ end
+
+ def call
+ return :continue
end
end
if (@player.status == "game")
array_str = @str.split(",")
move = array_str.shift
+ if @player.game.last_move &&
+ @player.game.last_move.split(",").first == move
+ log_warning("Received two sequencial identical moves [#{move}] from #{@player.name}. The last one was ignored.")
+ return :continue
+ end
additional = array_str.shift
comment = nil
if /^'(.*)/ =~ additional
comment = array_str.unshift("'*#{$1.toeuc}")
end
- s = @player.game.handle_one_move(move, @player)
+ s = @player.game.handle_one_move(move, @player, @time)
@player.game.log_game(Kconv.toeuc(comment.first)) if (comment && comment.first && !s)
return :return if (s && @player.protocol == LoginCSA::PROTOCOL)
end
def in_game_status
rc = :continue
- s = @player.game.handle_one_move(@str, @player)
+ s = @player.game.handle_one_move(@str, @player, @time)
rc = :return if (s && @player.protocol == LoginCSA::PROTOCOL)
return rc
rc = :continue
if @player.game.prepared_expire?
- log_warning("#{@player.status} lasted too long. This play has been expired.")
+ log_warning("#{@player.status} lasted too long. This play has been expired: %s" % [@player.game.game_id])
@player.game.reject("the Server (timed out)")
rc = :return if (@player.protocol == LoginCSA::PROTOCOL)
end
end
end
+ class MonitorHandler
+ def initialize(player)
+ @player = player
+ @type = nil
+ @header = nil
+ end
+ attr_reader :player, :type, :header
+
+ def ==(rhs)
+ return rhs != nil &&
+ rhs.is_a?(MonitorHandler) &&
+ @player == rhs.player &&
+ @type == rhs.type
+ end
+
+ def write_safe(game_id, str)
+ str.chomp.split("\n").each do |line|
+ @player.write_safe("##[%s][%s] %s\n" % [@header, game_id, line.chomp])
+ end
+ @player.write_safe("##[%s][%s] %s\n" % [@header, game_id, "+OK"])
+ end
+ end
+
+ class MonitorHandler1 < MonitorHandler
+ def initialize(player)
+ super
+ @type = 1
+ @header = "MONITOR"
+ end
+
+ def write_one_move(game_id, game)
+ write_safe(game_id, game.show.chomp)
+ end
+ end
+
+ class MonitorHandler2 < MonitorHandler
+ def initialize(player)
+ super
+ @type = 2
+ @header = "MONITOR2"
+ end
+
+ def write_one_move(game_id, game)
+ write_safe(game_id, game.last_move.gsub(",", "\n"))
+ end
+ end
+
# Command of MONITORON
#
class MonitorOnCommand < BaseCommandForGame
def call
if (@game)
- @game.monitoron(@player)
- @player.write_safe(@game.show.gsub(/^/, "##[MONITOR][#{@game_id}] "))
- @player.write_safe("##[MONITOR][#{@game_id}] +OK\n")
+ monitor_handler = MonitorHandler1.new(@player)
+ @game.monitoron(monitor_handler)
+ monitor_handler.write_safe(@game_id, @game.show)
end
return :continue
end
def call
if (@game)
- @game.monitoroff(@player)
+ @game.monitoroff(MonitorHandler1.new(@player))
+ end
+ return :continue
+ end
+ end
+
+ # Command of MONITOR2ON
+ #
+ class Monitor2OnCommand < BaseCommandForGame
+ def initialize(str, player, game)
+ super
+ end
+
+ def call
+ if (@game)
+ monitor_handler = MonitorHandler2.new(@player)
+ @game.monitoron(monitor_handler)
+ lines = IO::readlines(@game.logfile).join("")
+ monitor_handler.write_safe(@game_id, lines)
+ end
+ return :continue
+ end
+ end
+
+ class Monitor2OffCommand < MonitorOffCommand
+ def initialize(str, player, game)
+ super
+ end
+
+ def call
+ if (@game)
+ @game.monitoroff(MonitorHandler2.new(@player))
end
return :continue
end
@command_name = command_name
@game_name = game_name
@my_sente_str = my_sente_str
+ player.set_sente_from_str(@my_sente_str)
end
def call
if (! Login::good_game_name?(@game_name))
- @player.write_safe(sprintf("##[ERROR] bad game name\n"))
+ @player.write_safe(sprintf("##[ERROR] bad game name: %s.\n", @game_name))
+ if (/^(.+)-\d+-\d+$/ =~ @game_name)
+ if Login::good_identifier?($1)
+ # do nothing
+ else
+ @player.write_safe(sprintf("##[ERROR] invalid identifiers are found or too many characters are used.\n"))
+ end
+ else
+ @player.write_safe(sprintf("##[ERROR] game name should consist of three parts like game-1500-60.\n"))
+ end
return :continue
elsif ((@player.status == "connected") || (@player.status == "game_waiting"))
## continue
end
@player.sente = nil
else
- if (@my_sente_str == "*")
- rival = $league.get_player("game_waiting", @game_name, nil, @player) # no preference
- elsif (@my_sente_str == "+")
- rival = $league.get_player("game_waiting", @game_name, false, @player) # rival must be gote
- elsif (@my_sente_str == "-")
- rival = $league.get_player("game_waiting", @game_name, true, @player) # rival must be sente
- else
- ## never reached
- @player.write_safe(sprintf("##[ERROR] bad game option\n"))
- return :continue
+ rival = $league.find_rival(@player, @game_name)
+ if rival.instance_of?(Symbol)
+ # An error happened. rival is not a player instance, but an error
+ # symobl that must be returned to the main routine immediately.
+ return rival
end
end
if (rival)
@player.game_name = @game_name
- if ((@my_sente_str == "*") && (rival.sente == nil))
- if (rand(2) == 0)
- @player.sente = true
- rival.sente = false
+ Game::decide_turns(@player, @my_sente_str, rival)
+
+ if (Buoy.game_name?(@game_name))
+ buoy = Buoy.new # TODO config
+ if buoy.is_new_game?(@game_name)
+ # The buoy game is not ready yet.
+ # When the game is set, it will be started.
+ @player.status = "game_waiting"
else
- @player.sente = false
- rival.sente = true
+ buoy_game = buoy.get_game(@game_name)
+ if buoy_game.instance_of? NilBuoyGame
+ # error. never reach
+ end
+
+ moves_array = Board::split_moves(buoy_game.moves)
+ board = Board.new
+ begin
+ board.set_from_moves(moves_array)
+ rescue => err
+ # it will never happen since moves have already been checked
+ log_error "Failed to set up a buoy game: #{moves}"
+ return :continue
+ end
+ buoy.decrement_count(buoy_game)
+ Game::new(@player.game_name, @player, rival, board)
end
- elsif (rival.sente == true) # rival has higher priority
- @player.sente = false
- elsif (rival.sente == false)
- @player.sente = true
- elsif (@my_sente_str == "+")
- @player.sente = true
- rival.sente = false
- elsif (@my_sente_str == "-")
- @player.sente = false
- rival.sente = true
else
- ## never reached
+ klass = Login.handicapped_game_name?(@game_name) || Board
+ board = klass.new
+ board.initial
+ Game::new(@player.game_name, @player, rival, board)
end
- Game::new(@player.game_name, @player, rival)
else # rival not found
if (@command_name == "GAME")
@player.status = "game_waiting"
@player.game_name = @game_name
- if (@my_sente_str == "+")
- @player.sente = true
- elsif (@my_sente_str == "-")
- @player.sente = false
- else
- @player.sente = nil
- end
else # challenge
@player.write_safe(sprintf("##[ERROR] can't find rival for %s\n", @game_name))
@player.status = "connected"
# Command for an error
#
class ErrorCommand < Command
- def initialize(str, player)
- super
+ def initialize(str, player, msg=nil)
+ super(str, player)
+ @msg = msg || "unknown command"
+ end
+ attr_reader :msg
+
+ def call
+ cmd = @str.chomp
+ # Aim to hide a possible password
+ cmd.gsub!(/LOGIN\s*(\w+)\s+.*/i, 'LOGIN \1...')
+ @msg = "##[ERROR] %s: %s\n" % [@msg, cmd]
+ @player.write_safe(@msg)
+ log_error(@msg)
+ return :continue
+ end
+ end
+
+ #
+ #
+ class SetBuoyCommand < Command
+
+ def initialize(str, player, game_name, moves, count)
+ super(str, player)
+ @game_name = game_name
+ @moves = moves
+ @count = count
end
def call
- msg = "##[ERROR] unknown command %s\n" % [@str]
- @player.write_safe(msg)
- log_error(msg)
+ unless (Buoy.game_name?(@game_name))
+ @player.write_safe(sprintf("##[ERROR] wrong buoy game name: %s\n", @game_name))
+ log_error "Received a wrong buoy game name: %s from %s." % [@game_name, @player.name]
+ return :continue
+ end
+ buoy = Buoy.new
+ unless buoy.is_new_game?(@game_name)
+ @player.write_safe(sprintf("##[ERROR] duplicated buoy game name: %s\n", @game_name))
+ log_error "Received duplicated buoy game name: %s from %s." % [@game_name, @player.name]
+ return :continue
+ end
+ if @count < 1
+ @player.write_safe(sprintf("##[ERROR] invalid count: %s\n", @count))
+ log_error "Received an invalid count for a buoy game: %s, %s from %s." % [@count, @game_name, @player.name]
+ return :continue
+ end
+
+ # check moves
+ moves_array = Board::split_moves(@moves)
+ board = Board.new
+ begin
+ board.set_from_moves(moves_array)
+ rescue
+ raise WrongMoves
+ end
+
+ buoy_game = BuoyGame.new(@game_name, @moves, @player.name, @count)
+ buoy.add_game(buoy_game)
+ @player.write_safe(sprintf("##[SETBUOY] +OK\n"))
+ log_info("A buoy game was created: %s by %s" % [@game_name, @player.name])
+
+ # if two players are waiting for this buoy game, start it
+ candidates = $league.find_all_players do |player|
+ player.status == "game_waiting" &&
+ player.game_name == @game_name &&
+ player.name != @player.name
+ end
+ if candidates.empty?
+ log_info("No players found for a buoy game. Wait for players: %s" % [@game_name])
+ return :continue
+ end
+ p1 = candidates.first
+ p2 = $league.find_rival(p1, @game_name)
+ if p2.nil?
+ log_info("No opponent found for a buoy game. Wait for the opponent: %s by %s" % [@game_name, p1.name])
+ return :continue
+ elsif p2.instance_of?(Symbol)
+ # An error happened. rival is not a player instance, but an error
+ # symobl that must be returned to the main routine immediately.
+ return p2
+ end
+ # found two players: p1 and p2
+ log_info("Starting a buoy game: %s with %s and %s" % [@game_name, p1.name, p2.name])
+ buoy.decrement_count(buoy_game)
+ game = Game::new(@game_name, p1, p2, board)
+ return :continue
+
+ rescue WrongMoves => e
+ @player.write_safe(sprintf("##[ERROR] wrong moves: %s\n", @moves))
+ log_error "Received wrong moves: %s from %s. [%s]" % [@moves, @player.name, e.message]
return :continue
end
end
+ #
+ #
+ class DeleteBuoyCommand < Command
+ def initialize(str, player, game_name)
+ super(str, player)
+ @game_name = game_name
+ end
+
+ def call
+ buoy = Buoy.new
+ buoy_game = buoy.get_game(@game_name)
+ if buoy_game.instance_of?(NilBuoyGame)
+ @player.write_safe(sprintf("##[ERROR] buoy game not found: %s\n", @game_name))
+ log_error "Game name not found: %s by %s" % [@game_name, @player.name]
+ return :continue
+ end
+
+ if buoy_game.owner != @player.name
+ @player.write_safe(sprintf("##[ERROR] you are not allowed to delete a buoy game that you did not set: %s\n", @game_name))
+ log_error "%s are not allowed to delete a game: %s" % [@player.name, @game_name]
+ return :continue
+ end
+
+ buoy.delete_game(buoy_game)
+ @player.write_safe(sprintf("##[DELETEBUOY] +OK\n"))
+ log_info("A buoy game was deleted: %s" % [@game_name])
+ return :continue
+ end
+ end
+
+ #
+ #
+ class GetBuoyCountCommand < Command
+ def initialize(str, player, game_name)
+ super(str, player)
+ @game_name = game_name
+ end
+
+ def call
+ buoy = Buoy.new
+ buoy_game = buoy.get_game(@game_name)
+ if buoy_game.instance_of?(NilBuoyGame)
+ @player.write_safe("##[GETBUOYCOUNT] -1\n")
+ else
+ @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
+ end
+ @player.write_safe("##[GETBUOYCOUNT] +OK\n")
+ return :continue
+ end
+ end
+
+ # %%FORK <source_game> <new_buoy_game> [<nth-move>]
+ # Fork a new game from the posistion where the n-th (starting from 1) move
+ # of a source game is played. The new game should be a valid buoy game
+ # name. The default value of n is the position where the previous position
+ # of the last one.
+ #
+ class ForkCommand < Command
+ def initialize(str, player, source_game, new_buoy_game, nth_move)
+ super(str, player)
+ @source_game = source_game
+ @new_buoy_game = new_buoy_game
+ @nth_move = nth_move # may be nil
+ end
+ attr_reader :new_buoy_game
+
+ def decide_new_buoy_game_name
+ name = nil
+ total_time = nil
+ byo_time = nil
+
+ if @source_game.split("+").size >= 2 &&
+ /^([^-]+)-(\d+)-(\d+)/ =~ @source_game.split("+")[1]
+ name = $1
+ total_time = $2
+ byo_time = $3
+ end
+ if name == nil || total_time == nil || byo_time == nil
+ @player.write_safe(sprintf("##[ERROR] wrong source game name to make a new buoy game name: %s\n", @source_game))
+ log_error "Received a wrong source game name to make a new buoy game name: %s from %s." % [@source_game, @player.name]
+ return :continue
+ end
+ @new_buoy_game = "buoy_%s_%d-%s-%s" % [name, @nth_move, total_time, byo_time]
+ @player.write_safe(sprintf("##[FORK]: new buoy game name: %s\n", @new_buoy_game))
+ @player.write_safe("##[FORK] +OK\n")
+ end
+
+ def call
+ game = $league.games[@source_game]
+ unless game
+ @player.write_safe(sprintf("##[ERROR] wrong source game name: %s\n", @source_game))
+ log_error "Received a wrong source game name: %s from %s." % [@source_game, @player.name]
+ return :continue
+ end
+
+ moves = game.read_moves # [["+7776FU","T2"],["-3334FU","T5"]]
+ @nth_move = moves.size - 1 unless @nth_move
+ if @nth_move > moves.size or @nth_move < 1
+ @player.write_safe(sprintf("##[ERROR] number of moves to fork is out of range: %s.\n", moves.size))
+ log_error "Number of moves to fork is out of range: %s [%s]" % [@nth_move, @player.name]
+ return :continue
+ end
+ new_moves_str = ""
+ moves[0...@nth_move].each do |m|
+ new_moves_str << m.join(",")
+ end
+
+ unless @new_buoy_game
+ decide_new_buoy_game_name
+ end
+
+ buoy_cmd = SetBuoyCommand.new(@str, @player, @new_buoy_game, new_moves_str, 1)
+ return buoy_cmd.call
+ end
+ end
end # module ShogiServer