OSDN Git Service

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