X-Git-Url: http://git.sourceforge.jp/view?p=shogi-server%2Fshogi-server.git;a=blobdiff_plain;f=shogi_server%2Fleague%2Ffloodgate.rb;h=da46a8aa0eca18ec8bfc86ab40b15326c7d6a9d6;hp=aeab05c3e7b3dc75c9df210e06101ba5117913cb;hb=9c4be91e5120e8fd4c5aa2e3def7bad24fea03cb;hpb=d70addf01e98103933e55e620b65e79a80900a9c diff --git a/shogi_server/league/floodgate.rb b/shogi_server/league/floodgate.rb index aeab05c..da46a8a 100644 --- a/shogi_server/league/floodgate.rb +++ b/shogi_server/league/floodgate.rb @@ -1,3 +1,5 @@ +require 'shogi_server/util' +require 'date' require 'thread' require 'ostruct' require 'pathname' @@ -7,49 +9,267 @@ module ShogiServer 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 + return /^floodgate\-\d+\-\d+F?$/.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 + + # @next_time is updated if and only if charge() was called + # + attr_reader :next_time + attr_reader :league, :game_name + attr_reader :options - def initialize(league, next_time=nil) + def initialize(league, hash={}) @league = league - @next_time = next_time - charge + @next_time = hash[:next_time] || nil + @game_name = hash[:game_name] || "floodgate-900-0" + # Options will be updated by NextTimeGenerator and then passed to a + # pairing factory. + @options = {} + @options[:pairing_factory] = hash[:pairing_factory] || "default_factory" + @options[:sacrifice] = hash[:sacrifice] || "gps500+e293220e3f8a3e59f79f6b0efffaa931" + @options[:max_moves] = hash[:max_moves] || Default_Max_Moves + @options[:least_time_per_move] = hash[:least_time_per_move] || Default_Least_Time_Per_Move + charge if @next_time.nil? + end + + def game_name?(str) + return Regexp.new(@game_name).match(str) ? true : false + end + + def pairing_factory + return @options[:pairing_factory] + end + + def sacrifice + return @options[:sacrifice] + end + + def max_moves + return @options[:max_moves] + end + + def least_time_per_move + return @options[:least_time_per_move] 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) + @options[:pairing_factory] = ntg.pairing_factory + @options[:sacrifice] = ntg.sacrifice + @options[:max_moves] = ntg.max_moves + @options[:least_time_per_move] = ntg.least_time_per_move 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 - def match_game + # Returns an array of players who are allowed to participate in this + # Floodgate match + # + def select_players players = @league.find_all_players do |pl| pl.status == "game_waiting" && - Floodgate.game_name?(pl.game_name) && - pl.sente == nil + game_name?(pl.game_name) && + pl.sente == nil && + pl.rated? # Only players who have player ID can participate in Floodgate (rating match) end - Pairing.match(players) + return players end + def match_game + log_message("Starting Floodgate games...: %s, %s" % [@game_name, @options]) + logics = Pairing.send(@options[:pairing_factory], @options) + Pairing.match(select_players(), logics, @options) + end + + # + # + class NextTimeGenerator + class << self + def factory(game_name) + ret = nil + conf_file_name = File.join($topdir, "#{game_name}.conf") + + if $DEBUG + ret = NextTimeGenerator_Debug.new + elsif File.exists?(conf_file_name) + lines = IO.readlines(conf_file_name) + ret = NextTimeGeneratorConfig.new(lines) + 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 AbstructNextTimeGenerator + + attr_reader :pairing_factory + attr_reader :sacrifice + attr_reader :max_moves + attr_reader :least_time_per_move + + # Constructor. + # + def initialize + @pairing_factory = "default_factory" + @sacrifice = "gps500+e293220e3f8a3e59f79f6b0efffaa931" + @max_moves = Default_Max_Moves + @least_time_per_move = Default_Least_Time_Per_Move + end + end + + # Schedule the next time from configuration files. + # + # Line format: + # # This is a comment line + # set + # DoW Time + # ... + # where + # DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | + # "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | + # "Friday" | "Saturday" + # Time := HH:MM + # + # For example, + # Sat 13:00 + # Sat 22:00 + # Sun 13:00 + # + # Set parameters: + # + # * pairing_factory: + # Specifies a factory function name generating a pairing + # method which will be used in a specific Floodgate game. + # ex. set pairing_factory floodgate_zyunisen + # * sacrifice: + # Specifies a sacrificed player. + # ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931 + # * max_moves: + # Sepcifies a number of max moves + # ex. set max_moves 256 + # * least_time_per_move: + # Sepcifies a least time per move + # ex. set least_time_per_move 0 + # + class NextTimeGeneratorConfig < AbstructNextTimeGenerator + + # Constructor. + # Read configuration contents. + # + def initialize(lines) + super() + @lines = lines + end + + def call(now=Time.now) + if now.kind_of?(Time) + now = ::ShogiServer::time2datetime(now) + end + candidates = [] + # now.cweek 1-53 + # now.cwday 1(Monday)-7 + @lines.each do |line| + case line + when %r!^\s*set\s+pairing_factory\s+(\w+)! + @pairing_factory = $1.chomp + when %r!^\s*set\s+sacrifice\s+(.*)! + @sacrifice = $1.chomp + when %r!^\s*set\s+max_moves\s+(\d+)! + @max_moves = $1.chomp.to_i + when %r!^\s*set\s+least_time_per_move\s+(\d+)! + @least_time_per_move = $1.chomp.to_i + when %r!^\s*(\w+)\s+(\d{1,2}):(\d{1,2})! + dow, hour, minute = $1, $2.to_i, $3.to_i + dow_index = ::ShogiServer::parse_dow(dow) + next if dow_index.nil? + next unless (0..23).include?(hour) + next unless (0..59).include?(minute) + time = DateTime::commercial(now.cwyear, now.cweek, dow_index, hour, minute) rescue next + time += 7 if time <= now + candidates << time + when %r!^\s*#! + # Skip comment line + when %r!^\s*$! + # Skip empty line + else + log_warning("Floodgate: Unsupported syntax in a next time generator config file: %s" % [line]) + end + end + candidates.map! {|dt| ::ShogiServer::datetime2time(dt)} + return candidates.empty? ? nil : candidates.min + end + end + + # Schedule the next time for floodgate-900-0: each 30 minutes + # + class NextTimeGenerator_Floodgate_900_0 < AbstructNextTimeGenerator + + # Constructor. + # + def initialize + super + end + + def call(now) + 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 + + # Schedule the next time for floodgate-3600-0: each 2 hours (odd hour) + # + class NextTimeGenerator_Floodgate_3600_0 < AbstructNextTimeGenerator + + # Constructor. + # + def initialize + super + end + + def call(now) + return Time.mktime(now.year, now.month, now.day, now.hour) + ((now.hour%2)+1)*3600 + end + end + + # Schedule the next time for debug: each 30 seconds. + # + class NextTimeGenerator_Debug < AbstructNextTimeGenerator + + # Constructor. + # + def initialize + super + end + + def call(now) + 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 # # @@ -57,9 +277,12 @@ class League @@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 @@ -102,8 +325,13 @@ class League def load return unless @file.exist? - @records = YAML.load_file(@file) - unless @records && @records.instance_of?(Array) + begin + @records = YAML.load_file(@file) + unless @records && @records.instance_of?(Array) + $logger.error "%s is not a valid yaml file. Instead, an empty array will be used and updated." % [@file] + @records = [] + end + rescue $logger.error "%s is not a valid yaml file. Instead, an empty array will be used and updated." % [@file] @records = [] end @@ -143,17 +371,51 @@ class League return rc[:loser] == player_id end + def last_opponent(player_id) + rc = last_valid_game(player_id) + return nil unless rc + if rc[:black] == player_id + return rc[:white] + elsif rc[:white] == player_id + return rc[:black] + else + return nil + end + end + def last_valid_game(player_id) records = nil @@mutex.synchronize do records = @records.reverse end - rc = records.find do |rc| + ret = records.find do |rc| rc[:winner] && rc[:loser] && (rc[:black] == player_id || rc[:white] == player_id) end - return rc + return ret + end + + def win_games(player_id) + records = nil + @@mutex.synchronize do + records = @records.reverse + end + ret = records.find_all do |rc| + rc[:winner] == player_id && rc[:loser] + end + return ret + end + + def loss_games(player_id) + records = nil + @@mutex.synchronize do + records = @records.reverse + end + ret = records.find_all do |rc| + rc[:winner] && rc[:loser] == player_id + end + return ret end end # class History