OSDN Git Service

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