]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/games/wheelfortune.rb
wheelfortune: display round count on cancel
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / games / wheelfortune.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Hangman/Wheel Of Fortune
5 #
6 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
7 # Copyright:: (C) 2007 Giuseppe Bilotta
8 # License:: rbot
9
10 # Wheel-of-Fortune Question/Answer
11 class WoFQA
12   attr_accessor :cat, :clue, :answer, :hint
13   def initialize(cat, clue, ans=nil)
14     @cat = cat # category
15     @clue = clue # clue phrase
16     self.answer = ans
17   end
18
19   def catclue
20     ret = ""
21     ret << "(" + cat + ") " unless cat.empty?
22     ret << clue
23   end
24
25   def answer=(ans)
26     if !ans
27       @answer = nil
28       @split = []
29       @hint = []
30       return
31     end
32     @answer = ans.dup.downcase
33     @split = @answer.scan(/./u)
34     @hint = @split.inject([]) { |list, ch|
35       if ch !~ /[a-z]/
36         list << ch
37       else
38         list << "*"
39       end
40     }
41     @used = []
42   end
43
44   def announcement
45     ret = self.catclue << "\n"
46     ret << _("Letters called so far: ") << @used.join(" ") << "\n" unless @used.empty?
47     ret << @hint.join
48   end
49
50   def check(ans_or_letter)
51     d = ans_or_letter.downcase
52     if d == @answer
53       return :gotit
54     elsif d.length == 1
55       if @used.include?(d)
56         return :used
57       else
58         @used << d
59         @used.sort!
60         if @split.include?(d)
61           count = 0
62           @split.each_with_index { |c, i|
63             if c == d
64               @hint[i] = d.upcase
65               count += 1
66             end
67           }
68           return count
69         else
70           return :missing
71         end
72       end
73     else
74       return :wrong
75     end
76   end
77
78 end
79
80 # Wheel-of-Fortune game
81 class WoFGame
82   attr_reader :manager, :single, :max, :pending
83   attr_writer :running
84   def initialize(manager, single, max)
85     @manager = manager
86     @single = single.to_i
87     @max = max.to_i
88     @pending = nil
89     @qas = []
90     @curr_idx = nil
91     @running = false
92     @scores = Hash.new
93   end
94
95   def running?
96     @running
97   end
98
99   def round
100     @curr_idx+1 rescue 0
101   end
102
103   def mark_winner(user)
104     @running = false
105     k = user.botuser
106     if @scores.key?(k)
107       @scores[k][:nick] = user.nick
108       @scores[k][:score] += @single
109     else
110       @scores[k] = { :nick => user.nick, :score => @single }
111     end
112     if @scores[k][:score] >= @max
113       return :done
114     else
115       return :more
116     end
117   end
118
119   def score_table
120     table = []
121     @scores.each { |k, val|
122       table << ["%s (%s)" % [val[:nick], k], val[:score]]
123     }
124     table.sort! { |a, b| b.last <=> a.last }
125   end
126
127   def current
128     return nil unless @curr_idx
129     @qas[@curr_idx]
130   end
131
132   def next
133     # don't advance if there are no further QAs
134     return nil if @curr_idx == @qas.length - 1
135     if @curr_idx
136       @curr_idx += 1
137     else
138       @curr_idx = 0
139     end
140     return current
141   end
142
143   def check(whatever)
144     cur = self.current
145     return nil unless cur
146     return cur.check(whatever)
147   end
148
149   def start_add_qa(cat, clue)
150     return [nil, @pending] if @pending
151     @pending = WoFQA.new(cat.dup, clue.dup)
152     return [true, @pending]
153   end
154
155   def finish_add_qa(ans)
156     return nil unless @pending
157     @pending.answer = ans.dup
158     @qas << @pending
159     @pending = nil
160     return @qas.last
161   end
162 end
163
164 class WheelOfFortune < Plugin
165   def initialize
166     super
167     # TODO load/save running games?
168     @games = Hash.new
169   end
170
171   def setup_game(m, p)
172     chan = p[:chan] || m.channel
173     if !chan
174       m.reply _("you must specify a channel")
175       return
176     end
177     ch = chan.irc_downcase(m.server.casemap).intern
178
179     if @games.key?(ch)
180       m.reply _("there's already a Wheel-of-Fortune game on %{chan}, managed by %{who}") % {
181         :chan => chan,
182         :who => @games[ch].manager
183       }
184       return
185     end
186     @games[ch] = game = WoFGame.new(m.botuser, p[:single], p[:max])
187     @bot.say chan, _("%{who} just created a new Wheel-of-Fortune game to %{max} points (%{single} per question)") % {
188       :who => game.manager,
189       :max => game.max,
190       :single => game.single
191     }
192     @bot.say m.source, _("ok, the game has been created. now add clues and answers with \"wof %{chan} [category: <category>,] clue: <clue>, answer: <ans>\". if the clue and answer don't fit in one line, add the answer separately with \"wof %{chan} answer <answer>\"") % {
193       :chan => chan
194     }
195   end
196
197   def setup_qa(m, p)
198     ch = p[:chan].irc_downcase(m.server.casemap).intern
199     if !@games.key?(ch)
200       m.reply _("there's no Wheel-of-Fortune game running on %{chan}") % {
201         :chan => p[:chan]
202       }
203       return
204     end
205     game = @games[ch]
206     cat = p[:cat].to_s
207     clue = p[:clue].to_s
208     ans = p[:ans].to_s
209     if ans.include?('*')
210       m.reply _("sorry, the answer cannot contain the '*' character")
211       return
212     end
213
214     if !clue.empty?
215       worked, qa = game.start_add_qa(cat, clue)
216       if worked
217         str = ans.empty? ?  _("ok, new clue added for %{chan}: %{catclue}") : nil
218       else
219         str = _("there's already a pending clue for %{chan}: %{catclue}")
220       end
221       m.reply _(str) % { :chan => p[:chan], :catclue => qa.catclue } if str
222       return unless worked or !ans.empty?
223     end
224     if !ans.empty?
225       qa = game.finish_add_qa(ans)
226       if qa
227         str = _("ok, new QA added for %{chan}: %{catclue} => %{ans}")
228       else
229         str = _("there's no pending clue for %{chan}!")
230       end
231       m.reply _(str) % { :chan => p[:chan], :catclue => qa ? qa.catclue : nil, :ans => qa ? qa.answer : nil}
232       announce(m, p.merge({ :next => true }) ) unless game.running?
233     else
234       m.reply _("something went wrong, I can't seem to understand what you're trying to set up")
235     end
236   end
237
238   def announce(m, p={})
239     chan = p[:chan] || m.channel
240     ch = chan.irc_downcase(m.server.casemap).intern
241     if !@games.key?(ch)
242       m.reply _("there's no Wheel-of-Fortune game running on %{chan}") % { :chan => p[:chan] }
243       return
244     end
245     game = @games[ch]
246     qa = p[:next] ? game.next : game.current
247     if !qa
248       m.reply _("there are no Wheel-of-Fortune questions for %{chan}, I'm waiting for %{who} to add them") % {
249         :chan => chan,
250         :who => game.manager
251       }
252       return
253     end
254
255     @bot.say chan, qa.announcement
256     game.running = true
257   end
258
259   def score_table(chan, game, opts={})
260     limit = opts[:limit] || -1
261     table = game.score_table[0..limit]
262     nick_wd = table.map { |a| a.first.length }.max
263     score_wd = table.first.last.to_s.length
264     table.each { |t|
265       @bot.say chan, "%*s : %*u" % [nick_wd, t.first, score_wd, t.last]
266     }
267   end
268
269   def listen(m)
270     return unless m.kind_of?(PrivMessage) and not m.address?
271     ch = m.channel.irc_downcase(m.server.casemap).intern
272     return unless game = @games[ch]
273     return unless game.running?
274     check = game.check(m.message)
275     debug "check: #{check.inspect}"
276     case check
277     when nil
278       # can this happen?
279       warning "game #{game}, qa #{game.current} checked nil against #{m.message}"
280       return
281     when :used
282       # m.reply "STUPID! YOU SO STUPID!"
283       return
284     when :wrong
285       return
286     when Numeric, :missing
287       # TODO may alter score depening on how many letters were guessed
288       # TODO what happens when the last hint reveals the whole answer?
289       announce(m)
290     when :gotit
291       want_more = game.mark_winner(m.source)
292       m.reply _("%{who} got it! The answer was: %{ans}") % {
293         :who => m.sourcenick,
294         :ans => game.current.answer
295       }
296       if want_more == :done
297         # max score reached
298         m.reply _("%{who} wins the game after %{count} rounds!") % {
299           :who => m.sourcenick,
300           :count => game.round
301         }
302         score_table(m.channel, game)
303         @games.delete(ch)
304       else :more
305         score_table(m.channel, game)
306         announce(m, :next => true)
307       end
308     else
309       # can this happen?
310       warning "game #{game}, qa #{game.current} checked #{check} against #{m.message}"
311     end
312   end
313
314   def cancel(m, p)
315     ch = m.channel.irc_downcase(m.server.casemap).intern
316     if !@games.key?(ch)
317       m.reply _("there's no Wheel-of-Fortune game running on %{chan}") % {
318         :chan => m.channel
319       }
320       return
321     end
322     do_cancel(ch)
323   end
324
325   def do_cancel(ch)
326     game = @games.delete(ch)
327     chan = ch.to_s
328     @bot.say chan, _("Wheel-of-Fortune game cancelled after %{count} rounds. Partial score:") % {
329       :count => game.round
330     }
331     score_table(chan, game)
332   end
333
334   def cleanup
335     @games.each_key { |k| do_cancel(k) }
336     super
337   end
338 end
339
340 plugin = WheelOfFortune.new
341
342 plugin.map "wof", :action => 'announce', :private => false
343 plugin.map "wof cancel", :action => 'cancel', :private => false
344 plugin.map "wof [:chan] play for :single [points] to :max [points]", :action => 'setup_game'
345 plugin.map "wof :chan [category: *cat,] clue: *clue[, answer: *ans]", :action => 'setup_qa', :public => false
346 plugin.map "wof :chan answer: *ans", :action => 'setup_qa', :public => false