2 # A-Z Game: guess the word by reducing the interval of allowed ones
\r
4 # Author: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
\r
6 # (C) 2006 Giuseppe Bilotta
\r
8 # TODO allow manual addition of words
\r
12 :good => /s\.f\.|s\.m\.|agg\.|v\.tr\.|v\.(pronom\.)?intr\./, # avv\.|pron\.|cong\.
\r
15 :last => 'zuzzurellone',
\r
16 :url => "http://www.demauroparavia.it/%s",
\r
17 :wapurl => "http://wap.demauroparavia.it/index.php?lemma=%s"
\r
20 :good => /(?:singular )?noun|verb|adj/,
\r
23 :url => "http://www.chambersharrap.co.uk/chambers/features/chref/chref.py/main?query=%s&title=21st"
\r
29 attr_reader :range, :word
\r
30 attr_accessor :tries, :total_tries, :total_failed, :failed, :winner
\r
31 def initialize(plugin, lang, word)
\r
34 @word = word.downcase
\r
35 @range = [AZ_RULES[lang][:first].dup, AZ_RULES[lang][:last].dup]
\r
37 @total_failed = 0 # not used, reported, updated
\r
38 @tries = Hash.new(0)
\r
39 @failed = Hash.new(0) # not used, not reported, updated
\r
42 return "%s -- %s" % self
\r
48 debug "checking #{w} for #{@word} in #{@range}"
\r
49 return [:bingo, nil] if w == @word
\r
50 return [:out, @range] if w < @range.first or w > @range.last
\r
51 return [:ignore, @range] if w == @range.first or w == @range.last
\r
52 return [:noexist, @range] unless @plugin.send("is_#{@lang}?", w)
\r
55 @range.first.replace(w)
\r
57 @range.last.replace(w)
\r
59 return [:in, @range]
\r
62 # TODO scoring: base score is t = ceil(100*exp(-(n-1)^2/50))+p for n attempts
\r
63 # done by p players; players that didn't win but contributed
\r
64 # with a attempts will get t*a/n points
\r
70 p = @tries.keys.length
\r
71 t = (100*exp(-(n-1)**2/50**2)).ceil + p
\r
72 debug "Total score: #{t}"
\r
74 @tries.each { |k, a|
\r
75 ret[k] = [t*a/n, "%d %s" % [a, a > 1 ? "tries" : "try"]]
\r
78 debug "replacing winner score of %d with %d" % [ret[@winner].first, t]
\r
79 tries = ret[@winner].last
\r
80 ret[@winner] = [t, "winner, #{tries}"]
\r
82 return ret.sort_by { |h| h.last.first }.reverse
\r
87 class AzGamePlugin < Plugin
\r
91 # if @registry.has_key?(:games)
\r
92 # @games = @registry[:games]
\r
96 if @registry.has_key?(:wordcache) and @registry[:wordcache]
\r
97 @wordcache = @registry[:wordcache]
\r
99 @wordcache = Hash.new
\r
101 debug "\n\n\nA-Z wordcache: #{@wordcache.inspect}\n\n\n"
\r
105 # @registry[:games] = @games
\r
106 @registry[:wordcache] = @wordcache
\r
110 return unless m.kind_of?(PrivMessage)
\r
111 return if m.channel.nil? or m.address?
\r
112 k = m.channel.downcase.to_s # to_sym?
\r
113 return unless @games.key?(k)
\r
115 word = m.plugin.downcase
\r
116 return unless word =~ /^[a-z]+$/
\r
117 word_check(m, k, word)
\r
120 def word_check(m, k, word)
\r
121 isit = @games[k].check(word)
\r
124 m.reply "#{Bold}BINGO!#{Bold}: the word was #{Underline}#{word}#{Underline}. Congrats, #{Bold}#{m.sourcenick}#{Bold}!"
\r
125 @games[k].total_tries += 1
\r
126 @games[k].tries[m.source] += 1
\r
127 @games[k].winner = m.source
\r
128 ar = @games[k].score.inject([]) { |res, kv|
\r
129 res.push("%s: %d (%s)" % kv.flatten)
\r
131 m.reply "The game was won after #{@games[k].total_tries} tries. Scores for this game: #{ar.join('; ')}"
\r
134 m.reply "#{word} is not in the range #{Bold}#{isit.last}#{Bold}" if m.address?
\r
136 m.reply "#{word} doesn't exist or is not acceptable for the game"
\r
137 @games[k].total_failed += 1
\r
138 @games[k].failed[m.source] += 1
\r
140 m.reply "close, but no cigar. New range: #{Bold}#{isit.last}#{Bold}"
\r
141 @games[k].total_tries += 1
\r
142 @games[k].tries[m.source] += 1
\r
144 m.reply "#{word} is already one of the range extrema: #{isit.last}" if m.address?
\r
146 m.reply "hm, something went wrong while verifying #{word}"
\r
150 def manual_word_check(m, params)
\r
151 k = m.channel.downcase.to_s
\r
152 word = params[:word].downcase
\r
153 if not @games.key?(k)
\r
154 m.reply "no A-Z game running here, can't check if #{word} is valid, can I?"
\r
157 if word !~ /^[a-z]+$/
\r
158 m.reply "I only accept single words composed by letters only, sorry"
\r
161 word_check(m, k, word)
\r
164 def stop_game(m, params)
\r
165 return if m.channel.nil? # Shouldn't happen, but you never know
\r
166 k = m.channel.downcase.to_s # to_sym?
\r
168 m.reply "the word in #{Bold}#{@games[k].range}#{Bold} was: #{Bold}#{@games[k].word}"
\r
169 ar = @games[k].score.inject([]) { |res, kv|
\r
170 res.push("%s: %d (%s)" % kv.flatten)
\r
172 m.reply "The game was cancelled after #{@games[k].total_tries} tries. Scores for this game would have been: #{ar.join('; ')}"
\r
175 m.reply "no A-Z game running in this channel ..."
\r
179 def start_game(m, params)
\r
180 return if m.channel.nil? # Shouldn't happen, but you never know
\r
181 k = m.channel.downcase.to_s # to_sym?
\r
182 unless @games.key?(k)
\r
183 lang = (params[:lang] || @bot.config['core.language']).to_sym
\r
184 method = 'random_pick_'+lang.to_s
\r
185 m.reply "let me think ..."
\r
186 if AZ_RULES.has_key?(lang) and self.respond_to?(method)
\r
187 word = self.send(method)
\r
189 m.reply "couldn't think of anything ..."
\r
193 m.reply "I can't play A-Z in #{lang}, sorry"
\r
197 @games[k] = AzGame.new(self, lang, word)
\r
199 tr = @games[k].total_tries
\r
204 tr_msg = " (after 1 try"
\r
206 tr_msg = " (after #{tr} tries"
\r
209 unless tr_msg.empty?
\r
210 f_tr = @games[k].total_failed
\r
215 tr_msg << " and 1 invalid try)"
\r
217 tr_msg << " and #{f_tr} invalid tries)"
\r
221 m.reply "A-Z: #{Bold}#{@games[k].range}#{Bold}" + tr_msg
\r
225 def wordlist(m, params)
\r
226 pars = params[:params]
\r
227 lang = (params[:lang] || @bot.config['core.language']).to_sym
\r
228 wc = @wordcache[lang] || Hash.new rescue Hash.new
\r
229 cmd = params[:cmd].to_sym rescue :count
\r
232 m.reply "I have #{wc.size > 0 ? wc.size : 'no'} #{lang} words in my cache"
\r
235 m.reply "provide a regexp to match"
\r
239 regex = /#{pars[0]}/
\r
240 matches = wc.keys.map { |k|
\r
246 if matches.size == 0
\r
247 m.reply "no #{lang} word I know match #{pars[0]}"
\r
248 elsif matches.size > 25
\r
249 m.reply "more than 25 #{lang} words I know match #{pars[0]}, try a stricter matching"
\r
251 m.reply "#{matches.join(', ')}"
\r
255 m.reply "provide a word"
\r
258 word = pars[0].downcase.to_sym
\r
259 if not wc.key?(word)
\r
260 m.reply "I don't know any #{lang} word #{word}"
\r
263 tr = "#{word} learned from #{wc[word][:who]}"
\r
264 (tr << " on #{wc[word][:when]}") if wc[word].key?(:when)
\r
268 m.reply "provide a word"
\r
271 word = pars[0].downcase.to_sym
\r
272 if not wc.key?(word)
\r
273 m.reply "I don't know any #{lang} word #{word}"
\r
277 @bot.okay m.replyto
\r
280 m.reply "provide a word"
\r
283 word = pars[0].downcase.to_sym
\r
285 m.reply "I already know the #{lang} word #{word}"
\r
288 wc[word] = { :who => m.sourcenick, :when => Time.now }
\r
289 @bot.okay m.replyto
\r
294 def is_italian?(word)
\r
295 unless @wordcache.key?(:italian)
\r
296 @wordcache[:italian] = Hash.new
\r
298 wc = @wordcache[:italian]
\r
299 return true if wc.key?(word.to_sym)
\r
300 rules = AZ_RULES[:italian]
\r
301 p = @bot.httputil.get_cached(rules[:wapurl] % word)
\r
303 error "could not connect!"
\r
307 p.scan(/<anchor>#{word} - (.*?)<go href="lemma.php\?ID=([^"]*?)"/) { |qual, url|
\r
308 debug "new word #{word} of type #{qual}"
\r
309 if qual =~ rules[:good] and qual !~ rules[:bad]
\r
310 wc[word.to_sym] = {:who => :dict}
\r
318 def random_pick_italian(min=nil,max=nil)
\r
319 # Try to pick a random word between min and max
\r
324 m.reply "#{min} > #{max}"
\r
327 rules = AZ_RULES[:italian]
\r
328 min = rules[:first] if min.empty?
\r
329 max = rules[:last] if max.empty?
\r
330 debug "looking for word between #{min.inspect} and #{max.inspect}"
\r
331 return word if min.empty? or max.empty?
\r
333 while (word <= min or word >= max or word !~ /^[a-z]+$/)
\r
334 debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"
\r
335 # TODO for the time being, skip words with extended characters
\r
336 unless @wordcache.key?(:italian)
\r
337 @wordcache[:italian] = Hash.new
\r
339 wc = @wordcache[:italian]
\r
342 cache_or_url = rand(2)
\r
343 if cache_or_url == 0
\r
344 debug "getting word from wordcache"
\r
345 word = wc.keys[rand(wc.size)].to_s
\r
350 # TODO when doing ranges, adapt this choice
\r
351 l = ('a'..'z').to_a[rand(26)]
\r
352 debug "getting random word from dictionary, starting with letter #{l}"
\r
353 first = rules[:url] % "lettera_#{l}_0_50"
\r
354 p = @bot.httputil.get_cached(first)
\r
355 max_page = p.match(/ \/ (\d+)<\/label>/)[1].to_i
\r
356 pp = rand(max_page)+1
\r
357 debug "getting random word from dictionary, starting with letter #{l}, page #{pp}"
\r
358 p = @bot.httputil.get_cached(first+"&pagina=#{pp}") if pp > 1
\r
360 good = rules[:good]
\r
362 # We look for a lemma composed by a single word and of length at least two
\r
363 p.scan(/<li><a href="([^"]+?)" title="consulta il lemma ([^ "][^ "]+?)">.*? (.+?)<\/li>/) { |url, prelemma, tipo|
\r
364 lemma = prelemma.downcase.to_sym
\r
365 debug "checking lemma #{lemma} (#{prelemma}) of type #{tipo} from url #{url}"
\r
366 next if wc.key?(lemma)
\r
370 debug "refusing, #{bad}"
\r
375 wc[lemma] = {:who => :dict}
\r
377 debug "refusing, not #{good}"
\r
380 word = lemmi[rand(lemmi.length)].to_s
\r
383 error "error #{e.inspect} while looking up a word"
\r
384 error e.backtrace.join("\n")
\r
389 def is_english?(word)
\r
390 unless @wordcache.key?(:english)
\r
391 @wordcache[:english] = Hash.new
\r
393 wc = @wordcache[:english]
\r
394 return true if wc.key?(word.to_sym)
\r
395 rules = AZ_RULES[:english]
\r
396 p = @bot.httputil.get_cached(rules[:url] % URI.escape(word))
\r
398 error "could not connect!"
\r
402 if p =~ /<span class="(?:hwd|srch)">#{word}<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i
\r
403 debug "new word #{word}"
\r
404 wc[word.to_sym] = {:who => :dict}
\r
410 def random_pick_english(min=nil,max=nil)
\r
411 # Try to pick a random word between min and max
\r
416 m.reply "#{min} > #{max}"
\r
419 rules = AZ_RULES[:english]
\r
420 min = rules[:first] if min.empty?
\r
421 max = rules[:last] if max.empty?
\r
422 debug "looking for word between #{min.inspect} and #{max.inspect}"
\r
423 return word if min.empty? or max.empty?
\r
425 while (word <= min or word >= max or word !~ /^[a-z]+$/)
\r
426 debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"
\r
427 # TODO for the time being, skip words with extended characters
\r
428 unless @wordcache.key?(:english)
\r
429 @wordcache[:english] = Hash.new
\r
431 wc = @wordcache[:english]
\r
434 cache_or_url = rand(2)
\r
435 if cache_or_url == 0
\r
436 debug "getting word from wordcache"
\r
437 word = wc.keys[rand(wc.size)].to_s
\r
442 # TODO when doing ranges, adapt this choice
\r
443 l = ('a'..'z').to_a[rand(26)]
\r
444 ll = ('a'..'z').to_a[rand(26)]
\r
445 random = [l,ll].join('*') + '*'
\r
446 debug "getting random word from dictionary, matching #{random}"
\r
447 p = @bot.httputil.get_cached(rules[:url] % URI.escape(random))
\r
450 good = rules[:good]
\r
451 # We look for a lemma composed by a single word and of length at least two
\r
452 p.scan(/<span class="(?:hwd|srch)">(.*?)<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i) { |prelemma, discard|
\r
453 lemma = prelemma.downcase
\r
454 debug "checking lemma #{lemma} (#{prelemma}) and discarding #{discard}"
\r
455 next if wc.key?(lemma.to_sym)
\r
456 if lemma =~ /^[a-z]+$/
\r
459 wc[lemma.to_sym] = {:who => :dict}
\r
461 debug "funky characters, not good"
\r
464 next if lemmi.empty?
\r
465 word = lemmi[rand(lemmi.length)]
\r
468 error "error #{e.inspect} while looking up a word"
\r
469 error e.backtrace.join("\n")
\r
474 def help(plugin, topic="")
\r
477 return "az [lang] word [count|list|add|delete] => manage the az wordlist for language lang (defaults to current bot language)"
\r
479 return "az cancel => abort current game"
\r
481 return 'az check <word> => checks <word> against current game'
\r
483 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
485 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
487 return "az topics: play, rules, cancel, manage, check"
\r
492 plugin = AzGamePlugin.new
\r
493 plugin.map 'az [:lang] word :cmd *params', :action=>'wordlist', :defaults => { :lang => nil, :cmd => 'count', :params => [] }, :auth_path => '!az::edit!'
\r
494 plugin.map 'az cancel', :action=>'stop_game', :private => false
\r
495 plugin.map 'az check :word', :action => 'manual_word_check', :private => false
\r
496 plugin.map 'az [play] [:lang]', :action=>'start_game', :private => false, :defaults => { :lang => nil }
\r