4 # :title: Hangman/Wheel Of Fortune
6 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
7 # Copyright:: (C) 2007 Giuseppe Bilotta
10 # Wheel-of-Fortune Question/Answer
12 attr_accessor :cat, :clue, :hint
14 def initialize(cat, clue, ans=nil)
16 @clue = clue # clue phrase
22 ret << "(" + cat + ") " unless cat.empty?
33 @answer = ans.dup.downcase
34 @split = @answer.scan(/./u)
35 @hint = @split.inject([]) { |list, ch|
48 ret << _(" [Letters called so far: %{red}%{letters}%{nocolor}]") % {
49 :red => Irc.color(:red),
50 :letters => @used.join(" "),
51 :nocolor => Irc.color()
58 def check(ans_or_letter)
59 d = ans_or_letter.downcase
70 @split.each_with_index { |c, i|
88 # Wheel-of-Fortune game
90 attr_reader :name, :manager, :single, :max, :pending
92 attr_accessor :must_buy, :price
93 def initialize(name, manager, single, max)
104 # the default is to make vowels usable only
105 # after paying a price in points which is
106 # a fraction of the single round score equal
107 # to the number of rounds needed to win the game
109 @must_buy = %w{a e i o u y}
110 @price = @single*@single/@max
126 if @pending and round == self.length + 1
135 if @scores.key?(k) and @scores[k][:score] >= @price
136 @scores[k][:score] -= @price
152 def mark_winner(user)
156 @scores[k][:nick] = user.nick
157 @scores[k][:score] += @single
159 @scores[k] = { :nick => user.nick, :score => @single }
161 if @scores[k][:score] >= @max
170 @scores.each { |k, val|
171 table << ["%s (%s)" % [val[:nick], k], val[:score]]
173 table.sort! { |a, b| b.last <=> a.last }
177 return nil unless @curr_idx
182 # don't advance if there are no further QAs
183 return nil if @curr_idx == @qas.length - 1
192 def check(whatever, o={})
194 return nil unless cur
195 if @must_buy.include?(whatever) and not o[:buy]
198 return cur.check(whatever)
201 def start_add_qa(cat, clue)
202 return [nil, @pending] if @pending
203 @pending = WoFQA.new(cat.dup, clue.dup)
204 return [true, @pending]
207 def finish_add_qa(ans)
208 return nil unless @pending
209 @pending.answer = ans.dup
216 class WheelOfFortune < Plugin
217 Config.register Config::StringValue.new('wheelfortune.game_name',
218 :default => 'Wheel Of Fortune',
219 :desc => "default name of the Wheel Of Fortune game")
223 # TODO load/save running games?
227 def help(plugin, topic="")
230 _("wof [<channel>] play [<name>] for <single> to <max> => starts a wheel-of-fortune game on channel <channel> (default: current channel), named <name> (default: wheelfortune.game_name config setting, or the last game name used by the user), with <single> points per round. the game is won when a player reachers <max> points. vowels cost <single>*<single>/<max> points. The user that starts the game is the game manager and must set up the clues and answers in private. All the other users have to learn the answer to each clue by saying single consonants or the whole sentence. Every time a consonant is guessed, the bot will reveal the partial answer, showing the missing letters as * (asterisks).")
231 when 'category', 'clue', 'answer'
232 _("wof <channel> [category: <cat>,] clue: <clue>, answer: <ans> => set up a new question for the wheel-of-fortune game being played on channel <channel>. This command must be sent in private by the game manager. The category <cat> can be omitted. If you make mistakes, you can use 'wof replace' (see help) before the question gets asked")
234 _("wof <channel> replace <round> [category: <cat>,] [clue: <clue>,] [answer: <ans>] => fix the question for round <round> of the wheel-of-fortune game being played on <channel> by replacing the category and/or clue and/or answer")
236 _("wof cancel => cancels the wheel-of-fortune being played on the current channel")
238 _("wof buy <vowel> => buy the vowel <vowel>: the user buying the vowel will lose points equal to the vowel price, and the corresponding vowel will be revealed in the answer (if present)")
240 _("wof: wheel-of-fortune plugin. topics: play, category, clue, answer, replace, cancel, buy")
245 chan = p[:chan] || m.channel
247 m.reply _("you must specify a channel")
250 ch = chan.irc_downcase(m.server.casemap).intern
253 m.reply _("there's already a %{name} game on %{chan}, managed by %{who}") % {
262 name = m.source.get_botdata("wheelfortune.game_name") || @bot.config['wheelfortune.game_name']
264 m.source.set_botdata("wheelfortune.game_name", name.dup)
266 @games[ch] = game = WoFGame.new(name, m.botuser, p[:single], p[:max])
267 @bot.say chan, _("%{who} just created a new %{name} game to %{max} points (%{single} per question, %{price} per vowel)") % {
269 :who => game.manager,
271 :single => game.single,
274 @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>\"") % {
280 ch = p[:chan].irc_downcase(m.server.casemap).intern
282 m.reply _("there's no %{name} game running on %{chan}") % {
283 :name => @bot.config['wheelfortune.game_name'],
290 if m.botuser != game.manager and !m.botuser.permit?('wheelfortune::manage::other::add')
291 m.reply _("you can't add questions to the %{name} game on %{chan}") % {
301 m.reply _("sorry, the answer cannot contain the '*' character")
306 worked, qa = game.start_add_qa(cat, clue)
308 str = ans.empty? ? _("ok, clue added for %{name} round %{count} on %{chan}: %{catclue}") : nil
310 str = _("there's already a pending clue for %{name} round %{count} on %{chan}: %{catclue}")
314 :catclue => qa.catclue,
316 :count => game.length+1
318 return unless worked and !ans.empty?
321 qa = game.finish_add_qa(ans)
323 str = _("ok, QA added for %{name} round %{count} on %{chan}: %{catclue} => %{ans}")
325 str = _("there's no pending clue for %{name} on %{chan}!")
329 :catclue => qa ? qa.catclue : nil,
330 :ans => qa ? qa.answer : nil,
332 :count => game.length
334 announce(m, p.merge({ :next => true }) ) unless game.running?
336 m.reply _("something went wrong, I can't seem to understand what you're trying to set up") if clue.empty?
341 ch = p[:chan].irc_downcase(m.server.casemap).intern
343 m.reply _("there's no %{name} game running on %{chan}") % {
344 :name => @bot.config['wheelfortune.game_name'],
351 if m.botuser != game.manager and !m.botuser.permit?('wheelfortune::manage::other::add')
352 m.reply _("you can't replace questions to the %{name} game on %{chan}") % {
358 round = p[:round].to_i
362 max += 1 if game.pending
363 if round <= min or round > max
365 m.reply _("there are no questions in the %{name} game on %{chan} which can be replaced") % {
370 m.reply _("you can only replace questions between rounds %{min} and %{max} in the %{name} game on %{chan}") % {
383 m.reply _("sorry, the answer cannot contain the '*' character")
388 qa.cat = cat unless cat.empty?
389 qa.clue = clue unless clue.empty?
391 if game.pending and round == max
392 game.finish_add_qa(ans)
398 str = _("ok, replaced QA for %{name} round %{count} on %{chan}: %{catclue} => %{ans}")
401 :catclue => qa ? qa.catclue : nil,
402 :ans => qa ? qa.answer : nil,
408 def announce(m, p={})
409 chan = p[:chan] || m.channel
410 ch = chan.irc_downcase(m.server.casemap).intern
412 m.reply _("there's no %{name} game running on %{chan}") % {
413 :name => @bot.config['wheelfortune.game_name'],
419 qa = p[:next] ? game.next : game.current
421 m.reply _("there are no %{name} questions for %{chan}, I'm waiting for %{who} to add them") % {
429 @bot.say chan, _("%{bold}%{color}%{name}%{bold}, round %{count}:%{nocolor} %{qa}") % {
431 :color => Irc.color(:green),
433 :count => game.round,
434 :nocolor => Irc.color(),
435 :qa => qa.announcement,
440 def score_table(chan, game, opts={})
441 limit = opts[:limit] || -1
442 table = game.score_table[0..limit]
444 @bot.say chan, _("no scores")
447 nick_wd = table.map { |a| a.first.length }.max
448 score_wd = table.first.last.to_s.length
450 @bot.say chan, "%*s : %*u" % [nick_wd, t.first, score_wd, t.last]
454 def react_on_check(m, ch, game, check)
455 debug "check: #{check.inspect}"
459 warning "game #{game}, qa #{game.current} checked nil against #{m.message}"
462 # m.reply "STUPID! YOU SO STUPID!"
465 m.nickreply _("You must buy the %{vowel}") % {
470 when Numeric, :missing
471 # TODO may alter score depening on how many letters were guessed
472 # TODO what happens when the last hint reveals the whole answer?
475 want_more = game.mark_winner(m.source)
476 m.reply _("%{who} got it! The answer was: %{ans}") % {
477 :who => m.sourcenick,
478 :ans => game.current.answer
480 if want_more == :done
482 m.reply _("%{bold}%{color}%{name}%{bold}%{nocolor}: %{who} %{bold}wins%{bold} after %{count} rounds!\nThe final score is") % {
484 :color => Irc.color(:green),
485 :who => m.sourcenick,
487 :count => game.round,
488 :nocolor => Irc.color()
490 score_table(m.channel, game)
493 m.reply _("%{bold}%{color}%{name}%{bold}, round %{count}%{nocolor} -- score so far:") % {
495 :color => Irc.color(:green),
497 :count => game.round,
498 :nocolor => Irc.color()
500 score_table(m.channel, game)
501 announce(m, :next => true)
505 warning "game #{game}, qa #{game.current} checked #{check} against #{m.message}"
511 ch = m.channel.irc_downcase(m.server.casemap).intern
512 return unless game = @games[ch]
513 return unless game.running?
514 check = game.check(m.message, :buy => false)
515 react_on_check(m, ch, game, check)
519 ch = m.channel.irc_downcase(m.server.casemap).intern
522 m.reply _("there's no %{name} game running on %{chan}") % {
523 :name => @bot.config['wheelfortune.game_name'],
528 m.reply _("there are no %{name} questions for %{chan}, I'm waiting for %{who} to add them") % {
536 bought = game.buy(m.source)
538 m.reply _("%{who} buys a %{vowel} for %{price} points") % {
543 check = game.check(vowel, :buy => true)
544 react_on_check(m, ch, game, check)
546 m.reply _("you can't buy a %{vowel}, %{who}: it costs %{price} points and you only have %{score}") % {
549 :price => game.price,
550 :score => game.score(m.source)
557 ch = m.channel.irc_downcase(m.server.casemap).intern
559 m.reply _("there's no %{name} game running on %{chan}") % {
560 :name => @bot.config['wheelfortune.game_name'],
565 # is the botuser the manager or allowed to cancel someone else's game?
566 if m.botuser == @games[ch].manager or m.botuser.permit?('wheelfortune::manage::other::cancel')
569 m.reply _("you can't cancel the current game")
574 game = @games.delete(ch)
576 @bot.say chan, _("%{name} game cancelled after %{count} rounds. Partial score:") % {
580 score_table(chan, game)
584 @games.each_key { |k| do_cancel(k) }
589 plugin = WheelOfFortune.new
591 plugin.map "wof", :action => 'announce', :private => false
592 plugin.map "wof cancel", :action => 'cancel', :private => false
593 plugin.map "wof [:chan] play [*name] for :single [points] to :max [points]", :action => 'setup_game'
594 plugin.map "wof :chan [category: *cat,] clue: *clue[, answer: *ans]", :action => 'setup_qa', :public => false
595 plugin.map "wof :chan answer: *ans", :action => 'setup_qa', :public => false
596 plugin.map "wof :chan replace :round [category: *cat,] clue: *clue[, answer: *ans]", :action => 'replace_qa', :public => false
597 plugin.map "wof :chan replace :round [category: *cat,] answer: *ans", :action => 'replace_qa', :public => false
598 plugin.map "wof :chan replace :round category: *cat[, clue: *clue[, answer: *ans]]", :action => 'replace_qa', :public => false
599 plugin.map "wof buy :vowel", :action => 'buy', :requirements => { :vowel => /./u }