OSDN Git Service

Merge branch '201303-yamashita_matching' into wdoor-stable
authorDaigo Moriwaki <beatles@users.sourceforge.jp>
Mon, 4 Nov 2013 06:05:57 +0000 (15:05 +0900)
committerDaigo Moriwaki <beatles@users.sourceforge.jp>
Mon, 4 Nov 2013 06:05:57 +0000 (15:05 +0900)
Conflicts:
changelog

changelog
mk_rate
shogi_server/board.rb
shogi_server/buoy.rb
shogi_server/command.rb
shogi_server/game.rb
shogi_server/time_clock.rb [new file with mode: 0644]
test/TC_ALL.rb
test/TC_command.rb
test/TC_fork.rb [new file with mode: 0644]
test/TC_time_clock.rb [new file with mode: 0644]

index 8388526..6e69cdc 100644 (file)
--- a/changelog
+++ b/changelog
@@ -1,3 +1,37 @@
+2013-11-04  Daigo Moriwaki <daigo at debian dot org>
+
+       * [mk_rate]
+         - Added a new option, --ignore, which is imported from
+           mk_rate-from-grep.
+
+2013-09-08  Daigo Moriwaki <daigo at debian dot org>
+
+       * [shogi-server]
+         - shogi_server/{game,time_clock}.rb:
+           When StopWatchClock is used, "Time_Unit:" of starting messages
+           in CSA protocol supplies "1min".
+
+2013-04-07  Daigo Moriwaki <daigo at debian dot org>
+
+       * [shogi-server]
+         - shogi_server/{game,time_clock}.rb:
+           Adds variations of thinking time calculation: ChessClock
+           (current) and StopWatchClock (new).
+           StopWatchClock, which is usually used at official games of human
+           professional players, is a clock where thiking time less than a
+           miniute is regarded as zero.
+           To select StopWatchClock, use a special game name with "060"
+           byoyomi time. ex. "gamename_1500_060".
+
+2013-03-31  Daigo Moriwaki <daigo at debian dot org>
+
+       * [shogi-server]
+         - %%FORK command: %%FORK <source_game> [<new_buoy_game>] [<nth-move>]
+           The new_buoy_game parameter is now optional. If it is not
+           supplied, Shogi-server generates a new buoy game name from
+           source_game.
+         - command.rb: More elaborate error messages for the %%GAME command.
+
 2013-03-20  Daigo Moriwaki <daigo at debian dot org>
 
        * [shogi-server]
@@ -8,6 +42,22 @@
            It is based on a discussion with Yamashita-san on
            http://www.sgtpepper.net/kaneko/diary/20120511.html.
 
+2013-02-23  Daigo Moriwaki <daigo at debian dot org>
+
+       * [shogi-server]
+         - New command: %%FORK <source_game> <new_buoy_game> [<nth-move>]
+           Fork a new game from the posistion where the n-th (starting from
+           one) 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.
+           - The objective of this command: The shogi-server may be used as
+           the back end server of computer-human match where a human player
+           plays with a real board and someone, or a proxy, inputs moves to
+           the shogi-server. If the proxy happens to enter a wrong move,
+           with this command you can restart a new buoy game from the
+           previous stable position.
+           ex. %%FORK server-denou-14400-60+p1+p2+20130223185013 buoy_denou-14400-60
+
 2012-12-30  Daigo Moriwaki <daigo at debian dot org>
 
        * [shogi-server]
diff --git a/mk_rate b/mk_rate
index 2d2e94c..3678b45 100755 (executable)
--- a/mk_rate
+++ b/mk_rate
 #   m [days] (default  7)
 #   after m days, the half-life effect works
 #
+# --ignore::
+#   m [days] (default  365*2)
+#   old results will be ignored
+#
 # --fixed-rate-player::
 #   player whose rate is fixed at the rate
 #
@@ -664,6 +668,11 @@ def parse(line)
   return if state == "abnormal"
   time = Time.parse(time)
   return if $options["base-date"] < time
+  how_long_days = ($options["base-date"] - time)/(3600*24)
+  if (how_long_days > $options["ignore"])
+    return
+  end
+
   black_id = identify_id(black_id)
   white_id = identify_id(white_id)
 
