]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/azgame.rb
f2da4fa2a6a28dec6d6275c2aa89a3e914feb3df
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / azgame.rb
1 # vim: set et sw=2:\r
2 # A-Z Game: guess the word by reducing the interval of allowed ones\r
3 #\r
4 # Author: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>\r
5 #\r
6 # (C) 2006 Giuseppe Bilotta\r
7 #\r
8 # TODO allow manual addition of words\r
9 \r
10 AZ_RULES = {\r
11   :italian => {\r
12     :good => /s\.f\.|s\.m\.|agg\.|v\.tr\.|v\.(pronom\.)?intr\./, # avv\.|pron\.|cong\.\r
13     :bad => /var\./,\r
14     :first => 'abaco',\r
15     :last => 'zuzzurellone',\r
16     :url => "http://www.demauroparavia.it/%s",\r
17     :wapurl => "http://wap.demauroparavia.it/index.php?lemma=%s"\r
18   },\r
19   :english => {\r
20     :good => /(?:singular )?noun|verb|adj/,\r
21     :first => 'abacus',\r
22     :last => 'zuni',\r
23     :url => "http://www.chambersharrap.co.uk/chambers/features/chref/chref.py/main?query=%s&title=21st"\r
24   }\r
25 }\r
26 \r
27 class AzGame\r
28 \r
29   attr_reader :range, :word\r
30   def initialize(plugin, lang, word)\r
31     @plugin = plugin\r
32     @lang = lang.to_sym\r
33     @word = word.downcase\r
34     @range = [AZ_RULES[lang][:first], AZ_RULES[lang][:last]]\r
35     def @range.to_s\r
36       return "%s -- %s" % self\r
37     end\r
38   end\r
39 \r
40   def check(word)\r
41     w = word.downcase\r
42     debug "checking #{w} for #{@word} in #{@range}"\r
43     return [:bingo, nil] if w == @word\r
44     return [:out, @range] if w < @range.first or w > @range.last\r
45     return [:ignore, @range] if w == @range.first or w == @range.last\r
46     return [:noexist, @range] unless @plugin.send("is_#{@lang}?", w)\r
47     debug "we like it"\r
48     if w < @word\r
49       @range.first.replace(w)\r
50     else\r
51       @range.last.replace(w)\r
52     end\r
53     return [:in, @range]\r
54   end\r
55 \r
56 end\r
57 \r
58 class AzGamePlugin < Plugin\r
59 \r
60   def initialize\r
61     super\r
62     # if @registry.has_key?(:games)\r
63     #   @games = @registry[:games]\r
64     # else\r
65       @games = Hash.new\r
66     # end\r
67     if @registry.has_key?(:wordcache) and @registry[:wordcache]\r
68       @wordcache = @registry[:wordcache]\r
69     else\r
70       @wordcache = Hash.new\r
71     end\r
72     debug "\n\n\nA-Z wordcache: #{@wordcache.inspect}\n\n\n"\r
73   end\r
74 \r
75   def save\r
76     # @registry[:games] = @games\r
77     @registry[:wordcache] = @wordcache\r
78   end\r
79 \r
80   def listen(m)\r
81     return unless m.kind_of?(PrivMessage)\r
82     return if m.channel.nil? or m.address?\r
83     k = m.channel.to_s # to_sym?\r
84     return unless @games.key?(k)\r
85     return if m.params\r
86     word = m.plugin.downcase\r
87     return unless word =~ /^[a-z]+$/\r
88     word_check(m, k, word)\r
89   end\r
90 \r
91   def word_check(m, k, word)\r
92     isit = @games[k].check(word)\r
93     case isit.first\r
94     when :bingo\r
95       m.reply "#{Bold}BINGO!#{Bold}: the word was #{Underline}#{word}#{Underline}. Congrats, #{Bold}#{m.sourcenick}#{Bold}!"\r
96       @games.delete(k)\r
97     when :out\r
98       m.reply "#{word} is not in the range #{Bold}#{isit.last}#{Bold}" if m.address?\r
99     when :noexist\r
100       m.reply "#{word} doesn't exist or is not acceptable for the game"\r
101     when :in\r
102       m.reply "close, but no cigar. New range: #{Bold}#{isit.last}#{Bold}"\r
103     when :ignore\r
104       m.reply "#{word} is already one of the range extrema: #{isit.last}" if m.address?\r
105     else\r
106       m.reply "hm, something went wrong while verifying #{word}"\r
107     end\r
108   end\r
109 \r
110   def manual_word_check(m, params)\r
111     k = m.channel.to_s\r
112     word = params[:word].downcase\r
113     if not @games.key?(k)\r
114       m.reply "no A-Z game running here, can't check for #{word}, can I?"\r
115       return\r
116     end\r
117     if word !~ /^[a-z]+$/\r
118       m.reply "I only accept single words composed by letters only, sorry"\r
119       return\r
120     end\r
121     word_check(m, k, word)\r
122   end\r
123 \r
124   def stop_game(m, params)\r
125     return if m.channel.nil? # Shouldn't happen, but you never know\r
126     k = m.channel.to_s # to_sym?\r
127     if @games.key?(k)\r
128       m.reply "the word in #{Bold}#{@games[k].range}#{Bold} was:   #{Bold}#{@games[k].word}"\r
129       @games.delete(k)\r
130     else\r
131       m.reply "no A-Z game running in this channel ..."\r
132     end\r
133   end\r
134 \r
135   def start_game(m, params)\r
136     return if m.channel.nil? # Shouldn't happen, but you never know\r
137     k = m.channel.to_s # to_sym?\r
138     unless @games.key?(k)\r
139       lang = (params[:lang] || @bot.config['core.language']).to_sym\r
140       method = 'random_pick_'+lang.to_s\r
141       m.reply "let me think ..."\r
142       if AZ_RULES.has_key?(lang) and self.respond_to?(method)\r
143         word = self.send(method)\r
144         if word.empty?\r
145           m.reply "couldn't think of anything ..."\r
146           return\r
147         end\r
148       else\r
149         m.reply "I can't play A-Z in #{lang}, sorry"\r
150         return\r
151       end\r
152       m.reply "got it!"\r
153       @games[k] = AzGame.new(self, lang, word)\r
154     end\r
155     m.reply "A-Z: #{Bold}#{@games[k].range}#{Bold}"\r
156     return\r
157   end\r
158 \r
159   def wordlist(m, params)\r
160     pars = params[:params]\r
161     lang = (params[:lang] || @bot.config['core.language']).to_sym\r
162     wc = @wordcache[lang] || Hash.new rescue Hash.new\r
163     cmd = params[:cmd].to_sym rescue :count\r
164     case cmd\r
165     when :count\r
166       m.reply "I have #{wc.size > 0 ? wc.size : 'no'} #{lang} words in my cache"\r
167     when :show, :list\r
168       if pars.empty?\r
169         m.reply "provide a regexp to match"\r
170         return\r
171       end\r
172       begin\r
173         regex = /#{pars[0]}/\r
174         matches = wc.keys.map { |k|\r
175           k.to_s\r
176         }.grep(regex)\r
177       rescue\r
178         matches = []\r
179       end\r
180       if matches.size == 0\r
181         m.reply "no #{lang} word I know match #{pars[0]}"\r
182       elsif matches.size > 25\r
183         m.reply "more than 25 #{lang} words I know match #{pars[0]}, try a stricter matching"\r
184       else\r
185         m.reply "#{matches.join(', ')}"\r
186       end\r
187     when :info\r
188       if pars.empty?\r
189         m.reply "provide a word"\r
190         return\r
191       end\r
192       word = pars[0].downcase.to_sym\r
193       if not wc.key?(word)\r
194         m.reply "I don't know any #{lang} word #{word}"\r
195         return\r
196       end\r
197       tr = "#{word} learned from #{wc[word][:who]}"\r
198       (tr << " on #{wc[word][:when]}") if wc[word].key?(:when)\r
199       m.reply tr\r
200     when :delete\r
201       if pars.empty?\r
202         m.reply "provide a word"\r
203         return\r
204       end\r
205       word = pars[0].downcase.to_sym\r
206       if not wc.key?(word)\r
207         m.reply "I don't know any #{lang} word #{word}"\r
208         return\r
209       end\r
210       wc.delete(word)\r
211       @bot.okay m.replyto\r
212     when :add\r
213       if pars.empty?\r
214         m.reply "provide a word"\r
215         return\r
216       end\r
217       word = pars[0].downcase.to_sym\r
218       if wc.key?(word)\r
219         m.reply "I already know the #{lang} word #{word}"\r
220         return\r
221       end\r
222       wc[word] = { :who => m.sourcenick, :when => Time.now }\r
223       @bot.okay m.replyto\r
224     else\r
225     end\r
226   end\r
227 \r
228   def is_italian?(word)\r
229     unless @wordcache.key?(:italian)\r
230       @wordcache[:italian] = Hash.new\r
231     end\r
232     wc = @wordcache[:italian]\r
233     return true if wc.key?(word.to_sym)\r
234     rules = AZ_RULES[:italian]\r
235     p = @bot.httputil.get_cached(rules[:wapurl] % word)\r
236     if not p\r
237       error "could not connect!"\r
238       return false\r
239     end\r
240     debug p\r
241     p.scan(/<anchor>#{word} - (.*?)<go href="lemma.php\?ID=([^"]*?)"/) { |qual, url|\r
242       debug "new word #{word} of type #{qual}"\r
243       if qual =~ rules[:good] and qual !~ rules[:bad]\r
244         wc[word.to_sym] = {:who => :dict}\r
245         return true\r
246       end\r
247       next\r
248     }\r
249     return false\r
250   end\r
251 \r
252   def random_pick_italian(min=nil,max=nil)\r
253     # Try to pick a random word between min and max\r
254     word = String.new\r
255     min = min.to_s\r
256     max = max.to_s\r
257     if min > max\r
258       m.reply "#{min} > #{max}"\r
259       return word\r
260     end\r
261     rules = AZ_RULES[:italian]\r
262     min = rules[:first] if min.empty?\r
263     max = rules[:last]  if max.empty?\r
264     debug "looking for word between #{min.inspect} and #{max.inspect}"\r
265     return word if min.empty? or max.empty?\r
266     begin\r
267       while (word <= min or word >= max or word !~ /^[a-z]+$/)\r
268         debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"\r
269         # TODO for the time being, skip words with extended characters\r
270         unless @wordcache.key?(:italian)\r
271           @wordcache[:italian] = Hash.new\r
272         end\r
273         wc = @wordcache[:italian]\r
274 \r
275         if wc.size > 0\r
276           cache_or_url = rand(2)\r
277           if cache_or_url == 0\r
278             debug "getting word from wordcache"\r
279             word = wc.keys[rand(wc.size)].to_s\r
280             next\r
281           end\r
282         end\r
283 \r
284         # TODO when doing ranges, adapt this choice\r
285         l = ('a'..'z').to_a[rand(26)]\r
286         debug "getting random word from dictionary, starting with letter #{l}"\r
287         first = rules[:url] % "lettera_#{l}_0_50"\r
288         p = @bot.httputil.get_cached(first)\r
289         max_page = p.match(/ \/ (\d+)<\/label>/)[1].to_i\r
290         pp = rand(max_page)+1\r
291         debug "getting random word from dictionary, starting with letter #{l}, page #{pp}"\r
292         p = @bot.httputil.get_cached(first+"&pagina=#{pp}") if pp > 1\r
293         lemmi = Array.new\r
294         good = rules[:good]\r
295         bad =  rules[:bad]\r
296         # We look for a lemma composed by a single word and of length at least two\r
297         p.scan(/<li><a href="([^"]+?)" title="consulta il lemma ([^ "][^ "]+?)">.*?&nbsp;(.+?)<\/li>/) { |url, prelemma, tipo|\r
298           lemma = prelemma.downcase.to_sym\r
299           debug "checking lemma #{lemma} (#{prelemma}) of type #{tipo} from url #{url}"\r
300           next if wc.key?(lemma)\r
301           case tipo\r
302           when good\r
303             if tipo =~ bad\r
304               debug "refusing, #{bad}"\r
305               next\r
306             end\r
307             debug "good one"\r
308             lemmi << lemma\r
309             wc[lemma] = {:who => :dict}\r
310           else\r
311             debug "refusing, not #{good}"\r
312           end\r
313         }\r
314         word = lemmi[rand(lemmi.length)].to_s\r
315       end\r
316     rescue => e\r
317       error "error #{e.inspect} while looking up a word"\r
318       error e.backtrace.join("\n")\r
319     end\r
320     return word\r
321   end\r
322 \r
323   def is_english?(word)\r
324     unless @wordcache.key?(:english)\r
325       @wordcache[:english] = Hash.new\r
326     end\r
327     wc = @wordcache[:english]\r
328     return true if wc.key?(word.to_sym)\r
329     rules = AZ_RULES[:english]\r
330     p = @bot.httputil.get_cached(rules[:url] % URI.escape(word))\r
331     if not p\r
332       error "could not connect!"\r
333       return false\r
334     end\r
335     debug p\r
336     if p =~ /<span class="hwd">#{word}<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i\r
337       debug "new word #{word}"\r
338         wc[word.to_sym] = {:who => :dict}\r
339         return true\r
340     end\r
341     return false\r
342   end\r
343 \r
344   def random_pick_english(min=nil,max=nil)\r
345     # Try to pick a random word between min and max\r
346     word = String.new\r
347     min = min.to_s\r
348     max = max.to_s\r
349     if min > max\r
350       m.reply "#{min} > #{max}"\r
351       return word\r
352     end\r
353     rules = AZ_RULES[:english]\r
354     min = rules[:first] if min.empty?\r
355     max = rules[:last]  if max.empty?\r
356     debug "looking for word between #{min.inspect} and #{max.inspect}"\r
357     return word if min.empty? or max.empty?\r
358     begin\r
359       while (word <= min or word >= max or word !~ /^[a-z]+$/)\r
360         debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"\r
361         # TODO for the time being, skip words with extended characters\r
362         unless @wordcache.key?(:english)\r
363           @wordcache[:english] = Hash.new\r
364         end\r
365         wc = @wordcache[:english]\r
366 \r
367         if wc.size > 0\r
368           cache_or_url = rand(2)\r
369           if cache_or_url == 0\r
370             debug "getting word from wordcache"\r
371             word = wc.keys[rand(wc.size)].to_s\r
372             next\r
373           end\r
374         end\r
375 \r
376         # TODO when doing ranges, adapt this choice\r
377         l = ('a'..'z').to_a[rand(26)]\r
378         ll = ('a'..'z').to_a[rand(26)]\r
379         random = [l,ll].join('*') + '*'\r
380         debug "getting random word from dictionary, matching #{random}"\r
381         p = @bot.httputil.get_cached(rules[:url] % URI.escape(random))\r
382         debug p\r
383         debug "here 1"\r
384         lemmi = Array.new\r
385         debug "here 2"\r
386         good = rules[:good]\r
387         debug "here 3"\r
388         # We look for a lemma composed by a single word and of length at least two\r
389         p.scan(/<span class="hwd">(.*?)<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i) { |prelemma, discard|\r
390           debug "here 4"\r
391           lemma = prelemma.downcase\r
392           debug "here 5"\r
393           debug "checking lemma #{lemma} (#{prelemma}) and discarding #{discard}"\r
394           next if wc.key?(lemma.to_sym)\r
395           if lemma =~ /^[a-z]+$/\r
396             debug "good one"\r
397             lemmi << lemma\r
398             wc[lemma.to_sym] = {:who => :dict}\r
399           else\r
400             debug "funky characters, not good"\r
401           end\r
402         }\r
403         debug "here 6"\r
404         next if lemmi.empty?\r
405         debug "here 7"\r
406         word = lemmi[rand(lemmi.length)]\r
407         debug "here 8"\r
408       end\r
409     rescue => e\r
410       error "error #{e.inspect} while looking up a word"\r
411       error e.backtrace.join("\n")\r
412     end\r
413     return word\r
414   end\r
415 \r
416   def help(plugin, topic="")\r
417     case topic\r
418     when 'manage'\r
419       return "az [lang] word [count|list|add|delete] => manage the az wordlist for language lang (defaults to current bot language)"\r
420     when 'cancel'\r
421       return "az cancel => abort current game"\r
422     when 'check'\r
423       return 'az check <word> => checks <word> against current game'\r
424     when 'rules'\r
425       return "try to guess the word the bot is thinking of; if you guess wrong, the bot will use the new word to restrict the range of allowed words: eventually, the range will be so small around the correct word that you can't miss it"\r
426     when 'play'\r
427       return "az => start a game if none is running, show the current word range otherwise; you can say 'az <language>' if you want to play in a language different from the current bot default"\r
428     end\r
429     return "az topics: play, rules, cancel, manage, check"\r
430   end\r
431 \r
432 end\r
433 \r
434 plugin = AzGamePlugin.new\r
435 plugin.map 'az [:lang] word :cmd *params', :action=>'wordlist', :defaults => { :lang => nil, :cmd => 'count', :params => [] }, :auth_path => '!az::edit!'\r
436 plugin.map 'az cancel', :action=>'stop_game', :private => false\r
437 plugin.map 'az check :word', :action => 'manual_word_check', :private => false\r
438 plugin.map 'az [play] [:lang]', :action=>'start_game', :private => false, :defaults => { :lang => nil }\r
439 \r