OSDN Git Service

Merge remote-tracking branch 'origin/wdoor-stable'
authorDaigo Moriwaki <daigo@debian.org>
Fri, 22 Nov 2013 12:47:59 +0000 (21:47 +0900)
committerDaigo Moriwaki <daigo@debian.org>
Fri, 22 Nov 2013 12:47:59 +0000 (21:47 +0900)
Conflicts:
changelog

12 files changed:
1  2 
changelog
mk_game_results
mk_rate
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_command.rb
test/TC_floodgate.rb

diff --combined changelog
+++ b/changelog
@@@ -1,3 -1,78 +1,78 @@@
+ 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.
+       * [mk_game_results]
+         - Flush after each output line.
+       * Rleased: Revision "20131104"
+ 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]
+         - New pairing algorithm: ShogiServer::Pairing::LeastDiff
+           This pairing algorithm aims to minimize the total differences of
+           matching players' rates. It also includes penalyties when a match
+           is same as the previous one or a match is between human players.
+           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]
+         - Backported a5c94012656902e73e00f46e7a4c7004b24d4578:
+           test/TC_logger.rb depeneded on a specific directory where it was
+           running on. This issues has been fixed.
+         - Backported 87d145bd1f1a14a33f5f6fbc78b63a1952f1ca90 and
+           2df8c798aeb7f0e77735e893fd1370c2c6f15c4d:
+           shogi_server/floodgate.rb: Generating next time around the new
+           year day by reading configuration files did not work correctly.
+           This issue has been fixed.
  2012-12-28  Daigo Moriwaki <daigo at debian dot org>
  
        * [shogi-server]
            + Improved the logic avoiding human-human match. Human-human
              matches will less likely happen.
  
 +2012-01-07  Daigo Moriwaki <daigo at debian dot org>
 +
 +      * [shogi-server]
 +        - Added shogi_server/compatible.rb, which implements compatible
 +          methods and allows Ruby 1.8.7 to run the server.
 +        - test/TC_floodgate.rb failed with Ruby 1.8.7. This issue has
 +          been fixed.
 +        - test/TC_uchifuzume.rb did not run with Ruby 1.8.7. This issue
 +          has been fixed.
 +        - test/TC_league.rb failed with Ruby 1.8.7. This issue has been
 +          fixed.
 +      * csa-file-filter,mk_game_results,mk_html,mk_rate:
 +        - Updated documents in the command files.
 +          Both Ruby 1.9.3 and 1.8.7 are supported.
 +        - Make their shebang consistent (/usr/bin/ruby1.9.1)
 +      * README:
 +        - Both Ruby 1.9.3 and 1.8.7 are supported.
 +      * Renewed year of copyright notice in each file.
 +
 +2012-01-06  Daigo Moriwaki <daigo at debian dot org>
 +
 +      * [shogi-server]
 +        - test/TC_logger.rb depeneded on a specific directory where it was
 +          running on. This issues has been fixed.
 +
 +2012-01-01  Daigo Moriwaki <daigo at debian dot org>
 +
 +      * [shogi-server]
 +        - shogi_server/floodgate.rb: Generating next time around the new
 +          year day by reading configuration files did not work correctly.
 +          This issue has been fixed.
 +
 +2011-12-18  Daigo Moriwaki <daigo at debian dot org>
 +
 +      * [shogi-server]
 +        - shogi_server/board.rb, piece.rb: Refactoring to cache OU pieces,
 +          which was inspired by 81SquareShogi-server's change
 +          (74b24b88c843f1dd767412475b117481d1d5e8eb).
 +        - Added shogi-server-profile to take profile of shogi-server.
 +      * [mk_rate] [mk_game_results]
 +        - Supports Ruby 1.9.3.
 +
 +2011-12-12  Daigo Moriwaki <daigo at debian dot org>
 +
 +      * [shogi-server]
 +        - Support Ruby 1.9.3.
 +        - Result of test/benchmark.rb
 +          - Environment:
 +            - CPU: AMD Athlon(tm) 64 X2 Dual Core Processor 4200+  
 +            - RAM: 4GB
 +            - OS: Debian Squeeze
 +            - ruby 1.8.7 (2011-06-30 patchlevel 352) [x86_64-linux]
 +            - ruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-linux]
 +          - Server:  ruby1.8 (or ruby1.9.1) ./shogi-server hoge 4000
 +          - Clients: ruby1.8 (or ruby1.9.1) -d ./benchmark.rb
 +            csa/wdoor+floodgate-900-0+gps_normal+gps_l+20100507120007.csa 20
 +          - Scores in seconds: (the smaller, the better)
 +                          clients
 +                          1.8.7   1.9.3
 +            server 1.8.7  20 sec  21 sec
 +                   1.9.3  26 sec  27 sec
 +
  2010-10-06  Daigo Moriwaki <daigo at debian dot org>
  
        * [shogi-server]
  
        * [shogi-server]
          - shogi_server/{board,command,game,league,player}.rb
 -          The Buoy behaivor is changed.
 +          The Buoy behavior is changed.
            + Starting a buoy game, players are notified a starting game
 -            position with the initial position and moves, instread of a
 +            position with the initial position and moves, instead of a
              targeting position.
            + Players are allowed to start buoy games with specific turns.
              ex. %%GAME buoy_foo-1500-0 +
        * [shogi-server]
          - shogi_server.rb, shogi_server/board.rb, shogi_server/move.rb
            - Refactoring: Board can now move_to() and move_back() a move
 -            instread of deep_copy().
 +            instead of deep_copy().
  
  2010-07-11  Daigo Moriwaki <daigo at debian dot org>
  
            .
            Note: Without this option, no floodgate games are started. If
            you want floodgate-900-0 to run, which was default enabled in
 -          previous versions, you need to spefify the game name in this new
 +          previous versions, you need to specify the game name in this new
            option.
          - Floodgate time configuration file:
            You need to set starting times of floodgate groups in
 -          configuration files under the top directory. Each floodgat
 -          e group requires a correspoding configuration file named
 +          configuration files under the top directory. Each floodgate
 +          group requires a corresponding configuration file named
            "<game_name>.conf". The file will be re-read once just after a
            game starts. 
            .
            floodgate-3600-30.conf.  However, for floodgate-900-0 and
            floodgate-3600-0, which were default enabled in previous
            versions, configuration files are optional if you are happy with
 -          defualt time settings.
 +          default time settings.
            File format is:
              Line format: 
                # This is a comment line
  2010-02-27  Daigo Moriwaki <daigo at debian dot org>
  
        * [shogi-server]
 -        - The server now provides more accurate time control. Previouslly,
 +        - The server now provides more accurate time control. Previously,
            a player's thinking time included a time waiting to get the giant
            lock. This may have caused games to time up, especially, during
            byo-yomi etc.
  
        * [shogi-server]
          - shogi-server: The command line option --floodgate-history has
 -          been deprectated. The server will decide history file names such
 +          been deprecated. The server will decide history file names such
            as 'floodgate_history_900_0.yaml' and
            'floodgate_history_3600_0.yaml', and then put them in the top
            directory.
        * [shogi-server]
          - shogi_server/player.rb: Added new methods: is_human? and
            is_computer?. 
 -          A human player is recommened to use a name ending with '_human'.  
 +          A human player is recommended to use a name ending with '_human'.  
            ex. 'hoge_human', 'hoge_human@p1'
          - shogi_server/pairing.rb: Added a new class:
            StartGameWithoutHumans, which tries to make pairs trying to
 -          avoid a human-human match. This is now enabled instread of the
 +          avoid a human-human match. This is now enabled instead of the
            previous class: StartGame.
          - shogi-server, shogi_server/league/floodgate.rb:
            Changed the argument of Floodgate.new.
            which will be used to generate players.yaml. If the file does
            not exist, the server will create one automatically.
            Instruction to use the game result list file:
 -          1. Make a list of game results from exisiting CSA files with
 +          1. Make a list of game results from existing CSA files with
               mk_game_results
               % ./mk_game_results dir_of_csa_files > 00LIST
            2. Run the server. It appends a result of each game to
              - 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.
 +            specific 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
  2009-06-18 Daigo Moriwaki <daigo at debian dot org>
  
        * [shogi-server]
 -        - An emtpy floodgate_history.yaml caused a server error. This
 +        - An empty floodgate_history.yaml caused a server error. This
            issue has been fixed. 
            (Closes: #15124)
  
  
        * [mk_html]
          - Added a new option: --footer filename, which inserts contents of 
 -          the filename at the bottom of a genrated page. A text specific to 
 +          the filename at the bottom of a generated page. A text specific to 
            wdoor should be written by using this option. 
            (Closes: #14470)
          - It does no more depend on RDoc. RDoc::usage does not work well
  
        * [shogi-server]
          - Improved an existance check and etc. of directories specified
 -          by command line options, expecially in case of the daemon mode. 
 +          by command line options, especially in case of the daemon mode. 
            (Closes: #14244)
 -        - A lotated log file is moved to $topdir/YYYY/MM/DD.
 +        - A rotated log file is moved to $topdir/YYYY/MM/DD.
            (Closes: #14245)
  
  2008-11-27 Daigo Moriwaki <daigo at debian dot org>
  
        * [shogi-server]
          - .csa files will be located in a sub directory such as
 -          "2008/05/05/*.csa". Thease days, we have many games in a day. 
 +          "2008/05/05/*.csa". These days, we have many games in a day. 
            This change will help users browse a file list.
  
  2008-05-03 Daigo Moriwaki <daigo at debian dot org>
diff --combined mk_game_results
@@@ -1,11 -1,11 +1,11 @@@
 -#!/usr/bin/ruby
 +#!/usr/bin/ruby1.9.1
  # $Id$
  #
  # Author:: Daigo Moriwaki
  # Homepage:: http://sourceforge.jp/projects/shogi-server/
  #
  #--
 -# Copyright (C) 2009 Daigo Moriwaki <daigo at debian dot org>
 +# Copyright (C) 2009-2012 Daigo Moriwaki <daigo at debian dot org>
  #
  # This program is free software; you can redistribute it and/or modify
  # it under the terms of the GNU General Public License as published by
@@@ -35,9 -35,9 +35,9 @@@
  #
  # Sample Command lines that isntall prerequires will work on Debian.
  #
 -# * Ruby 1.8.7
 +# * Ruby 1.9.3 or 1.8.7
  #
 -#   $ sudo aptitude install ruby1.8
 +#   $ sudo aptitude install ruby1.9.1
  #
  # == Run
  #
@@@ -89,6 -89,7 +89,7 @@@ def grep(file
        puts [time, state, black_mark, black_id, white_id, white_mark, file].join("\t")
      end
    end
+   $stdout.flush
  end
  
  # Show Usage
diff --combined mk_rate
+++ b/mk_rate
@@@ -1,11 -1,11 +1,11 @@@
 -#!/usr/bin/ruby
 +#!/usr/bin/ruby1.9.1
  # $Id$
  #
  # Author:: Daigo Moriwaki
  # Homepage:: http://sourceforge.jp/projects/shogi-server/
  #
  #--
 -# Copyright (C) 2006-2009 Daigo Moriwaki <daigo at debian dot org>
 +# Copyright (C) 2006-2012 Daigo Moriwaki <daigo at debian dot org>
  #
  # This program is free software; you can redistribute it and/or modify
  # it under the terms of the GNU General Public License as published by
  #   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
  #
  #
  # Sample Command lines that isntall prerequires will work on Debian.
  #
 -# * Ruby 1.8.7
 +# * Ruby 1.9.3 or 1.8.7 (including Rubygems)
  #
 -#   $ sudo aptitude install ruby1.8
 -#
 -# * Rubygems
 -#
 -#   $ sudo aptitude install rubygems
 +#   $ sudo aptitude install ruby1.9.1
  #
  # * Ruby bindings for the GNU Scientific Library (GSL[http://rb-gsl.rubyforge.org/])
  #
 -#   $ sudo aptitude install libgsl-ruby1.8
 +#   $ sudo aptitude install ruby-gsl
  #
  # * RGL: {Ruby Graph Library}[http://rubyforge.org/projects/rgl/]
  #
 -#   $ sudo gem install rgl
 +#   $ sudo gem1.9.1 install rgl
  #
  # == Examples
  #
@@@ -660,6 -668,11 +664,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)
  
@@@ -697,6 -710,8 +706,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
@@@ -712,6 -727,7 +723,7 @@@ def mai
      ["--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])
    $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?
diff --combined shogi-server
@@@ -1,4 -1,4 +1,4 @@@
 -#! /usr/bin/env ruby
 +#! /usr/bin/ruby1.9.1
  # $Id$
  #
  # Author:: NABEYA Kenichi, Daigo Moriwaki
@@@ -6,7 -6,7 +6,7 @@@
  #
  #--
  # Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
 -# Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
 +# Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
  #
  # This program is free software; you can redistribute it and/or modify
  # it under the terms of the GNU General Public License as published by
@@@ -143,9 -143,6 +143,6 @@@ LICENS
  
  SEE ALSO
  
- RELEASE
-         #{ShogiServer::Release}
  REVISION
          #{ShogiServer::Revision}
  
@@@ -420,10 -417,7 +417,10 @@@ def mai
        client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
          # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
        player, login = login_loop(client) # loop
 -      next unless player
 +      unless player
 +        log_error("Detected a timed out login attempt")
 +        next
 +      end
  
        log_message(sprintf("user %s login", player.name))
        login.process
          if (player.game)
            player.game.kill(player)
          end
 -        player.finish # socket has been closed
 +        player.finish
          $league.delete(player)
          log_message(sprintf("user %s logout", player.name))
        ensure
          $mutex.unlock
        end
 +      player.wait_write_thread_finish(1000) # milliseconds
      rescue Exception => ex
        log_error("server.start: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
      end
diff --combined shogi_server.rb
@@@ -1,7 -1,7 +1,7 @@@
  ## $Id$
  
  ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
 -## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
 +## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
  ##
  ## This program is free software; you can redistribute it and/or modify
  ## it under the terms of the GNU General Public License as published by
@@@ -29,7 -29,6 +29,7 @@@ require 'webrick
  require 'fileutils'
  require 'logger'
  
 +require 'shogi_server/compatible'
  require 'shogi_server/board'
  require 'shogi_server/game'
  require 'shogi_server/league'
@@@ -51,8 -50,7 +51,7 @@@ Default_Game_Name = "default-1500-0
  One_Time = 10
  Least_Time_Per_Move = 1
  Login_Time = 300                # time for LOGIN
- Release  = "$Id$"
- Revision = (r = /Revision: (\d+)/.match("$Revision$") ? r[1] : 0)
+ Revision = "20131104"
  
  RELOAD_FILES = ["shogi_server/league/floodgate.rb",
                  "shogi_server/league/persistent.rb",
diff --combined shogi_server/board.rb
@@@ -1,7 -1,7 +1,7 @@@
  ## $Id$
  
  ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
 -## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
 +## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
  ##
  ## This program is free software; you can redistribute it and/or modify
  ## it under the terms of the GNU General Public License as published by
@@@ -43,17 -43,42 +43,42 @@@ EO
  
    # 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
@@@ -69,7 -94,6 +94,7 @@@
      @move_count = move_count
      @teban = nil # black => true, white => false
      @initial_moves = []
 +    @ous = [nil, nil] # keep OU pieces of Sente and Gote
    end
    attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban
    attr_reader :move_count
      @teban = true
    end
  
 +  # Cache OU piece.
 +  # Piece#new will call back this method.
 +  #
 +  def add_ou(ou)
 +    if ou.sente
 +      @ous[0] = ou
 +    else
 +      @ous[1] = ou
 +    end
 +  end
 +
    # Set up a board with the strs.
    # Failing to parse the moves raises an StandardError.
    # @param strs a board text
  
    # 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
    end
    
    # def each_reserved_square
 -
 +  
    def look_for_ou(sente)
 -    x = 1
 -    while (x <= 9)
 -      y = 1
 -      while (y <= 9)
 -        if (@array[x][y] &&
 -            (@array[x][y].name == "OU") &&
 -            (@array[x][y].sente == sente))
 -          return @array[x][y]
 -        end
 -        y = y + 1
 -      end
 -      x = x + 1
 +    if sente
 +      return @ous[0]
 +    else
 +      return @ous[1]
      end
 -    raise "can't find ou"
    end
  
    # See if sente is checked (i.e. loosing) or not.
diff --combined shogi_server/command.rb
@@@ -1,7 -1,7 +1,7 @@@
  ## $Id$
  
  ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
 -## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
 +## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
  ##
  ## This program is free software; you can redistribute it and/or modify
  ## it under the terms of the GNU General Public License as published by
@@@ -69,6 -69,9 +69,9 @@@ module ShogiServe
          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)
        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 /^%%%[^%]/
  
      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
    # 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
  
        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
          @player.write_safe("##[GETBUOYCOUNT] %s\n" % [buoy_game.count])
        end
        @player.write_safe("##[GETBUOYCOUNT] +OK\n")
 +      return :continue
      end
    end
  
+   # %%FORK <source_game> <new_buoy_game> [<nth-move>]
+   # Fork a new game from the posistion where the n-th (starting from 1) move
+   # of a source game is played. The new game should be a valid buoy game
+   # name. The default value of n is the position where the previous position
+   # of the last one.
+   #
+   class ForkCommand < Command
+     def initialize(str, player, source_game, new_buoy_game, nth_move)
+       super(str, player)
+       @source_game   = source_game
+       @new_buoy_game = new_buoy_game
+       @nth_move      = nth_move # may be nil
+     end
+     attr_reader :new_buoy_game
+     def decide_new_buoy_game_name
+       name       = nil
+       total_time = nil
+       byo_time   = nil
+       if @source_game.split("+").size >= 2 &&
+          /^([^-]+)-(\d+)-(\d+)/ =~ @source_game.split("+")[1]
+         name       = $1
+         total_time = $2
+         byo_time   = $3
+       end
+       if name == nil || total_time == nil || byo_time == nil
+         @player.write_safe(sprintf("##[ERROR] wrong source game name to make a new buoy game name: %s\n", @source_game))
+         log_error "Received a wrong source game name to make a new buoy game name: %s from %s." % [@source_game, @player.name]
+         return :continue
+       end
+       @new_buoy_game = "buoy_%s_%d-%s-%s" % [name, @nth_move, total_time, byo_time]
+       @player.write_safe(sprintf("##[FORK]: new buoy game name: %s\n", @new_buoy_game))
+       @player.write_safe("##[FORK] +OK\n")
+     end
+     def call
+       game = $league.games[@source_game]
+       unless game
+         @player.write_safe(sprintf("##[ERROR] wrong source game name: %s\n", @source_game))
+         log_error "Received a wrong source game name: %s from %s." % [@source_game, @player.name]
+         return :continue
+       end
+       moves = game.read_moves # [["+7776FU","T2"],["-3334FU","T5"]]
+       @nth_move = moves.size - 1 unless @nth_move
+       if @nth_move > moves.size or @nth_move < 1
+         @player.write_safe(sprintf("##[ERROR] number of moves to fork is out of range: %s.\n", moves.size))
+         log_error "Number of moves to fork is out of range: %s [%s]" % [@nth_move, @player.name]
+         return :continue
+       end
+       new_moves_str = ""
+       moves[0...@nth_move].each do |m|
+         new_moves_str << m.join(",")
+       end
+       unless @new_buoy_game
+         decide_new_buoy_game_name
+       end
+       buoy_cmd = SetBuoyCommand.new(@str, @player, @new_buoy_game, new_moves_str, 1)
+       return buoy_cmd.call
+     end
+   end
  end # module ShogiServer
diff --combined shogi_server/game.rb
@@@ -1,7 -1,7 +1,7 @@@
  ## $Id$
  
  ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
 -## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
 +## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
  ##
  ## This program is free software; you can redistribute it and/or modify
  ## it under the terms of the GNU General Public License as published by
@@@ -19,6 -19,7 +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 +70,8 @@@ class Gam
      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)
      @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"
      $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")
  
      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
  
        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])
  
      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 +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 +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
  
      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 --combined shogi_server/pairing.rb
@@@ -1,7 -1,7 +1,7 @@@
  ## $Id$
  
  ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
 -## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
 +## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
  ##
  ## This program is free software; you can redistribute it and/or modify
  ## it under the terms of the GNU General Public License as published by
@@@ -25,7 -25,7 +25,7 @@@ module ShogiServe
  
      class << self
        def default_factory
-         return swiss_pairing
+         return least_diff_pairing
        end
  
        def sort_by_rate_with_randomness
                  StartGameWithoutHumans.new]
        end
  
+       def least_diff_pairing
+         return [LogPlayers.new,
+                 ExcludeSacrificeGps500.new,
+                 MakeEven.new,
+                 LeastDiff.new,
+                 StartGameWithoutHumans.new]
+       end
        def match(players)
          logics = default_factory
          logics.inject(players) do |result, item|
      end # class << self
  
  
+     # Make matches among players.
+     # @param players an array of players, which should be updated destructively
+     #        to pass the new list to subsequent logics.
+     #
      def match(players)
        # to be implemented
        log_message("Floodgate: %s" % [self.class.to_s])
    end
  
    class SortByRateWithRandomness < Pairing
-     def initialize(rand1, rand2)
+     def initialize(rand1, rand2, desc=false)
        super()
        @rand1, @rand2 = rand1, rand2
+       @desc = desc
      end
  
-     def match(players, desc=false)
+     def match(players)
        super(players)
        cur_rate = Hash.new
        players.each{|a| cur_rate[a] = a.rate ? a.rate + rand(@rand1) : rand(@rand2)}
        players.sort!{|a,b| cur_rate[a] <=> cur_rate[b]}
-       players.reverse! if desc
+       players.reverse! if @desc
        log_players(players) do |one|
          "%s %d (+ randomness %d)" % [one.name, one.rate, cur_rate[one] - one.rate]
        end
        rest = players - winners
  
        log_message("Floodgate: Ordering %d winners..." % [winners.size])
-       sbrwr_winners = SortByRateWithRandomness.new(800, 2500)
-       sbrwr_winners.match(winners, true)
+       sbrwr_winners = SortByRateWithRandomness.new(800, 2500, true)
+       sbrwr_winners.match(winners)
  
        log_message("Floodgate: Ordering the rest (%d)..." % [rest.size])
-       sbrwr_losers = SortByRateWithRandomness.new(200, 400)
-       sbrwr_losers.match(rest, true)
+       sbrwr_losers = SortByRateWithRandomness.new(200, 400, true)
+       sbrwr_losers.match(rest)
  
        players.clear
        [winners, rest].each do |group|
      def match(players)
        super
        return if less_than_one?(players)
 -      one = players.choice
 +      one = players.sample
        log_message("Floodgate: Deleted %s at random" % [one.name])
        players.delete(one)
        log_players(players)
      end
    end
  
+   # This pairing algorithm aims to minimize the total differences of
+   # matching players' rates. It also includes penalyties when a match is
+   # same as the previous one or a match is between human players.
+   # It is based on a discussion with Yamashita-san on
+   # http://www.sgtpepper.net/kaneko/diary/20120511.html.
+   #
+   class LeastDiff < Pairing
+     def random_match(players)
+       players.shuffle
+     end
+     # Returns a player's rate value.
+     # 1. If it has a valid rate, return the rate.
+     # 2. If it has no valid rate, return average of the following values:
+     #   a. For games it won, the opponent's rate + 100
+     #   b. For games it lost, the opponent's rate - 100
+     #   (if the opponent has no valid rate, count out the game)
+     #   (if there are not such games, return 2150 (default value)
+     #
+     def get_player_rate(player, history)
+       return player.rate if player.rate != 0
+       return 2150 unless history
+       count = 0
+       sum = 0
+       history.win_games(player.player_id).each do |g|
+         next unless g[:loser]
+         name = g[:loser].split("+")[0]
+         p = $league.find(name)
+         if p && p.rate != 0
+           count += 1
+           sum += p.rate + 100
+         end
+       end
+       history.loss_games(player.player_id).each do |g|
+         next unless g[:winner]
+         name = g[:winner].split("+")[0]
+         p = $league.find(name)
+         if p && p.rate != 0
+           count += 1
+           sum += p.rate - 100
+         end
+       end
+       estimate = (count == 0 ? 2150 : sum/count)
+       log_message("Floodgate: Estimated rate of %s is %d" % [player.name, estimate])
+       return estimate
+     end
+     def calculate_diff_with_penalty(players, history)
+       pairs = []
+       players.each_slice(2) do |pair|
+         if pair.size == 2
+           pairs << pair
+         end
+       end
+       ret = 0
+       # 1. Diff of players rate
+       pairs.each do |p1,p2|
+         ret += (get_player_rate(p1,history) - get_player_rate(p2,history)).abs
+       end
+       # 2. Penalties
+       pairs.each do |p1,p2|
+         # 2.1. same match
+         if (history &&
+             (history.last_opponent(p1.player_id) == p2.player_id ||
+              history.last_opponent(p2.player_id) == p1.player_id))
+           ret += 400
+         end
+         # 2.2 Human vs Human
+         if p1.is_human? && p2.is_human?
+           ret += 800
+         end
+       end
+       ret
+     end
+     def match(players)
+       super
+       if players.size < 3
+         log_message("Floodgate: players are small enough to skip LeastDiff pairing: %d" % [players.size])
+         return players
+       end
+       # 10 trials
+       matches = []
+       scores  = []
+       path = ShogiServer::League::Floodgate.history_file_path(players.first.game_name)
+       history = ShogiServer::League::Floodgate::History.factory(path)
+       10.times do 
+         m = random_match(players)
+         matches << m
+         scores << calculate_diff_with_penalty(m, history)
+       end
+       # Debug
+       #scores.each_with_index do |s,i|
+       #  puts
+       #  print s, ": ", matches[i].map{|p| p.name}.join(", "), "\n"
+       #end
+       # Select a match of the least score
+       min_index = 0
+       min_score = scores.first
+       scores.each_with_index do |s,i|
+         if s < min_score
+           min_index = i
+           min_score = s
+         end
+       end
+       log_message("Floodgate: the least score %d (%d per player) [%s]" % [min_score, min_score/players.size, scores.join(" ")])
+       players.replace(matches[min_index])
+     end
+   end
  end # ShogiServer
diff --combined test/TC_ALL.rb
@@@ -4,12 -4,12 +4,13 @@@ require 'TC_board
  require 'TC_before_agree'
  require 'TC_buoy'
  require 'TC_command'
 +require 'TC_compatible'
  require 'TC_config'
  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'
@@@ -24,6 -24,7 +25,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'
diff --combined test/TC_command.rb
@@@ -2,8 -2,8 +2,8 @@@ $:.unshift File.join(File.dirname(__FIL
  $topdir = File.expand_path File.dirname(__FILE__)
  require 'test/unit'
  require 'tempfile'
 -require 'mock_game'
 -require 'mock_log_message'
 +require 'test/mock_game'
 +require 'test/mock_log_message'
  require 'test/mock_player'
  require 'shogi_server/login'
  require 'shogi_server/player'
@@@ -228,6 -228,16 +228,16 @@@ class TestFactoryMethod < Test::Unit::T
      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)
      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
  
@@@ -832,7 -842,7 +842,7 @@@ class TestSetBuoyCommand < BaseTestBuoy
      assert @buoy.is_new_game?("buoy_hoge-1500-0")
      cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_hoge-1500-0", "+7776FU", 2
      rt = cmd.call
 -    assert :continue, rt
 +    assert_equal :continue, rt
      assert !@buoy.is_new_game?("buoy_hoge-1500-0")
      assert !$p1.out.empty?
      assert !$p2.out.empty?
      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_equal :continue, rt
      assert @buoy.is_new_game?("buoy_hoge-1500-0")
      assert !$p1.out.empty?
      assert !$p2.out.empty?
      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_equal :continue, rt
      assert $p1.out.empty?
      assert $p2.out.empty?
      assert @buoy.is_new_game?("buoy_hoge-1500-0")
      
      cmd = ShogiServer::SetBuoyCommand.new "%%SETBUOY", @p, "buoy_duplicated-1500-0", "+7776FU", 1
      rt = cmd.call
 -    assert :continue, rt
 +    assert_equal :continue, rt
      assert $p1.out.empty?
      assert $p2.out.empty?
      assert !@buoy.is_new_game?("buoy_duplicated-1500-0")
      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_equal :continue, rt
      assert $p1.out.empty?
      assert $p2.out.empty?
      assert @buoy.is_new_game?("buoy_badmoves-1500-0")
      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_equal :continue, rt
      assert $p1.out.empty?
      assert $p2.out.empty?
      assert @buoy.is_new_game?("buoy_badcounter-1500-0")
@@@ -906,7 -916,7 +916,7 @@@ class TestDeleteBuoyCommand < BaseTestB
      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 :continue, rt
      assert $p1.out.empty?
      assert $p2.out.empty?
      assert @buoy.is_new_game?(buoy_game.game_name)
      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 :continue, rt
      assert $p1.out.empty?
      assert $p2.out.empty?
      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 :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
@@@ -939,6 -949,28 +949,28 @@@ en
  
  #
  #
+ 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)
      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 :continue, rt
      assert_equal ["##[GETBUOYCOUNT] 1\n", "##[GETBUOYCOUNT] +OK\n"], @p.out
    end
  
      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 :continue, rt
      assert_equal ["##[GETBUOYCOUNT] -1\n", "##[GETBUOYCOUNT] +OK\n"], @p.out
    end
  end
@@@ -1051,4 -1083,3 +1083,3 @@@ class TestMonitorHandler2 < Test::Unit:
                   @player.out.join)
    end
  end
diff --combined test/TC_floodgate.rb
@@@ -24,12 -24,12 +24,12 @@@ class TestFloodgate < Test::Unit::TestC
    end
  
    def test_instance_game_name
 -    fg = ShogiServer::League::Floodgate.new(nil, "floodgate-900-0")
 -    assert(fg.game_name?("floodgate-900-0"))
 -    assert(!fg.game_name?("floodgate-3600-0"))
 -    fg = ShogiServer::League::Floodgate.new(nil, "floodgate-3600-0")
 +    fg = ShogiServer::League::Floodgate.new(nil, {:game_name => "floodgate-900-0"})
      assert(fg.game_name?("floodgate-900-0"))
      assert(!fg.game_name?("floodgate-3600-0"))
 +    fg = ShogiServer::League::Floodgate.new(nil, {:game_name => "floodgate-3600-0"})
 +    assert(!fg.game_name?("floodgate-900-0"))
 +    assert(fg.game_name?("floodgate-3600-0"))
    end
  
  end
@@@ -395,6 -395,22 +395,22 @@@ class TestFloodgateHistory < Test::Unit
      assert !@history.last_win?("foo")
      assert !@history.last_lose?("hoge")
      assert @history.last_lose?("foo")
+     assert_equal("foo", @history.last_opponent("hoge"))
+     assert_equal("hoge", @history.last_opponent("foo"))
+     games = @history.win_games("hoge")
+     assert_equal(1, games.size )
+     assert_equal("wdoor+floodgate-900-0-hoge-foo-2", games[0][:game_id])
+     games = @history.win_games("foo")
+     assert_equal(1, games.size )
+     assert_equal("wdoor+floodgate-900-0-hoge-foo-1", games[0][:game_id])
+     games = @history.loss_games("hoge")
+     assert_equal(1, games.size )
+     assert_equal("wdoor+floodgate-900-0-hoge-foo-1", games[0][:game_id])
+     games = @history.loss_games("foo")
+     assert_equal(1, games.size )
+     assert_equal("wdoor+floodgate-900-0-hoge-foo-2", games[0][:game_id])
    end
  end