OSDN Git Service

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