OSDN Git Service

* [shogi-server]
[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.year, 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         @records = YAML.load_file(@file)
220         unless @records && @records.instance_of?(Array)
221           $logger.error "%s is not a valid yaml file. Instead, an empty array will be used and updated." % [@file]
222           @records = []
223         end
224       end
225
226       def save
227         begin
228           @file.open("w") do |f| 
229             f << YAML.dump(@records)
230           end
231         rescue Errno::ENOSPC
232           # ignore
233         end
234       end
235
236       def update(game_result)
237         record = make_record(game_result)
238         @@mutex.synchronize do 
239           load
240           @records << record
241           while @records.size > @max_records
242             @records.shift
243           end
244           save
245         end
246       end
247       
248       def last_win?(player_id)
249         rc = last_valid_game(player_id)
250         return false unless rc
251         return rc[:winner] == player_id
252       end
253       
254       def last_lose?(player_id)
255         rc = last_valid_game(player_id)
256         return false unless rc
257         return rc[:loser] == player_id
258       end
259
260       def last_valid_game(player_id)
261         records = nil
262         @@mutex.synchronize do
263           records = @records.reverse
264         end
265         rc = records.find do |rc|
266           rc[:winner] && 
267           rc[:loser]  && 
268           (rc[:black] == player_id || rc[:white] == player_id)
269         end
270         return rc
271       end
272     end # class History
273
274
275   end # class Floodgate
276
277
278 end # class League
279 end # module ShogiServer