@@ -701,6 +710,8 @@ OPTOINS:
   --half-life         n [days] (default 60)
   --half-life-ignore  m [days] (default  7)
                       after m days, half-life effect works
+  --ignore            n [days] (default 730 [=365*2]).
+                      Results older than n days from the 'base-date' are ignored.
   --fixed-rate-player player whose rate is fixed at the rate
   --fixed-rate        rate 
   --skip-draw-games   skip draw games. [default: draw games are counted in
@@ -716,6 +727,7 @@ def main
     ["--half-life",         GetoptLong::REQUIRED_ARGUMENT],
     ["--half-life-ignore",  GetoptLong::REQUIRED_ARGUMENT],
     ["--help", "-h",        GetoptLong::NO_ARGUMENT],
+    ["--ignore",            GetoptLong::REQUIRED_ARGUMENT],
     ["--fixed-rate-player", GetoptLong::REQUIRED_ARGUMENT],
     ["--fixed-rate",        GetoptLong::REQUIRED_ARGUMENT],
     ["--skip-draw-games",   GetoptLong::NO_ARGUMENT])
@@ -748,6 +760,8 @@ def main
   $options["half-life"] = $options["half-life"].to_i
   $options["half-life-ignore"] ||= 7
   $options["half-life-ignore"] = $options["half-life-ignore"].to_i
+  $options["ignore"] ||= 365*2
+  $options["ignore"] = $options["ignore"].to_i
   $options["fixed-rate"] = $options["fixed-rate"].to_i if $options["fixed-rate"]
 
   if ARGV.empty?
index d2b304b..c006176 100644 (file)
@@ -43,17 +43,42 @@ EOF
 
   # 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"]
+  # @param moves a moves line. Ex. "+776FU-3334FU" or
+  #              moves with times. Ex "+776FU,T2-3334FU,T5"
+  # @return an array of a move string. Ex. ["+7776FU", "-3334FU"] or
+  #         an array of arrays. Ex. [["+7776FU","T2"], ["-3334FU", "T5"]]
   #
   def Board.split_moves(moves)
     ret = []
 
-    rs = moves.gsub %r{[\+\-]\d{4}\w{2}} do |s|
-           ret << s
-           ""
-         end
-    raise WrongMoves, rs unless rs.empty?
+    i=0
+    tmp = ""
+    while i<moves.size
+      if moves[i,1] == "+" ||
+         moves[i,1] == "-" ||
+         i == moves.size - 1
+        if i == moves.size - 1
+          tmp << moves[i,1]
+        end
+        unless tmp.empty?
+          a = tmp.split(",")
+          if a[0].size != 7
+            raise WrongMoves, a[0]
+          end
+          if a.size == 1 # "+7776FU"
+            ret << a[0]
+          else           # "+7776FU,T2"
+            unless /^T\d+/ =~ a[1] 
+              raise WrongMoves, a[1]
+            end
+            ret << a
+          end
+          tmp = ""
+        end
+      end
+      tmp << moves[i,1]
+      i += 1
+    end
 
     return ret
   end
@@ -230,14 +255,21 @@ EOF
 
   # 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"]
+  # @param moves an array of moves. ex. ["+7776FU", "-3334FU"] or
+  #        an array of arrays. ex. [["+7776FU","T2"], ["-3334FU","T5"]]
   #
   def set_from_moves(moves)
     initial()
     return :normal if moves.empty?
     rt = nil
     moves.each do |move|
-      rt = handle_one_move(move, @teban)
+      rt = nil
+      case move
+      when Array
+        rt = handle_one_move(move[0], @teban)
+      when String
+        rt = handle_one_move(move, @teban)
+      end
       raise ArgumentError, "bad moves: #{moves}" unless rt == :normal
     end
     @initial_moves = moves.dup
index c46c575..e2f88ee 100644 (file)
@@ -21,7 +21,7 @@ module ShogiServer
     end
 
     def ==(rhs)
-      return (@game_name == rhs.game_name && 
+      return (@game_name == rhs.game_name &&
               @moves == rhs.moves &&
               @owner == rhs.owner &&
               @count == rhs.count)
index a3b48a7..8a2b38c 100644 (file)
@@ -69,6 +69,9 @@ module ShogiServer
         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)
@@ -94,6 +97,19 @@ module ShogiServer
       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 /^%%%[^%]/
