OSDN Git Service

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