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