@@ -481,7 +497,16 @@ module ShogiServer
 
     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
@@ -666,9 +691,9 @@ module ShogiServer
   # Command for an error
   #
   class ErrorCommand < Command
-    def initialize(str, player)
-      super
-      @msg = nil
+    def initialize(str, player, msg=nil)
+      super(str, player)
+      @msg = msg || "unknown command"
     end
     attr_reader :msg
 
@@ -676,7 +701,7 @@ module ShogiServer
       cmd = @str.chomp
       # Aim to hide a possible password
       cmd.gsub!(/LOGIN\s*(\w+)\s+.*/i, 'LOGIN \1...')
-      @msg = "##[ERROR] unknown command %s\n" % [cmd]
+      @msg = "##[ERROR] %s: %s\n" % [@msg, cmd]
       @player.write_safe(@msg)
       log_error(@msg)
       return :continue
@@ -809,4 +834,69 @@ module ShogiServer
     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
index ee90d52..fb7ed18 100644 (file)
@@ -19,6 +19,7 @@
 
 require 'shogi_server/league/floodgate'
 require 'shogi_server/game_result'
+require 'shogi_server/time_clock'
 require 'shogi_server/util'
 
 module ShogiServer # for a namespace
@@ -69,6 +70,8 @@ class Game
     if (@game_name =~ /-(\d+)-(\d+)$/)
       @total_time = $1.to_i
       @byoyomi = $2.to_i
+
+      @time_clock = TimeClock::factory(Least_Time_Per_Move, @game_name)
     end
 
     if (player0.sente)
@@ -87,7 +90,16 @@ class Game
     @sente.game = self
     @gote.game  = self
 
-    @last_move = @board.initial_moves.empty? ? "" : "%s,T1" % [@board.initial_moves.last]
+    @last_move = ""
+    unless @board.initial_moves.empty?
+      last_move = @board.initial_moves.last
+      case last_move
+      when Array
+        @last_move = last_move.join(",")
+      when String
+        @last_move = "%s,T1" % [last_move]
+      end
+    end
     @current_turn = @board.initial_moves.size
 
     @sente.status = "agree_waiting"
@@ -110,6 +122,7 @@ class Game
     $league.games[@game_id] = self
 
     log_message(sprintf("game created %s", @game_id))
+    log_message("    " + @time_clock.to_s)
 
     @start_time = nil
     @fh = open(@logfile, "w")
@@ -118,7 +131,7 @@ class Game
 
     propose
   end
-  attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors
+  attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors, :time_clock
   attr_accessor :last_move, :current_turn
   attr_reader   :result, :prepared_time
 
@@ -218,22 +231,16 @@ class Game
       return nil
     end
 
-    finish_flag = true
     @end_time = end_time
-    t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max
-    
+    finish_flag = true
     move_status = nil
-    if ((@current_player.mytime - t <= -@byoyomi) && 
-        ((@total_time > 0) || (@byoyomi > 0)))
+
+    if (@time_clock.timeout?(@current_player, @start_time, @end_time))
       status = :timeout
     elsif (str == :timeout)
       return false            # time isn't expired. players aren't swapped. continue game
     else
-      @current_player.mytime -= t
-      if (@current_player.mytime < 0)
-        @current_player.mytime = 0
-      end
-
+      t = @time_clock.process_time(@current_player, @start_time, @end_time)
       move_status = @board.handle_one_move(str, @sente == @current_player)
       # log_debug("move_status: %s for %s's %s" % [move_status, @sente == @current_player ? "BLACK" : "WHITE", str])
 
@@ -332,8 +339,14 @@ class Game
     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"
+        case move
+        when Array
+          @fh.puts move[0]
+          @fh.puts move[1]
+        when String
+          @fh.puts move
+          @fh.puts "T1"
+        end
       end
     end
   end
@@ -351,7 +364,7 @@ Name-:#{@gote.name}
 Rematch_On_Draw:NO
 To_Move:+
 BEGIN Time
-Time_Unit:1sec
+Time_Unit:#{@time_clock.time_unit}
 Total_Time:#{@total_time}
 Byoyomi:#{@byoyomi}
 Least_Time_Per_Move:#{Least_Time_Per_Move}
@@ -385,14 +398,21 @@ Your_Turn:#{sg_flag}
 Rematch_On_Draw:NO
 To_Move:#{@board.teban ? "+" : "-"}
 BEGIN Time
