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