OSDN Git Service

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