+2009-12-26 Daigo Moriwaki <daigo at debian dot org>
+
+ * [shogi-server]
+ - The server can now provide multiple floodgate modes such as
+ floodgate-900-0 and floodgate-3600-0.
+
+2009-12-25 Daigo Moriwaki <daigo at debian dot org>
+
+ * [shogi-server]
+ - shogi-server: The command line option --floodgate-history has
+ been deprectated. 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.
+
2009-12-20 Daigo Moriwaki <daigo at debian dot org>
* [shogi-server]
$:.unshift File.dirname(__FILE__)
require 'shogi_server'
require 'shogi_server/config'
+require 'shogi_server/util'
require 'tempfile'
#################################################
--player-log-dir dir
log network messages for each player. Log files
will be put in the dir.
- --floodgate-history
- file name to record Floodgate game history
- default: './floodgate_history.yaml'
LICENSE
GPL versoin 2 or later
parser = GetoptLong.new(
["--daemon", GetoptLong::REQUIRED_ARGUMENT],
["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
- ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT],
- ["--floodgate-history", GetoptLong::REQUIRED_ARGUMENT])
+ ["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT])
parser.quiet = true
begin
parser.each_option do |name, arg|
if $options["pid-file"]
$options["pid-file"] = File.expand_path($options["pid-file"], $topdir)
- unless is_writable_file? $options["pid-file"]
+ unless ShogiServer::is_writable_file? $options["pid-file"]
usage
$stderr.puts "Can not create the pid file: %s" % [$options["pid-file"]]
exit 4
end
end
- $options["floodgate-history"] ||= File.join($topdir, "floodgate_history.yaml")
- $options["floodgate-history"] = File.expand_path($options["floodgate-history"], $topdir)
- unless is_writable_file? $options["floodgate-history"]
- usage
- $stderr.puts "Can not create the floodgate history file: %s" % [$options["floodgate-history"]]
- exit 6
+ if $options["floodgate-history"]
+ $stderr.puts "WARNING: --floodgate-history has been deprecated."
+ $options["floodgate-history"] = nil
end
end
-# See if the file is writable. The file will be created if it does not exist
-# yet.
-# Return true if the file is writable, otherwise false.
-#
-def is_writable_file?(file)
- if File.exist?(file)
- if FileTest.file?(file)
- return FileTest.writable_real?(file)
- else
- return false
- end
- end
-
- begin
- open(file, "w") {|fh| }
- FileUtils.rm file
- rescue
- return false
- end
-
- return true
-end
-
# See if a file can be created in the directory.
# Return true if a file is writable in the directory, otherwise false.
#
end
end
-def setup_floodgate_900
- return Thread.start do
+def setup_floodgate(game_names)
+ return Thread.start(game_names) do |game_names|
Thread.pass
- game_name = "floodgate-900-0"
- floodgate = ShogiServer::League::Floodgate.new($league,
- {:game_name => game_name})
- log_message("Flooddgate reloaded. The next match will start at %s." %
- [floodgate.next_time])
-
while (true)
begin
+ leagues = game_names.collect do |game_name|
+ floodgate = ShogiServer::League::Floodgate.new($league,
+ {:game_name => game_name})
+ log_message("Floodgate reloaded. The next match will start at %s." %
+ [floodgate.next_time])
+ floodgate
+ end
+ leagues.delete_if do |floodgate|
+ ret = false
+ unless floodgate.next_time
+ log_error("Unsupported game name: %s" % floodgate.game_name)
+ ret = true
+ end
+ ret
+ end
+ if leagues.empty?
+ log_error("No valid Floodgate game names found")
+ return # exit from this thread
+ end
+
+ floodgate = leagues.min {|a,b| a.next_time <=> b.next_time}
diff = floodgate.next_time - Time.now
if diff > 0
sleep(diff/2)
next
end
+ log_message("Starting Floodgate games...: %s" % [floodgate.game_name])
$league.reload
floodgate.match_game
floodgate.charge
floodgate = ShogiServer::League::Floodgate.new($league,
{:game_name => game_name,
:next_time => next_time})
- log_message("Floodgate: The next match will start at %s." %
- [floodgate.next_time])
+ log_message("Floodgate: The next match of %s will start at %s." %
+ [floodgate.game_name, floodgate.next_time])
rescue Exception => ex
# ignore errors
log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
end
setup_watchdog_for_giant_lock
$league.setup_players_database
- fg_thread = setup_floodgate_900
+ fg_thread = setup_floodgate(["floodgate-900-0", "floodgate-3600-0"])
end
config[:StopCallback] = Proc.new do
add_observer LoggingObserver.new
if League::Floodgate.game_name?(@game.game_name) &&
- @game.sente.player_id &&
- @game.gote.player_id &&
- $options["floodgate-history"]
- add_observer League::Floodgate::History.factory
+ @game.sente.player_id && @game.gote.player_id
+ path = League::Floodgate.history_file_path(@game.game_name)
+ history = League::Floodgate::History.factory(path)
+ add_observer history if history
end
end
class League
class Floodgate
class << self
- # "floodgate-900-0"
+ # ex. "floodgate-900-0"
#
def game_name?(str)
return /^floodgate\-\d+\-\d+$/.match(str) ? true : false
end
- end
- attr_reader :next_time, :league
+ def history_file_path(gamename)
+ return nil unless game_name?(gamename)
+ filename = "floodgate_history_%s.yaml" % [gamename.gsub("floodgate-", "").gsub("-","_")]
+ file = File.join($topdir, filename)
+ return Pathname.new(file)
+ end
+ end # class method
+
+ attr_reader :next_time, :league, :game_name
def initialize(league, hash={})
@league = league
end
def charge
- now = Time.now
- unless $DEBUG
- # each 30 minutes
- if now.min < 30
- @next_time = Time.mktime(now.year, now.month, now.day, now.hour, 30)
- else
- @next_time = Time.mktime(now.year, now.month, now.day, now.hour) + 3600
- end
+ ntg = NextTimeGenerator.factory(@game_name)
+ if ntg
+ @next_time = ntg.call(Time.now)
else
- # for test, each 30 seconds
- if now.sec < 30
- @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
- else
- @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60
- end
+ @next_time = nil
end
end
end
Pairing.match(players)
end
+
+ #
+ #
+ class NextTimeGenerator
+ class << self
+ def factory(game_name)
+ ret = nil
+ if $DEBUG
+ ret = NextTimeGenerator_Debug.new
+ elsif game_name == "floodgate-900-0"
+ ret = NextTimeGenerator_Floodgate_900_0.new
+ elsif game_name == "floodgate-3600-0"
+ ret = NextTimeGenerator_Floodgate_3600_0.new
+ end
+ return ret
+ end
+ end
+ end
+ class NextTimeGenerator_Floodgate_900_0
+ def call(now)
+ # each 30 minutes
+ if now.min < 30
+ return Time.mktime(now.year, now.month, now.day, now.hour, 30)
+ else
+ return Time.mktime(now.year, now.month, now.day, now.hour) + 3600
+ end
+ end
+ end
+
+ class NextTimeGenerator_Floodgate_3600_0
+ def call(now)
+ # each 2 hours (odd hour)
+ return Time.mktime(now.year, now.month, now.day, now.hour) + ((now.hour%2)+1)*3600
+ end
+ end
+
+ class NextTimeGenerator_Debug
+ def call(now)
+ # for test, each 30 seconds
+ if now.sec < 30
+ return Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
+ else
+ return Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60
+ end
+ end
+ end
#
#
@@mutex = Mutex.new
class << self
- def factory
- file = Pathname.new $options["floodgate-history"]
- history = History.new file
+ def factory(pathname)
+ unless ShogiServer::is_writable_file?(pathname.to_s)
+ log_error("Failed to write a history file: %s" % [pathname])
+ return nil
+ end
+ history = History.new pathname
history.load
return history
end
end
def swiss_pairing
- history = ShogiServer::League::Floodgate::History.factory
return [LogPlayers.new,
ExcludeSacrificeGps500.new,
MakeEven.new,
- Swiss.new(history),
+ Swiss.new,
StartGameWithoutHumans.new]
end
end
class Swiss < Pairing
- def initialize(history)
- super()
- @history = history
- end
-
def match(players)
super
- winners = players.find_all {|pl| @history.last_win?(pl.player_id)}
- rest = players - winners
+ if players.size < 3
+ log_message("Floodgate: players are small enough to skip Swiss pairing: %d" % [players.size])
+ return
+ end
+
+ path = ShogiServer::League::Floodgate.history_file_path(players.first.game_name)
+ history = ShogiServer::League::Floodgate::History.factory(path)
+
+ winners = []
+ if history
+ winners = players.find_all {|pl| history.last_win?(pl.player_id)}
+ end
+ rest = players - winners
log_message("Floodgate: Ordering %d winners..." % [winners.size])
sbrwr_winners = SortByRateWithRandomness.new(800, 2500)
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+require 'fileutils'
+require 'pathname'
+
module ShogiServer
# Generate a random number such as i<=n<max
end
module_function :shuffle
+ # See if the file is writable. The file will be created if it does not exist
+ # yet.
+ # Return true if the file is writable, otherwise false.
+ #
+ def is_writable_file?(file)
+ if String === file
+ file = Pathname.new file
+ end
+ if file.exist?
+ if file.file?
+ return file.writable_real?
+ else
+ return false
+ end
+ end
+
+ begin
+ file.open("w") {|fh| }
+ file.delete
+ rescue
+ return false
+ end
+
+ return true
+ end
+ module_function :is_writable_file?
+
end
require 'TC_config'
require 'TC_floodgate'
require 'TC_floodgate_history'
+require 'TC_floodgate_next_time_generator'
require 'TC_functional'
require 'TC_game'
require 'TC_game_result'
require 'shogi_server/pairing'
require 'shogi_server/league/floodgate'
+$topdir = File.expand_path File.dirname(__FILE__)
+
class MockLogger
def debug(str)
+ #puts str
end
def info(str)
#puts str
end
def warn(str)
+ puts str
end
def error(str)
+ puts str
end
end
$logger.warn(msg)
end
+def log_error(msg)
+ $logger.error(msg)
+end
+
class TestFloodgate < Test::Unit::TestCase
def setup
@fg = ShogiServer::League::Floodgate.new(nil)
@a = ShogiServer::BasicPlayer.new
@a.player_id = "a"
@a.rate = 0
+ @a.game_name = "floodgate-900-0"
@b = ShogiServer::BasicPlayer.new
@b.player_id = "b"
@b.rate = 1000
+ @b.game_name = "floodgate-900-0"
@c = ShogiServer::BasicPlayer.new
@c.player_id = "c"
@c.rate = 1500
+ @c.game_name = "floodgate-900-0"
@d = ShogiServer::BasicPlayer.new
@d.player_id = "d"
@d.rate = 2000
+ @d.game_name = "floodgate-900-0"
@players = [@a, @b, @c, @d]
- @file = Pathname.new(File.join(File.dirname(__FILE__), "floodgate_history.yaml"))
- @history = ShogiServer::League::Floodgate::History.new @file
+ @file = Pathname.new(File.join(File.dirname(__FILE__), "floodgate_history_900_0.yaml"))
+ @history = ShogiServer::League::Floodgate::History.factory @file
- @swiss = ShogiServer::Swiss.new @history
+ @swiss = ShogiServer::Swiss.new
end
def teardown
@file.delete if @file.exist?
end
+ def test_none
+ players = []
+ @swiss.match players
+ assert(players.empty?)
+ end
+
def test_all_win
- def @history.last_win?(player_id)
- true
+ ShogiServer::League::Floodgate::History.class_eval do
+ def last_win?(player_id)
+ true
+ end
end
@swiss.match @players
assert_equal([@d, @c, @b, @a], @players)
end
def test_all_lose
- def @history.last_win?(player_id)
- false
+ ShogiServer::League::Floodgate::History.class_eval do
+ def last_win?(player_id)
+ false
+ end
end
@swiss.match @players
assert_equal([@d, @c, @b, @a], @players)
end
def test_one_win
- def @history.last_win?(player_id)
- if player_id == "a"
- true
- else
- false
+ ShogiServer::League::Floodgate::History.class_eval do
+ def last_win?(player_id)
+ if player_id == "a"
+ true
+ else
+ false
+ end
end
end
@swiss.match @players
end
def test_two_win
- def @history.last_win?(player_id)
- if player_id == "a" || player_id == "d"
- true
- else
- false
+ ShogiServer::League::Floodgate::History.class_eval do
+ def last_win?(player_id)
+ if player_id == "a" || player_id == "d"
+ true
+ else
+ false
+ end
end
end
@swiss.match @players
--- /dev/null
+$:.unshift File.join(File.dirname(__FILE__), "..")
+require 'test/unit'
+require 'shogi_server'
+require 'shogi_server/league/floodgate'
+
+$topdir = File.expand_path File.dirname(__FILE__)
+
+class TestNextTimeGenerator_900_0 < Test::Unit::TestCase
+ def setup
+ @next = ShogiServer::League::Floodgate::NextTimeGenerator_Floodgate_900_0.new
+ end
+
+ def test_0_min
+ now = Time.mktime(2009,12,25,22,0)
+ assert_equal(Time.mktime(2009,12,25,22,30), @next.call(now))
+ end
+
+ def test_20_min
+ now = Time.mktime(2009,12,25,22,20)
+ assert_equal(Time.mktime(2009,12,25,22,30), @next.call(now))
+ end
+
+ def test_30_min
+ now = Time.mktime(2009,12,25,22,30)
+ assert_equal(Time.mktime(2009,12,25,23,00), @next.call(now))
+ end
+
+ def test_50_min
+ now = Time.mktime(2009,12,25,22,50)
+ assert_equal(Time.mktime(2009,12,25,23,00), @next.call(now))
+ end
+
+ def test_50_min_next_day
+ now = Time.mktime(2009,12,25,23,50)
+ assert_equal(Time.mktime(2009,12,26,0,0), @next.call(now))
+ end
+
+ def test_50_min_next_month
+ now = Time.mktime(2009,11,30,23,50)
+ assert_equal(Time.mktime(2009,12,1,0,0), @next.call(now))
+ end
+
+ def test_50_min_next_year
+ now = Time.mktime(2009,12,31,23,50)
+ assert_equal(Time.mktime(2010,1,1,0,0), @next.call(now))
+ end
+end
+
+class TestNextTimeGenerator_3600_0 < Test::Unit::TestCase
+ def setup
+ @next = ShogiServer::League::Floodgate::NextTimeGenerator_Floodgate_3600_0.new
+ end
+
+ def test_22_00
+ now = Time.mktime(2009,12,25,22,0)
+ assert_equal(Time.mktime(2009,12,25,23,0), @next.call(now))
+ end
+
+ def test_22_30
+ now = Time.mktime(2009,12,25,22,0)
+ assert_equal(Time.mktime(2009,12,25,23,0), @next.call(now))
+ end
+
+ def test_23_00
+ now = Time.mktime(2009,12,25,23,0)
+ assert_equal(Time.mktime(2009,12,26,1,0), @next.call(now))
+ end
+
+ def test_23_30
+ now = Time.mktime(2009,12,25,23,30)
+ assert_equal(Time.mktime(2009,12,26,1,0), @next.call(now))
+ end
+
+ def test_00_00
+ now = Time.mktime(2009,12,26,0,0)
+ assert_equal(Time.mktime(2009,12,26,1,0), @next.call(now))
+ end
+
+ def test_23_30_next_month
+ now = Time.mktime(2009,11,30,23,30)
+ assert_equal(Time.mktime(2009,12,1,1,0), @next.call(now))
+ end
+
+ def test_23_30_next_year
+ now = Time.mktime(2009,12,31,23,30)
+ assert_equal(Time.mktime(2010,1,1,1,0), @next.call(now))
+ end
+end
+