+2009-07-29 Daigo Moriwaki <daigo at debian dot org>
+
+ * [shogi-server]
+ - A experimental new feature, codenamed Buoy: it allows players to
+ play a game starting with a specified position. First, a player
+ sets a buoy game with moves to a specific position. Then, two
+ players can play a new game with the game name.
+ New commands:
+ + %%SETBUOY <game_name> <moves> [count]
+ Set a new buoy game.
+ ex. %%SETBUOYGAME buoy_foo-900-0 +7776FU 10
+ ex. %%SETBUOYGAME buoy_foo-1500-0 +7776FU-3334FU
+ - game_name is a valid game name with a prefix "buoy_".
+ ex. buoy_foo-900-0
+ - moves are initial moves from the Hirate position to a
+ spcific position that you want to start with.
+ ex. +7776FU-3334FU+8786FU
+ - count is an optional attribute to tell how many times the
+ game can be played (default 1). The count is decremented
+ when the game finishes. If the count reaches zero, the buoy
+ game is removed automatically by the server.
+ ex. 10
+ + %%DELETEBUOY <game_name>
+ Delete a buoy game. The only owner who set up the game is
+ allowed to delete it.
+ ex. %%DELETEBUOY buoy_foo-900-0
+ - game_name is the buoy game name that was created.
+ + %%GETBUOYCOUNT <game_name>
+ Show a current count of the buoy game or zero for non-existing
+ games.
+
+
2009-07-11 Daigo Moriwaki <daigo at debian dot org>
* [shogi-server]
$topdir = nil
$league = nil
$logger = nil
+$config = nil
$:.unshift File.dirname(__FILE__)
require 'shogi_server'
+require 'shogi_server/config'
require 'tempfile'
#################################################
$options = parse_command_line
check_command_line
+ $config = ShogiServer::Config.new $options
$league = ShogiServer::League.new($topdir)
require 'shogi_server/timeout_queue'
require 'shogi_server/usi'
require 'shogi_server/util'
+require 'shogi_server/command'
+require 'shogi_server/buoy'
module ShogiServer # for a namespace
module ShogiServer # for a namespace
class Board
+
def initialize(move_count=0)
@sente_hands = Array::new
@gote_hands = Array::new
@array = [[], [], [], [], [], [], [], [], [], []]
@move_count = move_count
@teban = nil # black => true, white => false
+ @initial_moves = []
end
attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
attr_reader :move_count
+
+ # Initial moves for a Buoy game. If it is an empty array, the game is
+ # normal with the initial setting; otherwise, the game is started after the
+ # moves.
+ attr_reader :initial_moves
def deep_copy
return Marshal.load(Marshal.dump(self))
@teban = true
end
+ # Set up a board with the strs.
+ # Failing to parse the moves raises an StandardError.
+ # @param strs a board text
+ #
def set_from_str(strs)
- strs.split(/\n/).each do |str|
- if (str =~ /^P\d/)
+ strs.each_line do |str|
+ case str
+ when /^P\d/
str.sub!(/^P(.)/, '')
y = $1.to_i
x = 9
end
x = x - 1
end
- elsif (str =~ /^P([\+\-])/)
+ when /^P([\+\-])/
sg = $1
if (sg == "+")
sente = true
raise "unkown piece #{name}"
end
end # while
- end # if
+ when /^\+$/
+ @teban = true
+ when /^\-$/
+ @teban = false
+ else
+ raise "bad line: #{str}"
+ end # case
end # do
end
+ # Set up a board starting with a position after the moves.
+ # Failing to parse the moves raises an ArgumentError.
+ # @param moves an array of moves. ex. ["+7776FU", "-3334FU"]
+ #
+ def set_from_moves(moves)
+ initial()
+ return :normal if moves.empty?
+ rt = nil
+ moves.each do |move|
+ rt = handle_one_move(move, @teban)
+ raise ArgumentError, "bad moves: #{moves}" unless rt == :normal
+ end
+ @initial_moves = moves.dup
+ end
+
def have_piece?(hands, name)
piece = hands.find { |i|
i.name == name
+## $Id$
+
+## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
+## Copyright (C) 2007-2008 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'
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 /^\s*$/
cmd = SpaceCommand.new(str, player)
else
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
rival = nil
+ if (Buoy.game_name?(@game_name))
+ if (@my_sente_str != "*")
+ @player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
+ return :continue
+ end
+ @player.sente = nil
+ end # really end
+
if (League::Floodgate.game_name?(@game_name))
if (@my_sente_str != "*")
@player.write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", @my_sente_str, @game_name))
if (rival)
@player.game_name = @game_name
+
if ((@my_sente_str == "*") && (rival.sente == nil))
if (rand(2) == 0)
@player.sente = true
else
## never reached
end
- Game::new(@player.game_name, @player, 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.
+ else
+ buoy_game = buoy.get_game(@game_name)
+ if buoy_game.instance_of NilBuoyGame
+ # error. never reach
+ end
+ board = Board.new
+ begin
+ board.set_from_moves(buoy_game.moves)
+ 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
+ Game::new(@player.game_name, @player, rival, board)
+ end
+ else
+ board = Board.new
+ board.initial
+ Game::new(@player.game_name, @player, rival, board)
+ end
else # rival not found
if (@command_name == "GAME")
@player.status = "game_waiting"
end
end
+ #
+ #
+ class SetBuoyCommand < Command
+ class WrongMoves < ArgumentError; end
+
+ def initialize(str, player, game_name, moves, count)
+ super(str, player)
+ @game_name = game_name
+ @moves = moves
+ @count = count
+ end
+
+ def call
+ 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 = 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)
+
+ # if two players, who are not @player, are waiting for a new game, start it
+ p1 = $league.get_player("game_waiting", @game_name, true, @player)
+ return :continue unless p1
+ p2 = $league.get_player("game_waiting", @game_name, false, @player)
+ return :continue unless p2
+
+ 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
+
+ private
+
+ # Split a moves line into an array of a move string.
+ # If it fails to parse the moves, it raises WrongMoves.
+ # @param moves a moves line. Ex. "+776FU-3334Fu"
+ # @return an array of a move string. Ex. ["+7776FU", "-3334FU"]
+ #
+ def split_moves(moves)
+ ret = []
+
+ rs = moves.gsub %r{[\+\-]\d{4}\w{2}} do |s|
+ ret << s
+ ""
+ end
+ raise WrongMoves, rs unless rs.empty?
+
+ return ret
+ 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)
+ 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] 0\n")
+ else
+ @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
+ end
+ @player.write_safe("##[GETBUOYCOUNT] +OK\n")
+ end
+ end
end # module ShogiServer
$options["floodgate-history"]
add_observer League::Floodgate::History.factory
end
+
+ # TODO take observers from the game object
+ if Buoy.game_name?(@game.game_name)
+ add_observer(BuoyObserver.new)
+ end
end
def process
@@mutex = Mutex.new
@@time = 0
- def initialize(game_name, player0, player1)
+ def initialize(game_name, player0, player1, board)
@monitors = Array::new
@game_name = game_name
if (@game_name =~ /-(\d+)-(\d+)$/)
end
@sente.socket_buffer.clear
@gote.socket_buffer.clear
- @current_player, @next_player = @sente, @gote
+ @board = board
+ if @board.teban
+ @current_player, @next_player = @sente, @gote
+ else
+ @current_player, @next_player = @gote, @sente
+ end
@sente.game = self
@gote.game = self
- @last_move = ""
- @current_turn = 0
+ @last_move = @board.initial_moves.empty? ? "" : "%s,T1" % [@board.initial_moves.last]
+ @current_turn = @board.initial_moves.size
@sente.status = "agree_waiting"
@gote.status = "agree_waiting"
log_message(sprintf("game created %s", @game_id))
- @board = Board::new
- @board.initial
@start_time = nil
@fh = open(@logfile, "w")
@fh.sync = true
attr_accessor :last_move, :current_turn
attr_reader :result, :prepared_time
+ # Path of a log file for this game.
+ attr_reader :logfile
+
def rated?
@sente.rated? && @gote.rated?
end
white_name = @gote.rated? ? @gote.player_id : @gote.name
@fh.puts("'rating:%s:%s" % [black_name, white_name])
end
+ unless @board.initial_moves.empty?
+ @fh.puts "'buoy game starting with %d moves" % [@board.initial_moves.size]
+ @board.initial_moves.each do |move|
+ @fh.puts move
+ @fh.puts "T1"
+ end
+ end
end
def show()
Least_Time_Per_Move:#{Least_Time_Per_Move}
END Time
BEGIN Position
-P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
-P2 * -HI * * * * * -KA *
-P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
-P4 * * * * * * * * *
-P5 * * * * * * * * *
-P6 * * * * * * * * *
-P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
-P8 * +KA * * * * * +HI *
-P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
-P+
-P-
-+
+#{@board.to_s.chomp}
END Position
END Game_Summary
EOM
log_message("Floodgate: Starting a game: BLACK %s vs WHITE %s" % [p1.name, p2.name])
p1.sente = true
p2.sente = false
- Game.new(p1.game_name, p1, p2)
+ board = Board.new
+ board.initial
+ Game.new(p1.game_name, p1, p2, board)
end
end
require 'TC_board'
require 'TC_before_agree'
+require 'TC_buoy'
require 'TC_command'
+require 'TC_config'
require 'TC_floodgate'
require 'TC_floodgate_history'
require 'TC_functional'
+require 'TC_game'
require 'TC_game_result'
require 'TC_jishogi_kachi'
require 'TC_league'
assert_equal(false, b.uchifuzume?(true))
end
end
+
+class TestBoardForBuoy < Test::Unit::TestCase
+ def setup
+ @board = ShogiServer::Board.new
+ end
+
+ def test_set_from_moves_empty
+ moves = []
+ rt = @board.set_from_moves moves
+ assert_equal(:normal, rt)
+ end
+
+ def test_set_from_moves
+ moves = ["+7776FU", "-3334FU"]
+ assert_nothing_raised do
+ @board.set_from_moves moves
+ end
+
+ correct = ShogiServer::Board.new
+ correct.set_from_str <<EOF
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU * -FU-FU
+P4 * * * * * * -FU * *
+P5 * * * * * * * * *
+P6 * * +FU * * * * * *
+P7+FU+FU * +FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
++
+EOF
+ assert_equal(correct.to_s, @board.to_s)
+ end
+
+ def test_set_from_moves_error1
+ moves = ["+7776FU", "-3435FU"]
+ assert_raise ArgumentError do
+ @board.set_from_moves moves
+ end
+ end
+
+ def test_set_from_moves_error2
+ moves = ["+7776FU", "+8786FU"]
+ assert_raise ArgumentError do
+ @board.set_from_moves moves
+ end
+ end
+end # TestBoardForBuoy
+
--- /dev/null
+$:.unshift File.join(File.dirname(__FILE__), "..")
+$topdir = File.expand_path File.dirname(__FILE__)
+require 'test/unit'
+require 'shogi_server/buoy'
+require 'mock_game'
+require 'mock_player'
+require 'mock_log_message'
+
+
+class TestBuoyGame < Test::Unit::TestCase
+ def test_equal
+ g1 = ShogiServer::BuoyGame.new("buoy_1234-900-0", [], "p1", 1)
+ g2 = ShogiServer::BuoyGame.new("buoy_1234-900-0", [], "p1", 1)
+ assert_equal g1, g2
+ end
+
+ def test_not_equal
+ g1 = ShogiServer::BuoyGame.new("buoy_1234-900-0", [], "p1", 1)
+ g2 = ShogiServer::BuoyGame.new("buoy_1234-900-0", [], "p1", 2)
+ assert_not_equal g1, g2
+ end
+end
+
+
+class TestBuoy < Test::Unit::TestCase
+ def setup
+ @dir = File.dirname(__FILE__)
+ @filename = File.join(@dir, "buoy.yaml")
+ @conf = {:topdir => @dir}
+ @buoy = ShogiServer::Buoy.new @conf
+ end
+
+ def teardown
+ if File.exist? @filename
+ File.delete @filename
+ end
+ end
+
+ def test_game_name
+ assert(ShogiServer::Buoy.game_name?("buoy_hoge-1500-0"))
+ assert(ShogiServer::Buoy.game_name?("buoy_hoge-900-0"))
+ assert(ShogiServer::Buoy.game_name?("buoy_hoge-0-30"))
+ assert(!ShogiServer::Buoy.game_name?("buoyhoge-1500-0"))
+ assert(!ShogiServer::Buoy.game_name?("hoge-1500-0"))
+ end
+
+ def test_is_new_game1
+ assert @buoy.is_new_game?("buoy_123-900-0")
+ end
+
+ def test_add_game
+ game = ShogiServer::BuoyGame.new("buoy_1234-900-0", [], "p1", 1)
+ @buoy.add_game(game)
+ assert !@buoy.is_new_game?("buoy_1234-900-0")
+ game2 = @buoy.get_game(game.game_name)
+ assert_equal game, game2
+
+ @buoy.delete_game game
+ assert @buoy.is_new_game?("buoy_1234-900-0")
+ end
+
+ def test_update_game
+ game = ShogiServer::BuoyGame.new("buoy_1234-900-0", [], "p1", 2)
+ @buoy.add_game(game)
+ g2 = ShogiServer::BuoyGame.new(game.game_name, game.moves, game.owner, game.count-1)
+ @buoy.update_game(g2)
+
+ get = @buoy.get_game(g2.game_name)
+ assert_equal g2, get
+ end
+end
+
+
+class TestBuoyObserver < Test::Unit::TestCase
+ def setup
+ @dir = File.dirname(__FILE__)
+ @filename = File.join(@dir, "buoy.yaml")
+ @conf = {:topdir => @dir}
+ @buoy = ShogiServer::Buoy.new @conf
+ end
+
+ def teardown
+ if File.exist? @filename
+ File.delete @filename
+ end
+ end
+
+ def test_update_game_result_win
+ p1 = MockPlayer.new
+ p1.sente = true
+ p2 = MockPlayer.new
+ p2.sente = false
+
+ buoy_game = ShogiServer::BuoyGame.new("buoy_1234-900-0", [], "p1", 2)
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ @buoy.add_game buoy_game
+ assert !@buoy.is_new_game?(buoy_game.game_name)
+
+ game = MockGame.new
+ game.game_name = buoy_game.game_name
+ gr = ShogiServer::GameResultWin.new game, p1, p2
+
+ observer = ShogiServer::BuoyObserver.new
+ observer.update(gr)
+
+ assert !@buoy.is_new_game?(buoy_game.game_name)
+ buoy_game2 = @buoy.get_game(buoy_game.game_name)
+ assert_equal 1, buoy_game2.count
+ end
+
+ def test_update_game_result_win_zero
+ p1 = MockPlayer.new
+ p1.sente = true
+ p2 = MockPlayer.new
+ p2.sente = false
+
+ buoy_game = ShogiServer::BuoyGame.new("buoy_1234-900-0", [], "p1", 1)
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ @buoy.add_game buoy_game
+ assert !@buoy.is_new_game?(buoy_game.game_name)
+
+ game = MockGame.new
+ game.game_name = buoy_game.game_name
+ gr = ShogiServer::GameResultWin.new game, p1, p2
+
+ observer = ShogiServer::BuoyObserver.new
+ observer.update(gr)
+
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ end
+
+ def test_update_game_result_draw
+ p1 = MockPlayer.new
+ p1.sente = true
+ p2 = MockPlayer.new
+ p2.sente = false
+
+ buoy_game = ShogiServer::BuoyGame.new("buoy_1234-900-0", [], "p1", 2)
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ @buoy.add_game buoy_game
+ assert !@buoy.is_new_game?(buoy_game.game_name)
+
+ game = MockGame.new
+ game.game_name = buoy_game.game_name
+ gr = ShogiServer::GameResultDraw.new game, p1, p2
+
+ observer = ShogiServer::BuoyObserver.new
+ observer.update(gr)
+
+ assert !@buoy.is_new_game?(buoy_game.game_name)
+ buoy_game2 = @buoy.get_game(buoy_game.game_name)
+ assert_equal 2, buoy_game2.count
+ end
+end
$:.unshift File.join(File.dirname(__FILE__), "..")
+$topdir = File.expand_path File.dirname(__FILE__)
require 'test/unit'
+require 'mock_game'
+require 'mock_log_message'
+require 'test/mock_player'
require 'shogi_server/login'
require 'shogi_server/player'
require 'shogi_server/command'
-def log_warning(str)
- $stderr.puts str
-end
-
-def log_error(str)
- $stderr.puts str
-end
-
-class MockPlayer < ShogiServer::BasicPlayer
- attr_reader :out
- attr_accessor :game, :status, :protocol
- attr_accessor :game_name
-
- def initialize
- @out = []
- @game = nil
- @status = nil
- @protocol = nil
- @game_name = "dummy_game_name"
- end
-
- def write_safe(str)
- @out << str
- end
-end
-
-class MockGame
- attr_accessor :finish_flag
- attr_reader :log
- attr_accessor :prepared_expire
- attr_accessor :rejected
- attr_accessor :is_startable_status
- attr_accessor :started
- attr_accessor :game_id
-
- def initialize
- @finish_flag = false
- @log = []
- @prepared_expire = false
- @rejected = false
- @is_startable_status = false
- @started = false
- @game_id = "dummy_game_id"
- @monitoron_called = false
- @monitoroff_called = false
- end
-
- def handle_one_move(move, player)
- return @finish_flag
- end
-
- def log_game(str)
- @log << str
- end
-
- def prepared_expire?
- return @prepared_expire
- end
-
- def reject(str)
- @rejected = true
- end
-
- def is_startable_status?
- return @is_startable_status
- end
-
- def start
- @started = true
- end
-
- def show
- return "dummy_game_show"
- end
-
- def monitoron(player)
- @monitoron_called = true
- end
-
- def monitoroff(player)
- @monitoroff_called = true
- end
-end
class MockLeague
def initialize
def players
return [MockPlayer.new]
end
+
+ def event
+ return "test"
+ end
+
+ def dir
+ return $topdir
+ end
+
+ def get_player(status, game_id, sente, searcher)
+ if sente == true
+ $p1 = MockPlayer.new
+ $p1.name = "p1"
+ return $p1
+ elsif sente == false
+ $p2 = MockPlayer.new
+ $p2.name = "p2"
+ return $p2
+ elsif sente == nil
+ return nil
+ else
+ return nil
+ end
+ end
end
def setup
@p = MockPlayer.new
+ @p.name = "test_factory_method_player"
$league = MockLeague.new
end
assert_instance_of(ShogiServer::SpaceCommand, cmd)
end
+ def test_setbuoy_command
+ cmd = ShogiServer::Command.factory("%%SETBUOY buoy_test-1500-0 +7776FU", @p)
+ assert_instance_of(ShogiServer::SetBuoyCommand, cmd)
+ end
+
+ def test_setbuoy_command_with_counter
+ cmd = ShogiServer::Command.factory("%%SETBUOY buoy_test-1500-0 +7776FU 3", @p)
+ assert_instance_of(ShogiServer::SetBuoyCommand, cmd)
+ end
+
+ def test_deletebuoy_command
+ cmd = ShogiServer::Command.factory("%%DELETEBUOY buoy_test-1500-0", @p)
+ assert_instance_of(ShogiServer::DeleteBuoyCommand, cmd)
+ end
+
+ def test_getbuoycount_command
+ cmd = ShogiServer::Command.factory("%%GETBUOYCOUNT buoy_test-1500-0", @p)
+ assert_instance_of(ShogiServer::GetBuoyCountCommand, cmd)
+ end
+
def test_error
cmd = ShogiServer::Command.factory("should_be_error", @p)
assert_instance_of(ShogiServer::ErrorCommand, cmd)
end
end
+class BaseTestBuoyCommand < Test::Unit::TestCase
+ def setup
+ @p = MockPlayer.new
+ $p1 = nil
+ $p2 = nil
+
+ delete_buoy_yaml
+ @buoy = ShogiServer::Buoy.new
+ end
+
+ def teadown
+ delete_buoy_yaml
+ end
+
+ def delete_buoy_yaml
+ if File.exist?(File.join($topdir, "buoy.yaml"))
+ File.delete File.join($topdir, "buoy.yaml")
+ end
+ end
+
+ def test_dummy
+ assert true
+ end
+end
+
+
+#
+#
+class TestSetBuoyCommand < BaseTestBuoyCommand
+
+ def setup
+ super
+ @p.name = "set_buoy_player"
+ end
+
+ def test_split_moves1
+ cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_hoge-1500-0", "+7776FU", 1
+ rs = cmd.__send__ :split_moves, "+7776FU"
+ assert_equal ["+7776FU"], rs
+ end
+
+ def test_split_moves2
+ cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_hoge-1500-0", "+7776FU", 1
+ rs = cmd.__send__ :split_moves, "+7776FU-3334FU"
+ assert_equal ["+7776FU", "-3334FU"], rs
+ end
+
+ def test_split_moves3
+ cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_hoge-1500-0", "+7776FU", 1
+ assert_nothing_raised do
+ cmd.__send__ :split_moves, ""
+ end
+ end
+
+ def test_split_moves_error1
+ cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_hoge-1500-0", "+7776FU", 1
+ assert_raise ShogiServer::SetBuoyCommand::WrongMoves do
+ cmd.__send__ :split_moves, "dummy"
+ end
+ end
+
+ def test_call
+ assert @buoy.is_new_game?("buoy_hoge-1500-0")
+ cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_hoge-1500-0", "+7776FU", 1
+ rt = cmd.call
+ assert :continue, rt
+ assert !@buoy.is_new_game?("buoy_hoge-1500-0")
+ assert !$p1.out.empty?
+ assert !$p2.out.empty?
+ buoy_game2 = @buoy.get_game("buoy_hoge-1500-0")
+ assert_equal ShogiServer::BuoyGame.new("buoy_hoge-1500-0", "+7776FU", @p.name, 1), buoy_game2
+ end
+
+ def test_call_error_not_buoy_game_name
+ assert @buoy.is_new_game?("buoy_hoge-1500-0")
+ cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoyhoge-1500-0", "+7776FU", 1
+ rt = cmd.call
+ assert :continue, rt
+ assert !$p1
+ assert !$p2
+ assert @buoy.is_new_game?("buoy_hoge-1500-0")
+ end
+
+ def test_call_error_duplicated_game_name
+ assert @buoy.is_new_game?("buoy_duplicated-1500-0")
+ bg = ShogiServer::BuoyGame.new("buoy_duplicated-1500-0", ["+7776FU"], @p.name, 1)
+ @buoy.add_game bg
+ assert !@buoy.is_new_game?("buoy_duplicated-1500-0")
+
+ cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_duplicated-1500-0", "+7776FU", 1
+ rt = cmd.call
+ assert :continue, rt
+ assert !$p1
+ assert !$p2
+ assert !@buoy.is_new_game?("buoy_duplicated-1500-0")
+ end
+
+ def test_call_error_bad_moves
+ assert @buoy.is_new_game?("buoy_badmoves-1500-0")
+ cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_badmoves-1500-0", "+7776FU+8786FU", 1
+ rt = cmd.call
+ assert :continue, rt
+ assert !$p1
+ assert !$p2
+ assert @buoy.is_new_game?("buoy_badmoves-1500-0")
+ end
+
+ def test_call_error_bad_counter
+ assert @buoy.is_new_game?("buoy_badcounter-1500-0")
+ cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_badcounter-1500-0", "+7776FU", 0
+ rt = cmd.call
+ assert :continue, rt
+ assert !$p1
+ assert !$p2
+ assert @buoy.is_new_game?("buoy_badcounter-1500-0")
+ end
+end
+
+
+#
+#
+class TestDeleteBuoyCommand < BaseTestBuoyCommand
+ def test_call
+ buoy_game = ShogiServer::BuoyGame.new("buoy_testdeletebuoy-1500-0", "+7776FU", @p.name, 1)
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ @buoy.add_game buoy_game
+ assert !@buoy.is_new_game?(buoy_game.game_name)
+ cmd = ShogiServer::DeleteBuoyCommand.new "%%DELETEBUOY", @p, buoy_game.game_name
+ rt = cmd.call
+ assert :continue, rt
+ assert !$p1
+ assert !$p2
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ end
+
+ def test_call_not_exist
+ buoy_game = ShogiServer::BuoyGame.new("buoy_notexist-1500-0", "+7776FU", @p.name, 1)
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ cmd = ShogiServer::DeleteBuoyCommand.new "%%DELETEBUOY", @p, buoy_game.game_name
+ rt = cmd.call
+ assert :continue, rt
+ assert !$p1
+ assert !$p2
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ end
+
+ def test_call_another_player
+ buoy_game = ShogiServer::BuoyGame.new("buoy_anotherplayer-1500-0", "+7776FU", "another_player", 1)
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ @buoy.add_game(buoy_game)
+ assert !@buoy.is_new_game?(buoy_game.game_name)
+
+ cmd = ShogiServer::DeleteBuoyCommand.new "%%DELETEBUOY", @p, buoy_game.game_name
+ rt = cmd.call
+ assert :continue, rt
+ assert_equal "##[ERROR] you are not allowed to delete a buoy game that you did not set: buoy_anotherplayer-1500-0\n", @p.out.first
+ assert !@buoy.is_new_game?(buoy_game.game_name)
+ end
+end
+
+#
+#
+class TestGetBuoyCountCommand < BaseTestBuoyCommand
+ def test_call
+ buoy_game = ShogiServer::BuoyGame.new("buoy_testdeletebuoy-1500-0", "+7776FU", @p.name, 1)
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ @buoy.add_game buoy_game
+ assert !@buoy.is_new_game?(buoy_game.game_name)
+ cmd = ShogiServer::GetBuoyCountCommand.new "%%GETBUOYCOUNT", @p, buoy_game.game_name
+ rt = cmd.call
+ assert :continue, rt
+ assert_equal ["##[GETBUOYCOUNT] 1\n", "##[GETBUOYCOUNT] +OK\n"], @p.out
+ end
+
+ def test_call_not_exist
+ buoy_game = ShogiServer::BuoyGame.new("buoy_notexist-1500-0", "+7776FU", @p.name, 1)
+ assert @buoy.is_new_game?(buoy_game.game_name)
+ cmd = ShogiServer::GetBuoyCountCommand.new "%%GETBUOYCOUNT", @p, buoy_game.game_name
+ rt = cmd.call
+ assert :continue, rt
+ assert_equal ["##[GETBUOYCOUNT] 0\n", "##[GETBUOYCOUNT] +OK\n"], @p.out
+ end
+end
+
--- /dev/null
+$:.unshift File.join(File.dirname(__FILE__), "..")
+require 'test/unit'
+$topdir = File.expand_path(File.dirname(__FILE__))
+require 'shogi_server/config'
+
+
+class TestHash < Test::Unit::TestCase
+ def test_merge1
+ a = {:a => 1}
+ b = {:a => 2}
+ a.merge! b
+ assert_equal({:a => 2}, a)
+ end
+
+ def test_merge2
+ a = {:a => 1}
+ b = {:b => 2}
+ a.merge! b
+ assert_equal({:a => 1, :b => 2}, a)
+ end
+
+ def test_merge3
+ a = {:a => {:aa => 1}}
+ b = {:a => {:aa => 2}}
+ a.merge! b
+ assert_equal({:a => {:aa => 2}}, a)
+ end
+
+ def test_merge4
+ a = {:a => 1}
+ b = {:a => {:aa => 2}}
+ a.merge! b
+ assert_equal({:a => {:aa => 2}}, a)
+ end
+end
+
+
+class TestConfig < Test::Unit::TestCase
+ def setup
+ remove_config_file
+ end
+
+ def teardown
+ remove_config_file
+ end
+
+ def remove_config_file
+ delete_file File.join(File.expand_path(File.dirname(__FILE__)),
+ ShogiServer::Config::FILENAME)
+ delete_file File.join("/", "tmp", ShogiServer::Config::FILENAME)
+ end
+
+ def delete_file(path)
+ if File.exist? path
+ File.delete path
+ end
+ end
+
+ def test_top_dir1
+ expected = File.expand_path(File.dirname(__FILE__))
+ assert_equal expected, $topdir
+
+ conf = ShogiServer::Config.new
+ assert_equal expected, conf[:topdir]
+ end
+
+ def test_top_dir2
+ topdir_orig = $topdir
+ $topdir = "/should_be_replaced"
+ conf = ShogiServer::Config.new({:topdir => "/tmp"})
+ assert_equal "/tmp", conf[:topdir]
+ $topdir = topdir_orig
+ end
+
+ def test_top_dir3
+ topdir_orig = $topdir
+ $topdir = "/should_be_replaced"
+ conf = ShogiServer::Config.new({"topdir" => "/tmp"})
+ assert_equal "/tmp", conf[:topdir]
+ $topdir = topdir_orig
+ end
+
+ def test_braces1
+ conf = ShogiServer::Config.new({:a => 1})
+ assert_equal 1, conf[:a]
+ end
+
+ def test_braces2
+ conf = ShogiServer::Config.new({:a => {:b => 1}})
+ assert_equal 1, conf[:a, :b]
+ end
+
+ def test_braces3
+ conf = ShogiServer::Config.new({:a => {:b => 1}})
+ assert_equal nil, conf[:b]
+ end
+end
+
end
end
-class TestChatCommand < BaseClient
+class TestFunctionalChatCommand < BaseClient
def test_chat
cmd "%%CHAT Hello"
sleep 1
--- /dev/null
+$:.unshift File.join(File.dirname(__FILE__), "..")
+require 'test/unit'
+require 'test/mock_player'
+require 'shogi_server/board'
+require 'shogi_server/game'
+require 'shogi_server/player'
+
+def log_message(str)
+ $stderr.puts str
+end
+
+def log_warning(str)
+ $stderr.puts str
+end
+
+def log_error(str)
+ $stderr.puts str
+end
+
+$league = ShogiServer::League.new(File.dirname(__FILE__))
+$league.event = "test"
+
+class TestGame < Test::Unit::TestCase
+
+ def test_new
+ game_name = "hoge-1500-0"
+ board = ShogiServer::Board.new
+ board.initial
+ p1 = MockPlayer.new
+ p1.sente = true
+ p1.name = "p1"
+ p2 = MockPlayer.new
+ p2.sente = false
+ p2.name = "p2"
+
+ game = ShogiServer::Game.new game_name, p1, p2, board
+ assert_equal "", game.last_move
+
+ p1_out = <<EOF
+BEGIN Game_Summary
+Protocol_Version:1.1
+Protocol_Mode:Server
+Format:Shogi 1.0
+Declaration:Jishogi 1.1
+Game_ID:#{game.game_id}
+Name+:p1
+Name-:p2
+Your_Turn:+
+Rematch_On_Draw:NO
+To_Move:+
+BEGIN Time
+Time_Unit:1sec
+Total_Time:1500
+Byoyomi:0
+Least_Time_Per_Move:1
+END Time
+BEGIN Position
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
+P4 * * * * * * * * *
+P5 * * * * * * * * *
+P6 * * * * * * * * *
+P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
++
+END Position
+END Game_Summary
+EOF
+ assert_equal(p1_out, p1.out.first)
+
+ p2_out = <<EOF
+BEGIN Game_Summary
+Protocol_Version:1.1
+Protocol_Mode:Server
+Format:Shogi 1.0
+Declaration:Jishogi 1.1
+Game_ID:#{game.game_id}
+Name+:p1
+Name-:p2
+Your_Turn:-
+Rematch_On_Draw:NO
+To_Move:+
+BEGIN Time
+Time_Unit:1sec
+Total_Time:1500
+Byoyomi:0
+Least_Time_Per_Move:1
+END Time
+BEGIN Position
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
+P4 * * * * * * * * *
+P5 * * * * * * * * *
+P6 * * * * * * * * *
+P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
++
+END Position
+END Game_Summary
+EOF
+ assert_equal(p2_out, p2.out.first)
+
+ file = Pathname.new(game.logfile)
+ log = file.read
+ assert_equal(<<EOF, log.gsub(/^\$START_TIME.*?\n/,''))
+V2
+N+p1
+N-p2
+$EVENT:#{game.game_id}
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
+P4 * * * * * * * * *
+P5 * * * * * * * * *
+P6 * * * * * * * * *
+P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
++
+EOF
+ end
+
+ def test_new_buoy_1_move
+ game_name = "buoyhoge-1500-0"
+ board = ShogiServer::Board.new
+ board.set_from_moves ["+7776FU"]
+ p1 = MockPlayer.new
+ p1.sente = true
+ p1.name = "p1"
+ p2 = MockPlayer.new
+ p2.sente = false
+ p2.name = "p2"
+
+ game = ShogiServer::Game.new game_name, p1, p2, board
+ assert_equal "+7776FU,T1", game.last_move
+
+ p1_out = <<EOF
+BEGIN Game_Summary
+Protocol_Version:1.1
+Protocol_Mode:Server
+Format:Shogi 1.0
+Declaration:Jishogi 1.1
+Game_ID:#{game.game_id}
+Name+:p1
+Name-:p2
+Your_Turn:+
+Rematch_On_Draw:NO
+To_Move:+
+BEGIN Time
+Time_Unit:1sec
+Total_Time:1500
+Byoyomi:0
+Least_Time_Per_Move:1
+END Time
+BEGIN Position
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
+P4 * * * * * * * * *
+P5 * * * * * * * * *
+P6 * * +FU * * * * * *
+P7+FU+FU * +FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
+-
+END Position
+END Game_Summary
+EOF
+ assert_equal(p1_out, p1.out.first)
+
+ p2_out = <<EOF
+BEGIN Game_Summary
+Protocol_Version:1.1
+Protocol_Mode:Server
+Format:Shogi 1.0
+Declaration:Jishogi 1.1
+Game_ID:#{game.game_id}
+Name+:p1
+Name-:p2
+Your_Turn:-
+Rematch_On_Draw:NO
+To_Move:+
+BEGIN Time
+Time_Unit:1sec
+Total_Time:1500
+Byoyomi:0
+Least_Time_Per_Move:1
+END Time
+BEGIN Position
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
+P4 * * * * * * * * *
+P5 * * * * * * * * *
+P6 * * +FU * * * * * *
+P7+FU+FU * +FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
+-
+END Position
+END Game_Summary
+EOF
+ assert_equal(p2_out, p2.out.first)
+
+ file = Pathname.new(game.logfile)
+ log = file.read
+ assert_equal(<<EOF, log.gsub(/^\$START_TIME.*?\n/,''))
+V2
+N+p1
+N-p2
+$EVENT:#{game.game_id}
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
+P4 * * * * * * * * *
+P5 * * * * * * * * *
+P6 * * * * * * * * *
+P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
++
+'buoy game starting with 1 moves
++7776FU
+T1
+EOF
+ end
+
+ def test_new_buoy_2_moves
+ game_name = "buoyhoge-1500-0"
+ board = ShogiServer::Board.new
+ board.set_from_moves ["+7776FU", "-3334FU"]
+ p1 = MockPlayer.new
+ p1.sente = true
+ p1.name = "p1"
+ p2 = MockPlayer.new
+ p2.sente = false
+ p2.name = "p2"
+
+ game = ShogiServer::Game.new game_name, p1, p2, board
+ assert_equal "-3334FU,T1", game.last_move
+
+ p1_out = <<EOF
+BEGIN Game_Summary
+Protocol_Version:1.1
+Protocol_Mode:Server
+Format:Shogi 1.0
+Declaration:Jishogi 1.1
+Game_ID:#{game.game_id}
+Name+:p1
+Name-:p2
+Your_Turn:+
+Rematch_On_Draw:NO
+To_Move:+
+BEGIN Time
+Time_Unit:1sec
+Total_Time:1500
+Byoyomi:0
+Least_Time_Per_Move:1
+END Time
+BEGIN Position
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU * -FU-FU
+P4 * * * * * * -FU * *
+P5 * * * * * * * * *
+P6 * * +FU * * * * * *
+P7+FU+FU * +FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
++
+END Position
+END Game_Summary
+EOF
+ assert_equal(p1_out, p1.out.first)
+
+ p2_out = <<EOF
+BEGIN Game_Summary
+Protocol_Version:1.1
+Protocol_Mode:Server
+Format:Shogi 1.0
+Declaration:Jishogi 1.1
+Game_ID:#{game.game_id}
+Name+:p1
+Name-:p2
+Your_Turn:-
+Rematch_On_Draw:NO
+To_Move:+
+BEGIN Time
+Time_Unit:1sec
+Total_Time:1500
+Byoyomi:0
+Least_Time_Per_Move:1
+END Time
+BEGIN Position
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU * -FU-FU
+P4 * * * * * * -FU * *
+P5 * * * * * * * * *
+P6 * * +FU * * * * * *
+P7+FU+FU * +FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
++
+END Position
+END Game_Summary
+EOF
+ assert_equal(p2_out, p2.out.first)
+
+ file = Pathname.new(game.logfile)
+ log = file.read
+ assert_equal(<<EOF, log.gsub(/^\$START_TIME.*?\n/,''))
+V2
+N+p1
+N-p2
+$EVENT:#{game.game_id}
+P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
+P2 * -HI * * * * * -KA *
+P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
+P4 * * * * * * * * *
+P5 * * * * * * * * *
+P6 * * * * * * * * *
+P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
+P8 * +KA * * * * * +HI *
+P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
++
+'buoy game starting with 2 moves
++7776FU
+T1
+-3334FU
+T1
+EOF
+ end
+end
+
--- /dev/null
+$:.unshift File.join(File.dirname(__FILE__), "..")
+require 'shogi_server/player'
+
+class MockPlayer < ShogiServer::BasicPlayer
+ attr_reader :out
+ attr_accessor :game, :status, :protocol
+ attr_accessor :game_name
+ attr_reader :socket_buffer
+
+ def initialize
+ @name = "mock_player"
+ @out = []
+ @game = nil
+ @status = nil
+ @protocol = nil
+ @game_name = "dummy_game_name"
+ @socket_buffer = []
+ end
+
+ def write_safe(str)
+ @out << str
+ end
+end
+
+