OSDN Git Service

Merge remote-tracking branch 'origin/wdoor-stable' into wdoor-stable
[shogi-server/shogi-server.git] / shogi_server / league / floodgate.rb
1 require 'shogi_server/util'
2 require 'date'
3 require 'thread'
4 require 'ostruct'
5 require 'pathname'
6
7 module ShogiServer
8
9 class League
10   class Floodgate
11     class << self
12       # ex. "floodgate-900-0"
13       #
14       def game_name?(str)
15         return /^floodgate\-\d+\-\d+$/.match(str) ? true : false
16       end
17
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)
23       end
24     end # class method
25
26     # @next_time is updated  if and only if charge() was called
27     #
28     attr_reader :next_time
29     attr_reader :league, :game_name
30     attr_reader :options
31
32     def initialize(league, hash={})
33       @league = league
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
37       # pairing factory.
38       @options = {}
39       @options[:pairing_factory] = hash[:pairing_factory] || "default_factory"
40       @options[:sacrifice]       = hash[:sacrifice] || "gps500+e293220e3f8a3e59f79f6b0efffaa931"
41       charge if @next_time.nil?
42     end
43
44     def game_name?(str)
45       return Regexp.new(@game_name).match(str) ? true : false
46     end
47
48     def pairing_factory
49       return @options[:pairing_factory]
50     end
51
52     def sacrifice
53       return @options[:sacrifice]
54     end
55
56     def charge
57       ntg = NextTimeGenerator.factory(@game_name)
58       if ntg
59         @next_time = ntg.call(Time.now)
60         @options[:pairing_factory] = ntg.pairing_factory
61         @options[:sacrifice]       = ntg.sacrifice
62       else
63         @next_time = nil
64       end
65     end
66
67     # Returns an array of players who are allowed to participate in this
68     # Floodgate match
69     #
70     def select_players
71       players = @league.find_all_players do |pl|
72         pl.status == "game_waiting" &&
73         game_name?(pl.game_name) &&
74         pl.sente == nil &&
75         pl.rated? # Only players who have player ID can participate in Floodgate (rating match)
76       end
77       return players
78     end
79
80     def match_game
81       log_message("Starting Floodgate games...: %s, %s" % [@game_name, @options])
82       logics = Pairing.send(@options[:pairing_factory], @options)
83       Pairing.match(select_players(), logics)
84     end
85     
86     #
87     #
88     class NextTimeGenerator
89       class << self
90         def factory(game_name)
91           ret = nil
92           conf_file_name = File.join($topdir, "#{game_name}.conf")
93
94           if $DEBUG
95             ret = NextTimeGenerator_Debug.new
96           elsif File.exists?(conf_file_name) 
97             lines = IO.readlines(conf_file_name)
98             ret =  NextTimeGeneratorConfig.new(lines)
99           elsif game_name == "floodgate-900-0"
100             ret = NextTimeGenerator_Floodgate_900_0.new
101           elsif game_name == "floodgate-3600-0"
102             ret = NextTimeGenerator_Floodgate_3600_0.new
103           end
104           return ret
105         end
106       end
107     end
108
109     class AbstructNextTimeGenerator
110
111       attr_reader :pairing_factory
112       attr_reader :sacrifice
113
114       # Constructor. 
115       #
116       def initialize
117         @pairing_factory = "default_factory"
118         @sacrifice       = "gps500+e293220e3f8a3e59f79f6b0efffaa931"
119       end
120     end
121
122     # Schedule the next time from configuration files.
123     #
124     # Line format: 
125     #   # This is a comment line
126     #   set <parameter_name> <value>
127     #   DoW Time
128     #   ...
129     # where
130     #   DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
131     #          "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
132     #          "Friday" | "Saturday" 
133     #   Time := HH:MM
134     #
135     # For example,
136     #   Sat 13:00
137     #   Sat 22:00
138     #   Sun 13:00
139     #
140     # Set parameters:
141     #
142     # * pairing_factory:
143     #   Specifies a factory function name generating a pairing
144     #   method which will be used in a specific Floodgate game.
145     #   ex. set pairing_factory floodgate_zyunisen
146     # * sacrifice:
147     #   Specifies a sacrificed player.
148     #   ex. set sacrifice gps500+e293220e3f8a3e59f79f6b0efffaa931
149     #
150     class NextTimeGeneratorConfig < AbstructNextTimeGenerator
151       
152       # Constructor. 
153       # Read configuration contents.
154       #
155       def initialize(lines)
156         super()
157         @lines = lines
158       end
159
160       def call(now=Time.now)
161         if now.kind_of?(Time)
162           now = ::ShogiServer::time2datetime(now)
163         end
164         candidates = []
165         # now.cweek 1-53
166         # now.cwday 1(Monday)-7
167         @lines.each do |line|
168           case line
169           when %r!^\s*set\s+pairing_factory\s+(\w+)!
170             @pairing_factory = $1.chomp
171           when %r!^\s*set\s+sacrifice\s+(.*)!
172             @sacrifice = $1.chomp
173           when %r!^\s*(\w+)\s+(\d{1,2}):(\d{1,2})!
174             dow, hour, minute = $1, $2.to_i, $3.to_i
175             dow_index = ::ShogiServer::parse_dow(dow)
176             next if dow_index.nil?
177             next unless (0..23).include?(hour)
178             next unless (0..59).include?(minute)
179             time = DateTime::commercial(now.cwyear, now.cweek, dow_index, hour, minute) rescue next
180             time += 7 if time <= now 
181             candidates << time
182           when %r!^\s*#!
183             # Skip comment line
184           when %r!^\s*$!
185             # Skip empty line
186           else
187             log_warning("Floodgate: Unsupported syntax in a next time generator config file: %s" % [line]) 
188           end
189         end
190         candidates.map! {|dt| ::ShogiServer::datetime2time(dt)}
191         return candidates.empty? ? nil : candidates.min
192       end
193     end
194
195     # Schedule the next time for floodgate-900-0: each 30 minutes
196     #
197     class NextTimeGenerator_Floodgate_900_0 < AbstructNextTimeGenerator
198
199       # Constructor. 
200       #
201       def initialize
202         super
203       end
204
205       def call(now)
206         if now.min < 30
207           return Time.mktime(now.year, now.month, now.day, now.hour, 30)
208         else
209           return Time.mktime(now.year, now.month, now.day, now.hour) + 3600
210         end
211       end
212     end
213
214     # Schedule the next time for floodgate-3600-0: each 2 hours (odd hour)
215     #
216     class NextTimeGenerator_Floodgate_3600_0 < AbstructNextTimeGenerator
217
218       # Constructor. 
219       #
220       def initialize
221         super
222       end
223
224       def call(now)
225         return Time.mktime(now.year, now.month, now.day, now.hour) + ((now.hour%2)+1)*3600
226       end
227     end
228
229     # Schedule the next time for debug: each 30 seconds.
230     #
231     class NextTimeGenerator_Debug < AbstructNextTimeGenerator
232
233       # Constructor. 
234       #
235       def initialize
236         super
237       end
238
239       def call(now)
240         if now.sec < 30
241           return Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
242         else
243           return Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60
244         end
245       end
246     end
247
248     #
249     #
250     class History
251       @@mutex = Mutex.new
252
253       class << self
254         def factory(pathname)
255           unless ShogiServer::is_writable_file?(pathname.to_s)
256             log_error("Failed to write a history file: %s" % [pathname]) 
257             return nil
258           end
259           history = History.new pathname
260           history.load
261           return history
262         end
263       end
264
265       attr_reader :records
266
267       # Initialize this instance.
268       # @param file_path_name a Pathname object for this storage
269       #
270       def initialize(file_path_name)
271         @records = []
272         @max_records = 100
273         @file = file_path_name
274       end
275
276       # Return a hash describing the game_result
277       # :game_id: game id
278       # :black:   Black's player id
279       # :white:   White's player id
280       # :winner:  Winner's player id or nil for the game without a winner
281       # :loser:   Loser's player id or nil for the game without a loser
282       #
283       def make_record(game_result)
284         hash = Hash.new
285         hash[:game_id] = game_result.game.game_id
286         hash[:black]   = game_result.black.player_id
287         hash[:white]   = game_result.white.player_id
288         case game_result
289         when GameResultWin
290           hash[:winner] = game_result.winner.player_id
291           hash[:loser]  = game_result.loser.player_id
292         else
293           hash[:winner] = nil
294           hash[:loser]  = nil
295         end
296         return hash
297       end
298
299       def load
300         return unless @file.exist?
301
302         begin
303           @records = YAML.load_file(@file)
304           unless @records && @records.instance_of?(Array)
305             $logger.error "%s is not a valid yaml file. Instead, an empty array will be used and updated." % [@file]
306             @records = []
307           end
308         rescue
309           $logger.error "%s is not a valid yaml file. Instead, an empty array will be used and updated." % [@file]
310           @records = []
311         end
312       end
313
314       def save
315         begin
316           @file.open("w") do |f| 
317             f << YAML.dump(@records)
318           end
319         rescue Errno::ENOSPC
320           # ignore
321         end
322       end
323
324       def update(game_result)
325         record = make_record(game_result)
326         @@mutex.synchronize do 
327           load
328           @records << record
329           while @records.size > @max_records
330             @records.shift
331           end
332           save
333         end
334       end
335       
336       def last_win?(player_id)
337         rc = last_valid_game(player_id)
338         return false unless rc
339         return rc[:winner] == player_id
340       end
341       
342       def last_lose?(player_id)
343         rc = last_valid_game(player_id)
344         return false unless rc
345         return rc[:loser] == player_id
346       end
347
348       def last_opponent(player_id)
349         rc = last_valid_game(player_id)
350         return nil unless rc
351         if rc[:black] == player_id
352           return rc[:white]
353         elsif rc[:white] == player_id
354           return rc[:black]
355         else
356           return nil
357         end
358       end
359
360       def last_valid_game(player_id)
361         records = nil
362         @@mutex.synchronize do
363           records = @records.reverse
364         end
365         rc = records.find do |rc|
366           rc[:winner] && 
367           rc[:loser]  && 
368           (rc[:black] == player_id || rc[:white] == player_id)
369         end
370         return rc
371       end
372
373       def win_games(player_id)
374         records = nil
375         @@mutex.synchronize do
376           records = @records.reverse
377         end
378         rc = records.find_all do |rc|
379           rc[:winner] == player_id && rc[:loser]
380         end
381         return rc
382       end
383
384       def loss_games(player_id)
385         records = nil
386         @@mutex.synchronize do
387           records = @records.reverse
388         end
389         rc = records.find_all do |rc|
390           rc[:winner] && rc[:loser] == player_id
391         end
392         return rc
393       end
394     end # class History
395
396
397   end # class Floodgate
398
399
400 end # class League
401 end # module ShogiServer