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 attr_accessor :guessed
15 def initialize(cat, clue, ans=nil)
17 @clue = clue # clue phrase
28 ret << "(" + cat + ") " unless cat.empty?
39 @answer = ans.dup.downcase
40 @split = @answer.scan(/./u)
41 @hint = @split.inject([]) { |list, ch|
54 ret << _(" [Letters called so far: %{red}%{letters}%{nocolor}]") % {
55 :red => Irc.color(:red),
56 :letters => @used.join(" "),
57 :nocolor => Irc.color()
64 def check(ans_or_letter)
65 d = ans_or_letter.downcase
76 @split.each_with_index { |c, i|
94 # Wheel-of-Fortune game
96 attr_reader :name, :manager, :single, :max, :pending
98 attr_accessor :must_buy, :price
99 def initialize(name, manager, single, max)
102 @single = single.to_i
110 # the default is to make vowels usable only
111 # after paying a price in points which is
112 # a fraction of the single round score equal
113 # to the number of rounds needed to win the game
115 @must_buy = %w{a e i o u y}
116 @price = @single*@single/@max
132 if @pending and round == self.length + 1
141 if @scores.key?(k) and @scores[k][:score] >= @price
142 @scores[k][:score] -= @price
162 def mark_winner(user)
166 @scores[k][:nick] = user.nick
167 @scores[k][:score] += @single
169 @scores[k] = { :nick => user.nick, :score => @single }
171 if @scores[k][:score] >= @max
180 @scores.each { |k, val|
181 table << ["%s (%s)" % [val[:nick], k], val[:score]]
183 table.sort! { |a, b| b.last <=> a.last }
187 return nil unless @curr_idx
192 # don't advance if there are no further QAs
193 return nil if @curr_idx == @qas.length - 1
202 def check(whatever, o={})
204 return nil unless cur
205 if @must_buy.include?(whatever) and not o[:buy]
208 return cur.check(whatever)
211 def start_add_qa(cat, clue)
212 return [nil, @pending] if @pending
213 @pending = WoFQA.new(cat.dup, clue.dup)
214 return [true, @pending]
217 def finish_add_qa(ans)
218 return nil unless @pending
219 @pending.answer = ans.dup
226 class WheelOfFortune < Plugin
227 Config.register Config::StringValue.new('wheelfortune.game_name',
228 :default => 'Wheel Of Fortune',
229 :desc => "default name of the Wheel Of Fortune game")
233 # TODO load/save running games?
237 def help(plugin, topic="")
240 _("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).")
241 when 'category', 'clue', 'answer'
242 _("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")
244 _("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")
246 _("wof cancel => cancels the wheel-of-fortune being played on the current channel")
248 _("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)")
250 _("wof: wheel-of-fortune plugin. topics: play, category, clue, answer, replace, cancel, buy")
255 chan = p[:chan] || m.channel
257 m.reply _("you must specify a channel")
260 ch = chan.irc_downcase(m.server.casemap).intern
263 m.reply _("there's already a %{name} game on %{chan}, managed by %{who}") % {
272 name = m.source.get_botdata("wheelfortune.game_name") || @bot.config['wheelfortune.game_name']
274 m.source.set_botdata("wheelfortune.game_name", name.dup)
276 @games[ch] = game = WoFGame.new(name, m.botuser, p[:single], p[:max])
277 @bot.say chan, _("%{who} just created a new %{name} game to %{max} points (%{single} per question, %{price} per vowel)") % {
279 :who => game.manager,
281 :single => game.single,
284 @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>\"") % {
290 ch = p[:chan].irc_downcase(m.server.casemap).intern
292 m.reply _("there's no %{name} game running on %{chan}") % {
293 :name => @bot.config['wheelfortune.game_name'],
300 if m.botuser != game.manager and !m.botuser.permit?('wheelfortune::manage::other::add')
301 m.reply _("you can't add questions to the %{name} game on %{chan}") % {
311 m.reply _("sorry, the answer cannot contain the '*' character")
316 worked, qa = game.start_add_qa(cat, clue)
318 str = ans.empty? ? _("ok, clue added for %{name} round %{count} on %{chan}: %{catclue}") : nil
320 str = _("there's already a pending clue for %{name} round %{count} on %{chan}: %{catclue}")
324 :catclue => qa.catclue,
326 :count => game.length+1
328 return unless worked and !ans.empty?
331 qa = game.finish_add_qa(ans)
333 str = _("ok, QA added for %{name} round %{count} on %{chan}: %{catclue} => %{ans}")
335 str = _("there's no pending clue for %{name} on %{chan}!")
339 :catclue => qa ? qa.catclue : nil,
340 :ans => qa ? qa.answer : nil,
342 :count => game.length
344 announce(m, p) unless game.running?
346 m.reply _("something went wrong, I can't seem to understand what you're trying to set up") if clue.empty?
351 ch = p[:chan].irc_downcase(m.server.casemap).intern
353 m.reply _("there's no %{name} game running on %{chan}") % {
354 :name => @bot.config['wheelfortune.game_name'],
361 if m.botuser != game.manager and !m.botuser.permit?('wheelfortune::manage::other::add')
362 m.reply _("you can't replace questions to the %{name} game on %{chan}") % {
368 round = p[:round].to_i
372 max += 1 if game.pending
373 if round <= min or round > max
375 m.reply _("there are no questions in the %{name} game on %{chan} which can be replaced") % {
380 m.reply _("you can only replace questions between rounds %{min} and %{max} in the %{name} game on %{chan}") % {
394 m.reply _("sorry, the answer cannot contain the '*' character")
399 qa.cat = cat unless cat.empty?
400 qa.clue = clue unless clue.empty?
402 if game.pending and round == max
403 game.finish_add_qa(ans)
409 str = _("ok, replaced QA for %{name} round %{count} on %{chan}: %{catclue} => %{ans}")
412 :catclue => qa ? qa.catclue : nil,
413 :ans => qa ? qa.answer : nil,
419 def announce(m, p={})
420 chan = p[:chan] || m.channel
421 ch = chan.irc_downcase(m.server.casemap).intern
423 m.reply _("there's no %{name} game running on %{chan}") % {
424 :name => @bot.config['wheelfortune.game_name'],
431 if !qa or qa.guessed?
435 m.reply _("there are no %{name} questions for %{chan}, I'm waiting for %{who} to add them") % {
443 @bot.say chan, _("%{bold}%{color}%{name}%{bold}, round %{count}:%{nocolor} %{qa}") % {
445 :color => Irc.color(:green),
447 :count => game.round,
448 :nocolor => Irc.color(),
449 :qa => qa.announcement,
454 def score_table(chan, game, opts={})
455 limit = opts[:limit] || -1
456 table = game.score_table[0..limit]
458 @bot.say chan, _("no scores")
461 nick_wd = table.map { |a| a.first.length }.max
462 score_wd = table.first.last.to_s.length
464 @bot.say chan, "%*s : %*u" % [nick_wd, t.first, score_wd, t.last]
468 def react_on_check(m, ch, game, check)
469 debug "check: #{check.inspect}"
473 warning "game #{game}, qa #{game.current} checked nil against #{m.message}"
476 # m.reply "STUPID! YOU SO STUPID!"
479 m.nickreply _("You must buy the %{vowel}") % {
484 when Numeric, :missing
485 # TODO may alter score depening on how many letters were guessed
486 # TODO what happens when the last hint reveals the whole answer?
489 game.mark_guessed(game.current)
490 want_more = game.mark_winner(m.source)
491 m.reply _("%{who} got it! The answer was: %{ans}") % {
492 :who => m.sourcenick,
493 :ans => game.current.answer
495 if want_more == :done
497 m.reply _("%{bold}%{color}%{name}%{bold}%{nocolor}: %{who} %{bold}wins%{bold} after %{count} rounds!\nThe final score is") % {
499 :color => Irc.color(:green),
500 :who => m.sourcenick,
502 :count => game.round,
503 :nocolor => Irc.color()
505 score_table(m.channel, game)
508 m.reply _("%{bold}%{color}%{name}%{bold}, round %{count}%{nocolor} -- score so far:") % {
510 :color => Irc.color(:green),
512 :count => game.round,
513 :nocolor => Irc.color()
515 score_table(m.channel, game)
520 warning "game #{game}, qa #{game.current} checked #{check} against #{m.message}"
526 ch = m.channel.irc_downcase(m.server.casemap).intern
527 return unless game = @games[ch]
528 return unless game.running?
529 return unless game.current and not game.current.guessed?
530 check = game.check(m.message, :buy => false)
531 react_on_check(m, ch, game, check)
535 ch = m.channel.irc_downcase(m.server.casemap).intern
538 m.reply _("there's no %{name} game running on %{chan}") % {
539 :name => @bot.config['wheelfortune.game_name'],
544 m.reply _("there are no %{name} questions for %{chan}, I'm waiting for %{who} to add them") % {
552 bought = game.buy(m.source)
554 m.reply _("%{who} buys a %{vowel} for %{price} points") % {
559 check = game.check(vowel, :buy => true)
560 react_on_check(m, ch, game, check)
562 m.reply _("you can't buy a %{vowel}, %{who}: it costs %{price} points and you only have %{score}") % {
565 :price => game.price,
566 :score => game.score(m.source)
573 ch = m.channel.irc_downcase(m.server.casemap).intern
575 m.reply _("there's no %{name} game running on %{chan}") % {
576 :name => @bot.config['wheelfortune.game_name'],
581 # is the botuser the manager or allowed to cancel someone else's game?
582 if m.botuser == @games[ch].manager or m.botuser.permit?('wheelfortune::manage::other::cancel')
585 m.reply _("you can't cancel the current game")
590 game = @games.delete(ch)
592 @bot.say chan, _("%{name} game cancelled after %{count} rounds. Partial score:") % {
596 score_table(chan, game)
600 @games.each_key { |k| do_cancel(k) }
605 plugin = WheelOfFortune.new
607 plugin.map "wof", :action => 'announce', :private => false
608 plugin.map "wof cancel", :action => 'cancel', :private => false
609 plugin.map "wof [:chan] play [*name] for :single [points] to :max [points]", :action => 'setup_game'
610 plugin.map "wof :chan [category: *cat,] clue: *clue[, answer: *ans]", :action => 'setup_qa', :public => false
611 plugin.map "wof :chan answer: *ans", :action => 'setup_qa', :public => false
612 plugin.map "wof :chan replace :round [category: *cat,] clue: *clue[, answer: *ans]", :action => 'replace_qa', :public => false
613 plugin.map "wof :chan replace :round [category: *cat,] answer: *ans", :action => 'replace_qa', :public => false
614 plugin.map "wof :chan replace :round category: *cat[, clue: *clue[, answer: *ans]]", :action => 'replace_qa', :public => false
615 plugin.map "wof buy :vowel", :action => 'buy', :requirements => { :vowel => /./u }