-Time_Unit:1sec
+Time_Unit:#{@time_clock.time_unit}
 Total_Time:#{@total_time}
 Byoyomi:#{@byoyomi}
 Least_Time_Per_Move:#{Least_Time_Per_Move}
 END Time
 BEGIN Position
 #{@board.initial_string.chomp}
-#{@board.initial_moves.collect {|m| m + ",T1"}.join("\n")}
+#{@board.initial_moves.collect do |m|
+  case m
+  when Array
+    m.join(",")
+  when String
+    m + ",T1"
+  end
+end.join("\n")}
 END Position
 END Game_Summary
 EOM
@@ -408,6 +428,21 @@ EOM
 
     return false
   end
+
+  # Read the .csa file and returns an array of moves and times.
+  # ex. [["+7776FU","T2"], ["-3334FU","T5"]]
+  #
+  def read_moves
+    ret = []
+    IO.foreach(@logfile) do |line|
+      if /^[\+\-]\d{4}[A-Z]{2}/ =~ line
+        ret << [line.chomp]
+      elsif /^T\d*/ =~ line
+        ret[-1] << line.chomp
+      end
+    end
+    return ret
+  end
   
   private
   
diff --git a/shogi_server/time_clock.rb b/shogi_server/time_clock.rb
new file mode 100644 (file)
index 0000000..6426fda
--- /dev/null
@@ -0,0 +1,139 @@
+## $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
+
+module ShogiServer # for a namespace
+
+# Abstract class to caclulate thinking time.
+#
+class TimeClock
+
+  def TimeClock.factory(least_time_per_move, game_name)
+    total_time_str = nil
+    byoyomi_str = nil
+    if (game_name =~ /-(\d+)-(\d+)$/)
+      total_time_str = $1
+      byoyomi_str    = $2
+    end
+    total_time = total_time_str.to_i
+    byoyomi    = byoyomi_str.to_i
+    if (byoyomi_str == "060")
+      @time_clock = StopWatchClock.new(least_time_per_move, total_time, byoyomi)
+    else
+      @time_clock = ChessClock.new(least_time_per_move, total_time, byoyomi)
+    end
+  end
+
+  def initialize(least_time_per_move, total_time, byoyomi)
+    @least_time_per_move = least_time_per_move
+    @total_time = total_time
+    @byoyomi     = byoyomi
+  end
+
+  # Returns thinking time duration
+  #
+  def time_duration(start_time, end_time)
+    # implement this
+    return 9999999
+  end
+
+  # Returns what "Time_Unit:" in CSA protocol should provide.
+  #
+  def time_unit
+    return "1sec"
+  end
+
+  # If thinking time runs out, returns true; false otherwise.
+  #
+  def timeout?(player, start_time, end_time)
+    # implement this
+    return true
+  end
+
+  # Updates a player's remaining time and returns thinking time.
+  #
+  def process_time(player, start_time, end_time)
+    t = time_duration(start_time, end_time)
+    
+    player.mytime -= t
+    if (player.mytime < 0)
+      player.mytime = 0
+    end
+
+    return t
+  end
+end
+
+# Calculates thinking time with chess clock.
+#
+class ChessClock < TimeClock
+  def initialize(least_time_per_move, total_time, byoyomi)
+    super
+  end
+
+  def time_duration(start_time, end_time)
+    return [(end_time - start_time).floor, @least_time_per_move].max
+  end
+
+  def timeout?(player, start_time, end_time)
+    t = time_duration(start_time, end_time)
+
+    if ((player.mytime - t <= -@byoyomi) && 
+        ((@total_time > 0) || (@byoyomi > 0)))
+      return true
+    else
+      return false
+    end
+  end
+
+  def to_s
+    return "ChessClock: LeastTimePerMove %d; TotalTime %d; Byoyomi %d" % [@least_time_per_move, @total_time, @byoyomi]
+  end
+end
+
+class StopWatchClock < TimeClock
+  def initialize(least_time_per_move, total_time, byoyomi)
+    super
+  end
+
+  def time_unit
+    return "1min"
+  end
+
+  def time_duration(start_time, end_time)
+    t = [(end_time - start_time).floor, @least_time_per_move].max
+    return (t / @byoyomi) * @byoyomi
+  end
+
+  def timeout?(player, start_time, end_time)
+    t = time_duration(start_time, end_time)
+
+    if (player.mytime <= t)
+      return true
+    else
+      return false
+    end
+  end
+
+  def to_s
+    return "StopWatchClock: LeastTimePerMove %d; TotalTime %d; Byoyomi %d" % [@least_time_per_move, @total_time, @byoyomi]
+  end
+end
+
+end
index 1c28109..ca66262 100644 (file)
@@ -9,6 +9,7 @@ require 'TC_floodgate'
 require 'TC_floodgate_history'
 require 'TC_floodgate_next_time_generator'
 require 'TC_floodgate_thread.rb'
