OSDN Git Service

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