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, :answer, :hint
13 def initialize(cat, clue, ans=nil)
15 @clue = clue # clue phrase
21 ret << "(" + cat + ") " unless cat.empty?
32 @answer = ans.dup.downcase
33 @split = @answer.scan(/./u)
34 @hint = @split.inject([]) { |list, ch|
47 ret << _(" [Letters called so far: %{red}%{letters}%{nocolor}]") % {
48 :red => Irc.color(:red),
49 :letters => @used.join(" "),
50 :nocolor => Irc.color()
57 def check(ans_or_letter)
58 d = ans_or_letter.downcase
69 @split.each_with_index { |c, i|
87 # Wheel-of-Fortune game
89 attr_reader :name, :manager, :single, :max, :pending
91 attr_accessor :must_buy, :price
92 def initialize(name, manager, single, max)
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
108 @must_buy = %w{a e i o u y}
109 @price = @single*@single/@max
125 if @pending and round == self.length + 1
134 if @scores.key?(k) and @scores[k][:score] >= @price
135 @scores[k][:score] -= @price
151 def mark_winner(user)
155 @scores[k][:nick] = user.nick
156 @scores[k][:score] += @single
158 @scores[k] = { :nick => user.nick, :score => @single }
160 if @scores[k][:score] >= @max
169 @scores.each { |k, val|
170 table << ["%s (%s)" % [val[:nick], k], val[:score]]
172 table.sort! { |a, b| b.last <=> a.last }
176 return nil unless @curr_idx
181 # don't advance if there are no further QAs
182 return nil if @curr_idx == @qas.length - 1
191 def check(whatever, o={})
193 return nil unless cur
194 if @must_buy.include?(whatever) and not o[:buy]
197 return cur.check(whatever)
200 def start_add_qa(cat, clue)
201 return [nil, @pending] if @pending
202 @pending = WoFQA.new(cat.dup, clue.dup)
203 return [true, @pending]
206 def finish_add_qa(ans)
207 return nil unless @pending
208 @pending.answer = ans.dup
215 class WheelOfFortune < Plugin
216 Config.register Config::StringValue.new('wheelfortune.game_name',
217 :default => 'Wheel Of Fortune',
218 :desc => "default name of the Wheel Of Fortune game")
222 # TODO load/save running games?
227 chan = p[:chan] || m.channel
229 m.reply _("you must specify a channel")
232 ch = chan.irc_downcase(m.server.casemap).intern
235 m.reply _("there's already a %{name} game on %{chan}, managed by %{who}") % {
244 name = m.source.get_botdata("wheelfortune.game_name") || @bot.config['wheelfortune.game_name']
246 m.source.set_botdata("wheelfortune.game_name", name.dup)
248 @games[ch] = game = WoFGame.new(name, m.botuser, p[:single], p[:max])
249 @bot.say chan, _("%{who} just created a new %{name} game to %{max} points (%{single} per question, %{price} per vowel)") % {
251 :who => game.manager,
253 :single => game.single,
256 @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>\"") % {
262 ch = p[:chan].irc_downcase(m.server.casemap).intern
264 m.reply _("there's no %{name} game running on %{chan}") % {
265 :name => @bot.config['wheelfortune.game_name'],
272 if m.botuser != game.manager and !m.botuser.permit?('wheelfortune::manage::other::add')
273 m.reply _("you can't add questions to the %{name} game on %{chan}") % {
283 m.reply _("sorry, the answer cannot contain the '*' character")
288 worked, qa = game.start_add_qa(cat, clue)
290 str = ans.empty? ? _("ok, clue added for %{name} round %{count} on %{chan}: %{catclue}") : nil
292 str = _("there's already a pending clue for %{name} round %{count} on %{chan}: %{catclue}")
296 :catclue => qa.catclue,
298 :count => game.length+1
300 return unless worked and !ans.empty?
303 qa = game.finish_add_qa(ans)
305 str = _("ok, QA added for %{name} round %{count} on %{chan}: %{catclue} => %{ans}")
307 str = _("there's no pending clue for %{name} on %{chan}!")
311 :catclue => qa ? qa.catclue : nil,
312 :ans => qa ? qa.answer : nil,
314 :count => game.length
316 announce(m, p.merge({ :next => true }) ) unless game.running?
318 m.reply _("something went wrong, I can't seem to understand what you're trying to set up") if clue.empty?
323 ch = p[:chan].irc_downcase(m.server.casemap).intern
325 m.reply _("there's no %{name} game running on %{chan}") % {
326 :name => @bot.config['wheelfortune.game_name'],
333 if m.botuser != game.manager and !m.botuser.permit?('wheelfortune::manage::other::add')
334 m.reply _("you can't replace questions to the %{name} game on %{chan}") % {
340 round = p[:round].to_i
344 max += 1 if game.pending
345 if round <= min or round > max
347 m.reply _("there are no questions in the %{name} game on %{chan} which can be replaced") % {
352 m.reply _("you can only replace questions between rounds %{min} and %{max} in the %{name} game on %{chan}") % {
365 m.reply _("sorry, the answer cannot contain the '*' character")
370 qa.cat = cat unless cat.empty?
371 qa.clue = clue unless clue.empty?
373 if game.pending and round == max
374 game.finish_add_qa(ans)
380 str = _("ok, replaced QA for %{name} round %{count} on %{chan}: %{catclue} => %{ans}")
383 :catclue => qa ? qa.catclue : nil,
384 :ans => qa ? qa.answer : nil,
390 def announce(m, p={})
391 chan = p[:chan] || m.channel
392 ch = chan.irc_downcase(m.server.casemap).intern
394 m.reply _("there's no %{name} game running on %{chan}") % {
395 :name => @bot.config['wheelfortune.game_name'],
401 qa = p[:next] ? game.next : game.current
403 m.reply _("there are no %{name} questions for %{chan}, I'm waiting for %{who} to add them") % {
411 @bot.say chan, _("%{bold}%{color}%{name}%{bold}, round %{count}:%{nocolor} %{qa}") % {
413 :color => Irc.color(:green),
415 :count => game.round,
416 :nocolor => Irc.color(),
417 :qa => qa.announcement,
422 def score_table(chan, game, opts={})
423 limit = opts[:limit] || -1
424 table = game.score_table[0..limit]
426 @bot.say chan, _("no scores")
429 nick_wd = table.map { |a| a.first.length }.max
430 score_wd = table.first.last.to_s.length
432 @bot.say chan, "%*s : %*u" % [nick_wd, t.first, score_wd, t.last]
436 def react_on_check(m, ch, game, check)
437 debug "check: #{check.inspect}"
441 warning "game #{game}, qa #{game.current} checked nil against #{m.message}"
444 # m.reply "STUPID! YOU SO STUPID!"
447 m.nickreply _("You must buy the %{vowel}") % {
452 when Numeric, :missing
453 # TODO may alter score depening on how many letters were guessed
454 # TODO what happens when the last hint reveals the whole answer?
457 want_more = game.mark_winner(m.source)
458 m.reply _("%{who} got it! The answer was: %{ans}") % {
459 :who => m.sourcenick,
460 :ans => game.current.answer
462 if want_more == :done
464 m.reply _("%{bold}%{color}%{name}%{bold}%{nocolor}: %{who} %{bold}wins%{bold} after %{count} rounds!\nThe final score is") % {
466 :color => Irc.color(:green),
467 :who => m.sourcenick,
469 :count => game.round,
470 :nocolor => Irc.color()
472 score_table(m.channel, game)
475 m.reply _("%{bold}%{color}%{name}%{bold}, round %{count}%{nocolor} -- score so far:") % {
477 :color => Irc.color(:green),
479 :count => game.round,
480 :nocolor => Irc.color()
482 score_table(m.channel, game)
483 announce(m, :next => true)
487 warning "game #{game}, qa #{game.current} checked #{check} against #{m.message}"
492 return unless m.kind_of?(PrivMessage) and not m.address?
493 ch = m.channel.irc_downcase(m.server.casemap).intern
494 return unless game = @games[ch]
495 return unless game.running?
496 check = game.check(m.message, :buy => false)
497 react_on_check(m, ch, game, check)
501 ch = m.channel.irc_downcase(m.server.casemap).intern
504 m.reply _("there's no %{name} game running on %{chan}") % {
505 :name => @bot.config['wheelfortune.game_name'],
510 m.reply _("there are no %{name} questions for %{chan}, I'm waiting for %{who} to add them") % {
518 bought = game.buy(m.source)
520 m.reply _("%{who} buys a %{vowel} for %{price} points") % {
525 check = game.check(vowel, :buy => true)
526 react_on_check(m, ch, game, check)
528 m.reply _("you can't buy a %{vowel}, %{who}: it costs %{price} points and you only have %{score}") % {
531 :price => game.price,
532 :score => game.score(m.source)
539 ch = m.channel.irc_downcase(m.server.casemap).intern
541 m.reply _("there's no %{name} game running on %{chan}") % {
542 :name => @bot.config['wheelfortune.game_name'],
547 # is the botuser the manager or allowed to cancel someone else's game?
548 if m.botuser == game.manager or m.botuser.permit?('wheelfortune::manage::other::cancel')
551 m.reply _("you can't cancel the current game")
556 game = @games.delete(ch)
558 @bot.say chan, _("%{name} game cancelled after %{count} rounds. Partial score:") % {
562 score_table(chan, game)
566 @games.each_key { |k| do_cancel(k) }
571 plugin = WheelOfFortune.new
573 plugin.map "wof", :action => 'announce', :private => false
574 plugin.map "wof cancel", :action => 'cancel', :private => false
575 plugin.map "wof [:chan] play [*name] for :single [points] to :max [points]", :action => 'setup_game'
576 plugin.map "wof :chan [category: *cat,] clue: *clue[, answer: *ans]", :action => 'setup_qa', :public => false
577 plugin.map "wof :chan answer: *ans", :action => 'setup_qa', :public => false
578 plugin.map "wof :chan replace :round [category: *cat,] clue: *clue[, answer: *ans]", :action => 'replace_qa', :public => false
579 plugin.map "wof :chan replace :round [category: *cat,] answer: *ans", :action => 'replace_qa', :public => false
580 plugin.map "wof :chan replace :round category: *cat[, clue: *clue[, answer: *ans]]", :action => 'replace_qa', :public => false
581 plugin.map "wof buy :vowel", :action => 'buy', :requirements => { :vowel => /./u }