+require 'TC_fork'
 require 'TC_functional'
 require 'TC_game'
 require 'TC_game_result'
@@ -23,6 +24,7 @@ require 'TC_oute_sennichite'
 require 'TC_pairing'
 require 'TC_player'
 require 'TC_rating'
+require 'TC_time_clock'
 require 'TC_uchifuzume'
 require 'TC_usi'
 require 'TC_util'
index f23062b..3cb3795 100644 (file)
@@ -228,6 +228,16 @@ class TestFactoryMethod < Test::Unit::TestCase
     assert_instance_of(ShogiServer::GetBuoyCountCommand, cmd)
   end
 
+  def test_fork_command
+    cmd = ShogiServer::Command.factory("%%FORK server-denou-14400-60+p1+p2+20130223185013 buoy_denou-14400-60", @p)
+    assert_instance_of(ShogiServer::ForkCommand, cmd)
+  end
+
+  def test_fork_command2
+    cmd = ShogiServer::Command.factory("%%FORK server-denou-14400-60+p1+p2+20130223185013", @p)
+    assert_instance_of(ShogiServer::ForkCommand, cmd)
+  end
+
   def test_void_command
     cmd = ShogiServer::Command.factory("%%%HOGE", @p)
     assert_instance_of(ShogiServer::VoidCommand, cmd)
@@ -237,29 +247,29 @@ class TestFactoryMethod < Test::Unit::TestCase
     cmd = ShogiServer::Command.factory("should_be_error", @p)
     assert_instance_of(ShogiServer::ErrorCommand, cmd)
     cmd.call
-    assert_match /unknown command should_be_error/, cmd.msg
+    assert_match /unknown command: should_be_error/, cmd.msg
   end
 
   def test_error_login
     cmd = ShogiServer::Command.factory("LOGIN hoge foo", @p)
     assert_instance_of(ShogiServer::ErrorCommand, cmd)
     cmd.call
-    assert_no_match /unknown command LOGIN hoge foo/, cmd.msg
+    assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg
 
     cmd = ShogiServer::Command.factory("LOGin hoge foo", @p)
     assert_instance_of(ShogiServer::ErrorCommand, cmd)
     cmd.call
-    assert_no_match /unknown command LOGIN hoge foo/, cmd.msg
+    assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg
 
     cmd = ShogiServer::Command.factory("LOGIN  hoge foo", @p)
     assert_instance_of(ShogiServer::ErrorCommand, cmd)
     cmd.call
-    assert_no_match /unknown command LOGIN hoge foo/, cmd.msg
+    assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg
 
     cmd = ShogiServer::Command.factory("LOGINhoge foo", @p)
     assert_instance_of(ShogiServer::ErrorCommand, cmd)
     cmd.call
-    assert_no_match /unknown command LOGIN hoge foo/, cmd.msg
+    assert_no_match /unknown command: LOGIN hoge foo/, cmd.msg
   end
 end
 
@@ -939,6 +949,28 @@ end
 
 #
 #
+class TestForkCommand < Test::Unit::TestCase
+  def setup
+    @player = MockPlayer.new
+  end
+
+  def test_new_buoy_game_name
+    src = "%%FORK server+denou-14400-60+p1+p2+20130223185013"
+    c = ShogiServer::ForkCommand.new src, @player, "server+denou-14400-60+p1+p2+20130223185013", nil, 13
+    c.decide_new_buoy_game_name
+    assert_equal "buoy_denou_13-14400-60", c.new_buoy_game
+  end
+
+  def test_new_buoy_game_name2
+    src = "%%FORK server+denou-14400-060+p1+p2+20130223185013"
+    c = ShogiServer::ForkCommand.new src, @player, "server+denou-14400-060+p1+p2+20130223185013", nil, 13
+    c.decide_new_buoy_game_name
+    assert_equal "buoy_denou_13-14400-060", c.new_buoy_game
+  end
+end
+
+#
+#
 class TestGetBuoyCountCommand < BaseTestBuoyCommand
   def test_call
     buoy_game = ShogiServer::BuoyGame.new("buoy_testdeletebuoy-1500-0", "+7776FU", @p.name, 1)
