]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/games/wheelfortune.rb
wheelfortune: rework replies to QA additions, providing the round number at which...
[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
46     if !@used.empty?
47       ret << _(" [Letters called so far: %{red}%{letters}%{nocolor}]") % {
48         :red => Irc.color(:red),
49         :letters => @used.join(" "),
50         :nocolor => Irc.color()
51       }
52     end
53     ret << "\n"
54     ret << @hint.join
55   end
56
57   def check(ans_or_letter)
58     d = ans_or_letter.downcase
59     if d == @answer
60       return :gotit
61     elsif d.length == 1
62       if @used.include?(d)
63         return :used
64       else
65         @used << d
66         @used.sort!
67         if @split.include?(d)
68           count = 0
69           @split.each_with_index { |c, i|
70             if c == d
71               @hint[i] = d.upcase
72               count += 1
73             end
74           }
75           return count
76         else
77           return :missing
78         end
79       end
80     else
81       return :wrong
82     end
83   end
84
85 end
86
87 # Wheel-of-Fortune game
88 class WoFGame
89   attr_reader :name, :manager, :single, :max, :pending
90   attr_writer :running
91   attr_accessor :must_buy, :price
92   def initialize(name, manager, single, max)
93     @name = name.dup
94     @manager = manager
95     @single = single.to_i
96     @max = max.to_i
97     @pending = nil
98     @qas = []
99     @curr_idx = nil
100     @running = false
101     @scores = Hash.new
102
103     # the default is to make vowels usable only
104     # after paying a price in points which is
105     # a fraction of the single round score equal
106     # to the number of rounds needed to win the game
107     # TODO customize
108     @must_buy = %w{a e i o u y}
109     @price = @single*@single/@max
110   end
111
112   def running?
113     @running
114   end
115
116   def round
117     @curr_idx+1 rescue 0
118   end
119
120   def length
121     @qas.length
122   end
123
124   def buy(user)
125     k = user.botuser
126     if @scores.key?(k) and @scores[k][:score] >= @price
127       @scores[k][:score] -= @price
128       return true
129     else
130       return false
131     end
132   end
133
134   def score(user)
135     k = user.botuser
136     if @scores.key?(k)
137       @scores[k][:score]
138     else
139       0
140     end
141   end
142
143   def mark_winner(user)
144     @running = false
145     k = user.botuser
146     if @scores.key?(k)
147       @scores[k][:nick] = user.nick
148       @scores[k][:score] += @single
149     else
150       @scores[k] = { :nick => user.nick, :score => @single }
151     end
152     if @scores[k][:score] >= @max
153       return :done
154     else
155       return :more
156     end
157   end
158
159   def score_table
160     table = []
161     @scores.each { |k, val|
162       table << ["%s (%s)" % [val[:nick], k], val[:score]]
163     }
164     table.sort! { |a, b| b.last <=> a.last }
165   end
166
167   def current
168     return nil unless @curr_idx
169     @qas[@curr_idx]
170   end
171
172   def next
173     # don't advance if there are no further QAs
174     return nil if @curr_idx == @qas.length - 1
175     if @curr_idx
176       @curr_idx += 1
177     else
178       @curr_idx = 0
179     end
180     return current
181   end
182
183   def check(whatever, o={})
184     cur = self.current
185     return nil unless cur
186     if @must_buy.include?(whatever) and not o[:buy]
187       return whatever
188     end
189     return cur.check(whatever)
190   end
191
192   def start_add_qa(cat, clue)
193     return [nil, @pending] if @pending
194     @pending = WoFQA.new(cat.dup, clue.dup)
195     return [true, @pending]
196   end
197
198   def finish_add_qa(ans)
199     return nil unless @pending
200     @pending.answer = ans.dup
201     @qas << @pending
202     @pending = nil
203     return @qas.last
204   end
205 end
206
207 class WheelOfFortune < Plugin
208   Config.register Config::StringValue.new('wheelfortune.game_name',
209     :default => 'Wheel Of Fortune',
210     :desc => "default name of the Wheel Of Fortune game")
211
212   def initialize
213     super
214     # TODO load/save running games?
215     @games = Hash.new
216   end
217
218   def setup_game(m, p)
219     chan = p[:chan] || m.channel
220     if !chan
221       m.reply _("you must specify a channel")
222       return
223     end
224     ch = chan.irc_downcase(m.server.casemap).intern
225
226     if game = @games[ch]
227       m.reply _("there's already a %{name} game on %{chan}, managed by %{who}") % {
228         :name => game.name,
229         :chan => chan,
230         :who => game.manager
231       }
232       return
233     end
234     name = p[:name].to_s
235     if name.empty?
236       name = m.source.get_botdata("wheelfortune.game_name") || @bot.config['wheelfortune.game_name']
237     else
238       m.source.set_botdata("wheelfortune.game_name", name.dup)
239     end
240     @games[ch] = game = WoFGame.new(name, m.botuser, p[:single], p[:max])
241     @bot.say chan, _("%{who} just created a new %{name} game to %{max} points (%{single} per question, %{price} per vowel)") % {
242       :name => game.name,
243       :who => game.manager,
244       :max => game.max,
245       :single => game.single,
246       :price => game.price
247     }
248     @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>\"") % {
249       :chan => chan
250     }
251   end
252
253   def setup_qa(m, p)
254     ch = p[:chan].irc_downcase(m.server.casemap).intern
255     if !@games.key?(ch)
256       m.reply _("there's no %{name} game running on %{chan}") % {
257         :name => @bot.config['wheelfortune.game_name'],
258         :chan => p[:chan]
259       }
260       return
261     end
262     game = @games[ch]
263
264     if m.botuser != game.manager and !m.botuser.permit?('wheelfortune::manage::other::add')
265       m.reply _("you can't add questions to the %{name} game on %{chan}") % {
266         :name => game.name,
267         :chan => p[:chan]
268       }
269     end
270
271     cat = p[:cat].to_s
272     clue = p[:clue].to_s
273     ans = p[:ans].to_s
274     if ans.include?('*')
275       m.reply _("sorry, the answer cannot contain the '*' character")
276       return
277     end
278
279     if !clue.empty?
280       worked, qa = game.start_add_qa(cat, clue)
281       if worked
282         str = ans.empty? ?  _("ok, clue added for %{name} round %{count} on %{chan}: %{catclue}") : nil
283       else
284         str = _("there's already a pending clue for %{name} round %{count} on %{chan}: %{catclue}")
285       end
286       m.reply _(str) % {
287         :chan => p[:chan],
288         :catclue => qa.catclue,
289         :name => game.name,
290         :count => game.length+1
291       } if str
292       return unless worked and !ans.empty?
293     end
294     if !ans.empty?
295       qa = game.finish_add_qa(ans)
296       if qa
297         str = _("ok, QA added for %{name} round %{count} on %{chan}: %{catclue} => %{ans}")
298       else
299         str = _("there's no pending clue for %{name} on %{chan}!")
300       end
301       m.reply _(str) % {
302         :chan => p[:chan],
303         :catclue => qa ? qa.catclue : nil,
304         :ans => qa ? qa.answer : nil,
305         :name => game.name,
306         :count => game.length
307       }
308       announce(m, p.merge({ :next => true }) ) unless game.running?
309     else
310       m.reply _("something went wrong, I can't seem to understand what you're trying to set up") if clue.empty?
311     end
312   end
313
314   def announce(m, p={})
315     chan = p[:chan] || m.channel
316     ch = chan.irc_downcase(m.server.casemap).intern
317     if !@games.key?(ch)
318       m.reply _("there's no %{name} game running on %{chan}") % {
319         :name => @bot.config['wheelfortune.game_name'],
320         :chan => chan
321       }
322       return
323     end
324     game = @games[ch]
325     qa = p[:next] ? game.next : game.current
326     if !qa
327       m.reply _("there are no %{name} questions for %{chan}, I'm waiting for %{who} to add them") % {
328         :name => game.name,
329         :chan => chan,
330         :who => game.manager
331       }
332       return
333     end
334
335     @bot.say chan, _("%{bold}%{color}%{name}%{bold}, round %{count}:%{nocolor} %{qa}") % {
336       :bold => Bold,
337       :color => Irc.color(:green),
338       :name => game.name,
339       :count => game.round,
340       :nocolor => Irc.color(),
341       :qa => qa.announcement,
342     }
343     game.running = true
344   end
345
346   def score_table(chan, game, opts={})
347     limit = opts[:limit] || -1
348     table = game.score_table[0..limit]
349     if table.length == 0
350       @bot.say chan, _("no scores")
351       return
352     end
353     nick_wd = table.map { |a| a.first.length }.max
354     score_wd = table.first.last.to_s.length
355     table.each { |t|
356       @bot.say chan, "%*s : %*u" % [nick_wd, t.first, score_wd, t.last]
357     }
358   end
359
360   def react_on_check(m, ch, game, check)
361     debug "check: #{check.inspect}"
362     case check
363     when nil
364       # can this happen?
365       warning "game #{game}, qa #{game.current} checked nil against #{m.message}"
366       return
367     when :used
368       # m.reply "STUPID! YOU SO STUPID!"
369       return
370     when *game.must_buy
371       m.nickreply _("You must buy the %{vowel}") % {
372         :vowel => check
373       }
374     when :wrong
375       return
376     when Numeric, :missing
377       # TODO may alter score depening on how many letters were guessed
378       # TODO what happens when the last hint reveals the whole answer?
379       announce(m)
380     when :gotit
381       want_more = game.mark_winner(m.source)
382       m.reply _("%{who} got it! The answer was: %{ans}") % {
383         :who => m.sourcenick,
384         :ans => game.current.answer
385       }
386       if want_more == :done
387         # max score reached
388         m.reply _("%{bold}%{color}%{name}%{bold}%{nocolor}: %{who} %{bold}wins%{bold} after %{count} rounds!\nThe final score is") % {
389           :bold => Bold,
390           :color => Irc.color(:green),
391           :who => m.sourcenick,
392           :name => game.name,
393           :count => game.round,
394           :nocolor => Irc.color()
395         }
396         score_table(m.channel, game)
397         @games.delete(ch)
398       else :more
399         m.reply _("%{bold}%{color}%{name}%{bold}, round %{count}%{nocolor} -- score so far:") % {
400           :bold => Bold,
401           :color => Irc.color(:green),
402           :name => game.name,
403           :count => game.round,
404           :nocolor => Irc.color()
405         }
406         score_table(m.channel, game)
407         announce(m, :next => true)
408       end
409     else
410       # can this happen?
411       warning "game #{game}, qa #{game.current} checked #{check} against #{m.message}"
412     end
413   end
414
415   def listen(m)
416     return unless m.kind_of?(PrivMessage) and not m.address?
417     ch = m.channel.irc_downcase(m.server.casemap).intern
418     return unless game = @games[ch]
419     return unless game.running?
420     check = game.check(m.message, :buy => false)
421     react_on_check(m, ch, game, check)
422   end
423
424   def buy(m, p)
425     ch = m.channel.irc_downcase(m.server.casemap).intern
426     game = @games[ch]
427     if not game
428       m.reply _("there's no %{name} game running on %{chan}") % {
429         :name => @bot.config['wheelfortune.game_name'],
430         :chan => m.channel
431       }
432       return
433     elsif !game.running?
434       m.reply _("there are no %{name} questions for %{chan}, I'm waiting for %{who} to add them") % {
435         :name => game.name,
436         :chan => chan,
437         :who => game.manager
438       }
439       return
440     else
441       vowel = p[:vowel]
442       bought = game.buy(m.source)
443       if bought
444         m.reply _("%{who} buys a %{vowel} for %{price} points") % {
445           :who => m.source,
446           :vowel => vowel,
447           :price => game.price
448         }
449         check = game.check(vowel, :buy => true)
450         react_on_check(m, ch, game, check)
451       else
452         m.reply _("you can't buy a %{vowel}, %{who}: it costs %{price} points and you only have %{score}") % {
453           :who => m.source,
454           :vowel => vowel,
455           :price => game.price,
456           :score => game.score(m.source)
457         }
458       end
459     end
460   end
461
462   def cancel(m, p)
463     ch = m.channel.irc_downcase(m.server.casemap).intern
464     if !@games.key?(ch)
465       m.reply _("there's no %{name} game running on %{chan}") % {
466         :name => @bot.config['wheelfortune.game_name'],
467         :chan => m.channel
468       }
469       return
470     end
471     # is the botuser the manager or allowed to cancel someone else's game?
472     if m.botuser == game.manager or m.botuser.permit?('wheelfortune::manage::other::cancel')
473       do_cancel(ch)
474     else
475       m.reply _("you can't cancel the current game")
476     end
477   end
478
479   def do_cancel(ch)
480     game = @games.delete(ch)
481     chan = ch.to_s
482     @bot.say chan, _("%{name} game cancelled after %{count} rounds. Partial score:") % {
483       :name => game.name,
484       :count => game.round
485     }
486     score_table(chan, game)
487   end
488
489   def cleanup
490     @games.each_key { |k| do_cancel(k) }
491     super
492   end
493 end
494
495 plugin = WheelOfFortune.new
496
497 plugin.map "wof", :action => 'announce', :private => false
498 plugin.map "wof cancel", :action => 'cancel', :private => false
499 plugin.map "wof [:chan] play [*name] for :single [points] to :max [points]", :action => 'setup_game'
500 plugin.map "wof :chan [category: *cat,] clue: *clue[, answer: *ans]", :action => 'setup_qa', :public => false
501 plugin.map "wof :chan answer: *ans", :action => 'setup_qa', :public => false
502 plugin.map "wof buy :vowel", :action => 'buy', :requirements => { :vowel => /./u }