1 require 'shogi_server/util'
12 # ex. "floodgate-900-0"
15 return /^floodgate\-\d+\-\d+F?$/.match(str) ? true : false
18 def history_file_path(gamename)
19 return nil unless game_name?(gamename)
20 filename = "floodgate_history_%s.yaml" % [gamename.gsub("floodgate-", "").gsub("-","_")]
21 file = File.join($topdir, filename)
22 return Pathname.new(file)
26 # @next_time is updated if and only if charge() was called
28 attr_reader :next_time
29 attr_reader :league, :game_name
32 def initialize(league, hash={})
34 @next_time = hash[:next_time] || nil
35 @game_name = hash[:game_name] || "floodgate-900-0"
36 # Options will be updated by NextTimeGenerator and then passed to a
39 @options[:pairing_factory] = hash[:pairing_factory] || "default_factory"
40 @options[:sacrifice] = hash[:sacrifice] || "gps500+e293220e3f8a3e59f79f6b0efffaa931"
41 @options[:max_moves] = hash[:max_moves] || Default_Max_Moves
42 @options[:least_time_per_move] = hash[:least_time_per_move] || Default_Least_Time_Per_Move
43 charge if @next_time.nil?
47 return Regexp.new(@game_name).match(str) ? true : false
51 return @options[:pairing_factory]
55 return @options[:sacrifice]
59 return @options[:max_moves]
62 def least_time_per_move
63 return @options[:least_time_per_move]
67 ntg = NextTimeGenerator.factory(@game_name)
69 @next_time = ntg.call(Time.now)
70 @options[:pairing_factory] = ntg.pairing_factory
71 @options[:sacrifice] = ntg.sacrifice
72 @options[:max_moves] = ntg.max_moves
73 @options[:least_time_per_move] = ntg.least_time_per_move
79 # Returns an array of players who are allowed to participate in this
83 players = @league.find_all_players do |pl|
84 pl.status == "game_waiting" &&
85 game_name?(pl.game_name) &&
87 pl.rated? # Only players who have player ID can participate in Floodgate (rating match)
93 log_message("Starting Floodgate games...: %s, %s" % [@game_name, @options])
94 logics = Pairing.send(@options[:pairing_factory], @options)
95 Pairing.match(select_players(), logics, @options)
100 class NextTimeGenerator
102 def factory(game_name)
104 conf_file_name = File.join($topdir, "#{game_name}.conf")
107 ret = NextTimeGenerator_Debug.new
108 elsif File.exists?(conf_file_name)
109 lines = IO.readlines(conf_file_name)
110 ret = NextTimeGeneratorConfig.new(lines)
111 elsif game_name == "floodgate-900-0"
112 ret = NextTimeGenerator_Floodgate_900_0.new
113 elsif game_name == "floodgate-3600-0"
114 ret = NextTimeGenerator_Floodgate_3600_0.new
121 class AbstructNextTimeGenerator
123 attr_reader :pairing_factory
124 attr_reader :sacrifice
125 attr_reader :max_moves
126 attr_reader :least_time_per_move
131 @pairing_factory = "default_factory"
132 @sacrifice = "gps500+e293220e3f8a3e59f79f6b0efffaa931"
133 @max_moves = Default_Max_Moves
134 @least_time_per_move = Default_Least_Time_Per_Move
138 # Schedule the next time from configuration files.
141 # # This is a comment line
142 # set <parameter_name> <value>
146 # DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
147 # "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
148 # "Friday" | "Saturday"
159 # Specifies a factory function name generating a pairing
160 # method which will be used in a specific Floodgate game.
161 # ex. set pairing_factory floodgate_zyunisen
163 # Specifies a sacrificed player.
164 # ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
166 # Sepcifies a number of max moves
167 # ex. set max_moves 256
168 # * least_time_per_move:
169 # Sepcifies a least time per move
170 # ex. set least_time_per_move 0
172 class NextTimeGeneratorConfig < AbstructNextTimeGenerator
175 # Read configuration contents.
177 def initialize(lines)
182 def call(now=Time.now)
183 if now.kind_of?(Time)
184 now = ::ShogiServer::time2datetime(now)
188 # now.cwday 1(Monday)-7
189 @lines.each do |line|
191 when %r!^\s*set\s+pairing_factory\s+(\w+)!
192 @pairing_factory = $1.chomp
193 when %r!^\s*set\s+sacrifice\s+(.*)!
194 @sacrifice = $1.chomp
195 when %r!^\s*set\s+max_moves\s+(\d+)!
196 @max_moves = $1.chomp.to_i
197 when %r!^\s*set\s+least_time_per_move\s+(\d+)!
198 @least_time_per_move = $1.chomp.to_i
199 when %r!^\s*(\w+)\s+(\d{1,2}):(\d{1,2})!
200 dow, hour, minute = $1, $2.to_i, $3.to_i
201 dow_index = ::ShogiServer::parse_dow(dow)
202 next if dow_index.nil?
203 next unless (0..23).include?(hour)
204 next unless (0..59).include?(minute)
205 time = DateTime::commercial(now.cwyear, now.cweek, dow_index, hour, minute) rescue next
206 time += 7 if time <= now
213 log_warning("Floodgate: Unsupported syntax in a next time generator config file: %s" % [line])
216 candidates.map! {|dt| ::ShogiServer::datetime2time(dt)}
217 return candidates.empty? ? nil : candidates.min
221 # Schedule the next time for floodgate-900-0: each 30 minutes
223 class NextTimeGenerator_Floodgate_900_0 < AbstructNextTimeGenerator
233 return Time.mktime(now.year, now.month, now.day, now.hour, 30)
235 return Time.mktime(now.year, now.month, now.day, now.hour) + 3600
240 # Schedule the next time for floodgate-3600-0: each 2 hours (odd hour)
242 class NextTimeGenerator_Floodgate_3600_0 < AbstructNextTimeGenerator
251 return Time.mktime(now.year, now.month, now.day, now.hour) + ((now.hour%2)+1)*3600
255 # Schedule the next time for debug: each 30 seconds.
257 class NextTimeGenerator_Debug < AbstructNextTimeGenerator
267 return Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
269 return Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60
280 def factory(pathname)
281 unless ShogiServer::is_writable_file?(pathname.to_s)
282 log_error("Failed to write a history file: %s" % [pathname])
285 history = History.new pathname
293 # Initialize this instance.
294 # @param file_path_name a Pathname object for this storage
296 def initialize(file_path_name)
299 @file = file_path_name
302 # Return a hash describing the game_result
304 # :black: Black's player id
305 # :white: White's player id
306 # :winner: Winner's player id or nil for the game without a winner
307 # :loser: Loser's player id or nil for the game without a loser
309 def make_record(game_result)
311 hash[:game_id] = game_result.game.game_id
312 hash[:black] = game_result.black.player_id
313 hash[:white] = game_result.white.player_id
316 hash[:winner] = game_result.winner.player_id
317 hash[:loser] = game_result.loser.player_id
326 return unless @file.exist?
329 @records = YAML.load_file(@file)
330 unless @records && @records.instance_of?(Array)
331 $logger.error "%s is not a valid yaml file. Instead, an empty array will be used and updated." % [@file]
335 $logger.error "%s is not a valid yaml file. Instead, an empty array will be used and updated." % [@file]
342 @file.open("w") do |f|
343 f << YAML.dump(@records)
350 def update(game_result)
351 record = make_record(game_result)
352 @@mutex.synchronize do
355 while @records.size > @max_records
362 def last_win?(player_id)
363 rc = last_valid_game(player_id)
364 return false unless rc
365 return rc[:winner] == player_id
368 def last_lose?(player_id)
369 rc = last_valid_game(player_id)
370 return false unless rc
371 return rc[:loser] == player_id
374 def last_opponent(player_id)
375 rc = last_valid_game(player_id)
377 if rc[:black] == player_id
379 elsif rc[:white] == player_id
386 def last_valid_game(player_id)
388 @@mutex.synchronize do
389 records = @records.reverse
391 ret = records.find do |rc|
394 (rc[:black] == player_id || rc[:white] == player_id)
399 def win_games(player_id)
401 @@mutex.synchronize do
402 records = @records.reverse
404 ret = records.find_all do |rc|
405 rc[:winner] == player_id && rc[:loser]
410 def loss_games(player_id)
412 @@mutex.synchronize do
413 records = @records.reverse
415 ret = records.find_all do |rc|
416 rc[:winner] && rc[:loser] == player_id
423 end # class Floodgate
427 end # module ShogiServer