@@ -1051,4 +1083,3 @@ class TestMonitorHandler2 < Test::Unit::TestCase
                  @player.out.join)
   end
 end
-
diff --git a/test/TC_fork.rb b/test/TC_fork.rb
new file mode 100644 (file)
index 0000000..7e2b6f3
--- /dev/null
@@ -0,0 +1,131 @@
+$:.unshift File.join(File.dirname(__FILE__), "..")
+$topdir = File.expand_path File.dirname(__FILE__)
+require "baseclient"
+require "shogi_server/buoy.rb"
+
+class TestFork < BaseClient
+  def parse_game_name(player)
+    player.puts "%%LIST"
+    sleep 1
+    if /##\[LIST\] (.*)/ =~ player.message
+      return $1
+    end
+  end
+
+  def test_wrong_game
+    @admin = SocketPlayer.new "dummy", "admin", false
+    @admin.connect
+    @admin.reader
+    @admin.login
+
+    result, result2 = handshake do
+      @admin.puts "%%FORK wronggame-900-0 buoy_WrongGame-900-0"
+      sleep 1
+    end
+
+    assert /##\[ERROR\] wrong source game name/ =~ @admin.message
+    @admin.logout
+  end
+
+  def test_too_short_fork
+    @admin = SocketPlayer.new "dummy", "admin", false
+    @admin.connect
+    @admin.reader
+    @admin.login
+
+    result, result2 = handshake do
+      source_game = parse_game_name(@admin)
+      @admin.puts "%%FORK #{source_game} buoy_TooShortFork-900-0 0"
+      sleep 1
+    end
+
+    assert /##\[ERROR\] number of moves to fork is out of range/ =~ @admin.message
+    @admin.logout
+  end
+
+  def test_fork
+    buoy = ShogiServer::Buoy.new
+    
+    @admin = SocketPlayer.new "dummy", "admin", "*"
+    @admin.connect
+    @admin.reader
+    @admin.login
+    assert buoy.is_new_game?("buoy_Fork-1500-0")
+
+    result, result2 = handshake do
+      source_game = parse_game_name(@admin)
+      @admin.puts "%%FORK #{source_game} buoy_Fork-1500-0"
+      sleep 1
+    end
+
+    assert buoy.is_new_game?("buoy_Fork-1500-0")
+    @p1 = SocketPlayer.new "buoy_Fork", "p1", true
+    @p2 = SocketPlayer.new "buoy_Fork", "p2", false
+    @p1.connect
+    @p2.connect
+    @p1.reader
+    @p2.reader
+    @p1.login
+    @p2.login
+    sleep 1
+    @p1.game
+    @p2.game
+    sleep 1
+    @p1.agree
+    @p2.agree
+    sleep 1
+    assert /^Total_Time:1500/ =~ @p1.message
+    assert /^Total_Time:1500/ =~ @p2.message
+    @p2.move("-3334FU")
+    sleep 1
+    @p1.toryo
+    sleep 1
+    @p2.logout
+    @p1.logout
+
+    @admin.logout
+  end
+
+  def test_fork2
+    buoy = ShogiServer::Buoy.new
+    
+    @admin = SocketPlayer.new "dummy", "admin", "*"
+    @admin.connect
+    @admin.reader
+    @admin.login
+
+    result, result2 = handshake do
+      source_game = parse_game_name(@admin)
+      @admin.puts "%%FORK #{source_game}" # nil for new_buoy_game name
+      sleep 1
+      assert /##\[FORK\]: new buoy game name: buoy_TestFork_1-1500-0/ =~ @admin.message
+    end
+
+    assert buoy.is_new_game?("buoy_TestFork_1-1500-0")
+    @p1 = SocketPlayer.new "buoy_TestFork_1", "p1", true
+    @p2 = SocketPlayer.new "buoy_TestFork_1", "p2", false
+    @p1.connect
+    @p2.connect
+    @p1.reader
+    @p2.reader
+    @p1.login
+    @p2.login
+    sleep 1
+    @p1.game
+    @p2.game
+    sleep 1
+    @p1.agree
+    @p2.agree
+    sleep 1
+    assert /^Total_Time:1500/ =~ @p1.message
+    assert /^Total_Time:1500/ =~ @p2.message
+    @p2.move("-3334FU")
+    sleep 1
+    @p1.toryo
+    sleep 1
+    @p2.logout
+    @p1.logout
+
+    @admin.logout
+  end
+end
diff --git a/test/TC_time_clock.rb b/test/TC_time_clock.rb
new file mode 100644 (file)
index 0000000..151ba81
--- /dev/null
@@ -0,0 +1,92 @@
+$:.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'
+
+class DummyPlayer
+  def initialize(mytime)
+    @mytime = mytime
+  end
+  attr_reader :mytime
+end
+
+class TestTimeClockFactor < Test::Unit::TestCase
+  def test_chess_clock
+    c = ShogiServer::TimeClock::factory(1, "hoge-900-0")
+    assert_instance_of(ShogiServer::ChessClock, c)
+
+    c = ShogiServer::TimeClock::factory(1, "hoge-1500-60")
+    assert_instance_of(ShogiServer::ChessClock, c)
+  end
+
+  def test_stop_watch_clock
+    c = ShogiServer::TimeClock::factory(1, "hoge-1500-060")
+    assert_instance_of(ShogiServer::StopWatchClock, c)
+  end
+end
+
+class TestChessClock < Test::Unit::TestCase
+  def test_time_duration
+    tc = ShogiServer::ChessClock.new(1, 1500, 60)
+    assert_equal(1, tc.time_duration(100.1, 100.9))
+    assert_equal(1, tc.time_duration(100, 101))
+    assert_equal(1, tc.time_duration(100.1, 101.9))
+    assert_equal(2, tc.time_duration(100.1, 102.9))
+    assert_equal(2, tc.time_duration(100, 102))
+  end
+
+  def test_without_byoyomi
+    tc = ShogiServer::ChessClock.new(1, 1500, 0)
+
+    p = DummyPlayer.new 100
+    assert(!tc.timeout?(p, 100, 101))
+    assert(!tc.timeout?(p, 100, 199))
+    assert(tc.timeout?(p, 100, 200))
+    assert(tc.timeout?(p, 100, 201))
+  end
+
+  def test_with_byoyomi
+    tc = ShogiServer::ChessClock.new(1, 1500, 60)
+
+    p = DummyPlayer.new 100
+    assert(!tc.timeout?(p, 100, 101))
+    assert(!tc.timeout?(p, 100, 259))
+    assert(tc.timeout?(p, 100, 260))
+    assert(tc.timeout?(p, 100, 261))
+
+    p = DummyPlayer.new 30
+    assert(!tc.timeout?(p, 100, 189))
+    assert(tc.timeout?(p, 100, 190))
+  end
+
+  def test_with_byoyomi2
+    tc = ShogiServer::ChessClock.new(1, 0, 60)
+
+    p = DummyPlayer.new 0
+    assert(!tc.timeout?(p, 100, 159))
+    assert(tc.timeout?(p, 100, 160))
+  end
+end
+
+class TestStopWatchClock < Test::Unit::TestCase
+  def test_time_duration
+    tc = ShogiServer::StopWatchClock.new(1, 1500, 60)
+    assert_equal(0, tc.time_duration(100.1, 100.9))
+    assert_equal(0, tc.time_duration(100, 101))
+    assert_equal(0, tc.time_duration(100, 159.9))
+    assert_equal(60, tc.time_duration(100, 160))
+    assert_equal(60, tc.time_duration(100, 219))
+    assert_equal(120, tc.time_duration(100, 220))
+  end
+
+  def test_with_byoyomi
+    tc = ShogiServer::StopWatchClock.new(1, 600, 60)
+
+    p = DummyPlayer.new 60
+    assert(!tc.timeout?(p, 100, 159))
+    assert(tc.timeout?(p, 100, 160))
+  end
+end
+