OSDN Git Service

Experimantal implementation for specified games, codenamed Buoy.
authorbeatles <beatles@b8c68f68-1e22-0410-b08e-880e1f8202b4>
Wed, 29 Jul 2009 13:48:31 +0000 (13:48 +0000)
committerbeatles <beatles@b8c68f68-1e22-0410-b08e-880e1f8202b4>
Wed, 29 Jul 2009 13:48:31 +0000 (13:48 +0000)
15 files changed:
changelog
shogi-server
shogi_server.rb
shogi_server/board.rb
shogi_server/command.rb
shogi_server/game.rb
shogi_server/pairing.rb
test/TC_ALL.rb
test/TC_board.rb
test/TC_buoy.rb [new file with mode: 0644]
test/TC_command.rb
test/TC_config.rb [new file with mode: 0644]
test/TC_functional.rb
test/TC_game.rb [new file with mode: 0644]
test/mock_player.rb [new file with mode: 0644]

index 7c5a250..39131eb 100644 (file)
--- a/changelog
+++ b/changelog
@@ -1,3 +1,35 @@
+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]
index 2c7f920..adc18bc 100755 (executable)
 $topdir = nil
 $league = nil
 $logger = nil
+$config = nil
 $:.unshift File.dirname(__FILE__)
 require 'shogi_server'
+require 'shogi_server/config'
 require 'tempfile'
 
 #################################################
@@ -335,6 +337,7 @@ def main
   
   $options = parse_command_line
   check_command_line
+  $config = ShogiServer::Config.new $options
 
   $league = ShogiServer::League.new($topdir)
 
index 4632c64..587798a 100644 (file)
@@ -38,6 +38,8 @@ require 'shogi_server/player'
 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
 
index 1fe8ee9..9c9f6d4 100644 (file)
@@ -20,6 +20,7 @@
 module ShogiServer # for a namespace
 
 class Board
+
   def initialize(move_count=0)
     @sente_hands = Array::new
     @gote_hands  = Array::new
@@ -29,9 +30,15 @@ class Board
     @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))
@@ -70,9 +77,14 @@ class Board
     @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
@@ -125,7 +137,7 @@ class Board
           end
           x = x - 1
         end
-      elsif (str =~ /^P([\+\-])/)
+      when /^P([\+\-])/
         sg = $1
         if (sg == "+")
           sente = true
@@ -155,10 +167,31 @@ class Board
             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
index 6334c28..5910a3e 100644 (file)
@@ -1,3 +1,22 @@
+## $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'
 
@@ -55,6 +74,20 @@ module ShogiServer
         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
@@ -142,7 +175,7 @@ module ShogiServer
       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
@@ -349,6 +382,14 @@ module ShogiServer
       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))
@@ -371,6 +412,7 @@ module ShogiServer
 
       if (rival)
         @player.game_name = @game_name
+        
         if ((@my_sente_str == "*") && (rival.sente == nil))
           if (rand(2) == 0)
             @player.sente = true
@@ -392,7 +434,31 @@ module ShogiServer
         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"
@@ -538,5 +604,130 @@ module ShogiServer
     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
index 02853e1..ccb537e 100644 (file)
@@ -76,6 +76,11 @@ class GameResult
        $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
@@ -262,7 +267,7 @@ class Game
 
   @@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+)$/)
@@ -277,12 +282,17 @@ class Game
     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"
@@ -305,8 +315,6 @@ class Game
 
     log_message(sprintf("game created %s", @game_id))
 
-    @board = Board::new
-    @board.initial
     @start_time = nil
     @fh = open(@logfile, "w")
     @fh.sync = true
@@ -318,6 +326,9 @@ class Game
   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
@@ -528,6 +539,13 @@ EOM
       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()
@@ -583,18 +601,7 @@ Byoyomi:#{@byoyomi}
 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
index 057fd70..2f5c616 100644 (file)
@@ -129,7 +129,9 @@ module ShogiServer
       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
 
index 245c3a3..43282af 100644 (file)
@@ -2,10 +2,13 @@ $:.unshift File.dirname(__FILE__)
 
 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'
index 7f78f84..8ca437d 100644 (file)
@@ -685,3 +685,52 @@ EOM
     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
+
diff --git a/test/TC_buoy.rb b/test/TC_buoy.rb
new file mode 100644 (file)
index 0000000..6311ff4
--- /dev/null
@@ -0,0 +1,154 @@
+$:.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
index 7c23e1b..d343c8b 100644 (file)
@@ -1,92 +1,13 @@
 $:.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
@@ -105,6 +26,30 @@ class MockLeague
   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
 
 
@@ -112,6 +57,7 @@ class TestFactoryMethod < Test::Unit::TestCase
 
   def setup
     @p = MockPlayer.new
+    @p.name = "test_factory_method_player"
     $league = MockLeague.new
   end
 
@@ -225,6 +171,26 @@ class TestFactoryMethod < Test::Unit::TestCase
     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)
@@ -706,4 +672,188 @@ class TestErrorCommand < Test::Unit::TestCase
   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
+
 
diff --git a/test/TC_config.rb b/test/TC_config.rb
new file mode 100644 (file)
index 0000000..9dc3fd4
--- /dev/null
@@ -0,0 +1,98 @@
+$:.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
+
index c29c160..27e9171 100644 (file)
@@ -287,7 +287,7 @@ class TestDuplicatedMoves < BaseClient
   end
 end
 
-class TestChatCommand < BaseClient
+class TestFunctionalChatCommand < BaseClient
   def test_chat
     cmd "%%CHAT Hello"
     sleep 1
diff --git a/test/TC_game.rb b/test/TC_game.rb
new file mode 100644 (file)
index 0000000..be3ccab
--- /dev/null
@@ -0,0 +1,339 @@
+$:.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
+
diff --git a/test/mock_player.rb b/test/mock_player.rb
new file mode 100644 (file)
index 0000000..55319fd
--- /dev/null
@@ -0,0 +1,25 @@
+$:.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
+
+