]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/games/wheelfortune.rb
wheelfortune: say when there are no scores
[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   def initialize(name, manager, single, max)
85     @name = name.dup
86     @manager = manager
87     @single = single.to_i
88     @max = max.to_i
89     @pending = nil
90     @qas = []
91     @curr_idx = nil
92     @running = false
93     @scores = Hash.new
94   end
95
96   def running?
97     @running
98   end
99
100   def round
101     @curr_idx+1 rescue 0
102   end
103
104   def mark_winner(user)
105     @running = false
106     k = user.botuser
107     if @scores.key?(k)
108       @scores[k][:nick] = user.nick
109       @scores[k][:score] += @single
110     else
111       @scores[k] = { :nick => user.nick, :score => @single }
112     end
113     if @scores[k][:score] >= @max
114       return :done
115     else
116       return :more
117     end
118   end
119
120   def score_table
121     table = []
122     @scores.each { |k, val|
123       table << ["%s (%s)" % [val[:nick], k], val[:score]]
124     }
125     table.sort! { |a, b| b.last <=> a.last }
126   end
127
128   def current
129     return nil unless @curr_idx
130     @qas[@curr_idx]
131   end
132
133   def next
134     # don't advance if there are no further QAs
135     return nil if @curr_idx == @qas.length - 1
136     if @curr_idx
137       @curr_idx += 1
138     else
139       @curr_idx = 0
140     end
141     return current
142   end
143
144   def check(whatever)
145     cur = self.current
146     return nil unless cur
147     return cur.check(whatever)
148   end
149
150   def start_add_qa(cat, clue)
151     return [nil, @pending] if @pending
152     @pending = WoFQA.new(cat.dup, clue.dup)
153     return [true, @pending]
154   end
155
156   def finish_add_qa(ans)
157     return nil unless @pending
158     @pending.answer = ans.dup
159     @qas << @pending
160     @pending = nil
161     return @qas.last
162   end
163 end
164
165 class WheelOfFortune < Plugin
166   Config.register Config::StringValue.new('wheelfortune.game_name',
167     :default => 'Wheel Of Fortune',
168     :desc => "default name of the Wheel Of Fortune game")
169
170   def initialize
171     super
172     # TODO load/save running games?
173     @games = Hash.new
174   end
175
176   def setup_game(m, p)
177     chan = p[:chan] || m.channel
178     if !chan
179       m.reply _("you must specify a channel")
180       return
181     end
182     ch = chan.irc_downcase(m.server.casemap).intern
183
184     if game = @games[ch]
185       m.reply _("there's already a %{name} game on %{chan}, managed by %{who}") % {
186         :name => game.name,
187         :chan => chan,
188         :who => game.manager
189       }
190       return
191     end
192     name = p[:name].to_s
193     name = @bot.config['wheelfortune.game_name'] if name.empty?
194     @games[ch] = game = WoFGame.new(name, m.botuser, p[:single], p[:max])
195     @bot.say chan, _("%{who} just created a new %{name} game to %{max} points (%{single} per question)") % {
196       :name => game.name,
197       :who => game.manager,
198       :max => game.max,
199       :single => game.single
200     }
201     @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>\"") % {
202       :chan => chan
203     }
204   end
205
206   def setup_qa(m, p)
207     ch = p[:chan].irc_downcase(m.server.casemap).intern
208     if !@games.key?(ch)
209       m.reply _("there's no %{name} game running on %{chan}") % {
210         :name => @bot.config['wheelfortune.game_name'],
211         :chan => p[:chan]
212       }
213       return
214     end
215     game = @games[ch]
216     cat = p[:cat].to_s
217     clue = p[:clue].to_s
218     ans = p[:ans].to_s
219     if ans.include?('*')
220       m.reply _("sorry, the answer cannot contain the '*' character")
221       return
222     end
223
224     if !clue.empty?
225       worked, qa = game.start_add_qa(cat, clue)
226       if worked
227         str = ans.empty? ?  _("ok, new clue added for %{chan}: %{catclue}") : nil
228       else
229         str = _("there's already a pending clue for %{chan}: %{catclue}")
230       end
231       m.reply _(str) % { :chan => p[:chan], :catclue => qa.catclue } if str
232       return unless worked or !ans.empty?
233     end
234     if !ans.empty?
235       qa = game.finish_add_qa(ans)
236       if qa
237         str = _("ok, new QA added for %{chan}: %{catclue} => %{ans}")
238       else
239         str = _("there's no pending clue for %{chan}!")
240       end
241       m.reply _(str) % { :chan => p[:chan], :catclue => qa ? qa.catclue : nil, :ans => qa ? qa.answer : nil}
242       announce(m, p.merge({ :next => true }) ) unless game.running?
243     else
244       m.reply _("something went wrong, I can't seem to understand what you're trying to set up")
245     end
246   end
247
248   def announce(m, p={})
249     chan = p[:chan] || m.channel
250     ch = chan.irc_downcase(m.server.casemap).intern
251     if !@games.key?(ch)
252       m.reply _("there's no %{name} game running on %{chan}") % {
253         :name => @bot.config['wheelfortune.game_name'],
254         :chan => chan
255       }
256       return
257     end
258     game = @games[ch]
259     qa = p[:next] ? game.next : game.current
260     if !qa
261       m.reply _("there are no %{name} questions for %{chan}, I'm waiting for %{who} to add them") % {
262         :name => game.name,
263         :chan => chan,
264         :who => game.manager
265       }
266       return
267     end
268
269     @bot.say chan, qa.announcement
270     game.running = true
271   end
272
273   def score_table(chan, game, opts={})
274     limit = opts[:limit] || -1
275     table = game.score_table[0..limit]
276     if table.length == 0
277       @bot.say chan, _("no scores")
278       return
279     end
280     nick_wd = table.map { |a| a.first.length }.max
281     score_wd = table.first.last.to_s.length
282     table.each { |t|
283       @bot.say chan, "%*s : %*u" % [nick_wd, t.first, score_wd, t.last]
284     }
285   end
286
287   def listen(m)
288     return unless m.kind_of?(PrivMessage) and not m.address?
289     ch = m.channel.irc_downcase(m.server.casemap).intern
290     return unless game = @games[ch]
291     return unless game.running?
292     check = game.check(m.message)
293     debug "check: #{check.inspect}"
294     case check
295     when nil
296       # can this happen?
297       warning "game #{game}, qa #{game.current} checked nil against #{m.message}"
298       return
299     when :used
300       # m.reply "STUPID! YOU SO STUPID!"
301       return
302     when :wrong
303       return
304     when Numeric, :missing
305       # TODO may alter score depening on how many letters were guessed
306       # TODO what happens when the last hint reveals the whole answer?
307       announce(m)
308     when :gotit
309       want_more = game.mark_winner(m.source)
310       m.reply _("%{who} got it! The answer was: %{ans}") % {
311         :who => m.sourcenick,
312         :ans => game.current.answer
313       }
314       if want_more == :done
315         # max score reached
316         m.reply _("%{who} wins the game after %{count} rounds!") % {
317           :who => m.sourcenick,
318           :count => game.round
319         }
320         score_table(m.channel, game)
321         @games.delete(ch)
322       else :more
323         score_table(m.channel, game)
324         announce(m, :next => true)
325       end
326     else
327       # can this happen?
328       warning "game #{game}, qa #{game.current} checked #{check} against #{m.message}"
329     end
330   end
331
332   def cancel(m, p)
333     ch = m.channel.irc_downcase(m.server.casemap).intern
334     if !@games.key?(ch)
335       m.reply _("there's no %{name} game running on %{chan}") % {
336         :name => @bot.config['wheelfortune.game_name'],
337         :chan => m.channel
338       }
339       return
340     end
341     do_cancel(ch)
342   end
343
344   def do_cancel(ch)
345     game = @games.delete(ch)
346     chan = ch.to_s
347     @bot.say chan, _("%{name} game cancelled after %{count} rounds. Partial score:") % {
348       :name => game.name,
349       :count => game.round
350     }
351     score_table(chan, game)
352   end
353
354   def cleanup
355     @games.each_key { |k| do_cancel(k) }
356     super
357   end
358 end
359
360 plugin = WheelOfFortune.new
361
362 plugin.map "wof", :action => 'announce', :private => false
363 plugin.map "wof cancel", :action => 'cancel', :private => false
364 plugin.map "wof [:chan] play [*name] for :single [points] to :max [points]", :action => 'setup_game'
365 plugin.map "wof :chan [category: *cat,] clue: *clue[, answer: *ans]", :action => 'setup_qa', :public => false
366 plugin.map "wof :chan answer: *ans", :action => 'setup_qa', :public => false