4 # :title: A-Z Game Plugin for rbot
\r
6 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
\r
7 # Author:: Yaohan Chen <yaohan.chen@gmail.com>: Japanese support
\r
9 # Copyright:: (C) 2006 Giuseppe Bilotta
\r
10 # Copyright:: (C) 2007 GIuseppe Bilotta, Yaohan Chen
\r
14 # A-Z Game: guess the word by reducing the interval of allowed ones
\r
16 # TODO allow manual addition of words
\r
20 attr_reader :range, :word
\r
21 attr_reader :lang, :rules, :listener
\r
22 attr_accessor :tries, :total_tries, :total_failed, :failed, :winner
\r
23 def initialize(plugin, lang, rules, word)
\r
26 @word = word.downcase
\r
28 @range = [@rules[:first].dup, @rules[:last].dup]
\r
29 @listener = @rules[:listener]
\r
31 @total_failed = 0 # not used, reported, updated
\r
32 @tries = Hash.new(0)
\r
33 @failed = Hash.new(0) # not used, not reported, updated
\r
36 return "%s -- %s" % self
\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
49 @range.first.replace(w)
\r
51 @range.last.replace(w)
\r
53 return [:in, @range]
\r
56 # TODO scoring: base score is t = ceil(100*exp(-((n-1)^2)/(50^2)))+p for n attempts
\r
57 # done by p players; players that didn't win but contributed
\r
58 # with a attempts will get t*a/n points
\r
64 p = @tries.keys.length
\r
65 t = (100*exp(-((n-1)**2)/(50.0**2))).ceil + p
\r
66 debug "Total score: #{t}"
\r
68 @tries.each { |k, a|
\r
69 ret[k] = [t*a/n, n_("%{count} try", "%{count} tries", a) % {:count => a}]
\r
72 debug "replacing winner score of %d with %d" % [ret[@winner].first, t]
\r
73 tries = ret[@winner].last
\r
74 ret[@winner] = [t, _("winner, %{tries}") % {:tries => tries}]
\r
76 return ret.sort_by { |h| h.last.first }.reverse
\r
81 class AzGamePlugin < Plugin
\r
85 # if @registry.has_key?(:games)
\r
86 # @games = @registry[:games]
\r
90 if @registry.has_key?(:wordcache) and @registry[:wordcache]
\r
91 @wordcache = @registry[:wordcache]
\r
93 @wordcache = Hash.new
\r
95 debug "A-Z wordcache: #{@wordcache.pretty_inspect}"
\r
99 :good => /s\.f\.|s\.m\.|agg\.|v\.tr\.|v\.(pronom\.)?intr\./, # avv\.|pron\.|cong\.
\r
102 :last => 'zuzzurellone',
\r
103 :url => "http://www.demauroparavia.it/%s",
\r
104 :wapurl => "http://wap.demauroparavia.it/index.php?lemma=%s",
\r
105 :listener => /^[a-z]+$/
\r
108 :good => /(?:singular )?noun|verb|adj/,
\r
109 :first => 'abacus',
\r
111 :url => "http://www.chambersharrap.co.uk/chambers/features/chref/chref.py/main?query=%s&title=21st",
\r
112 :listener => /^[a-z]+$/
\r
116 japanese_wordlist = "#{@bot.botclass}/azgame/wordlist-japanese"
\r
117 if File.exist?(japanese_wordlist)
\r
118 words = File.readlines(japanese_wordlist) \
\r
119 .map {|line| line.strip} .uniq
\r
120 if(words.length >= 4) # something to guess
\r
121 @rules[:japanese] = {
\r
124 :first => words[0],
\r
125 :last => words[-1],
\r
126 :listener => /^\S+$/
\r
128 debug "Japanese wordlist loaded, #{@rules[:japanese][:list].length} lines; first word: #{@rules[:japanese][:first]}, last word: #{@rules[:japanese][:last]}"
\r
134 # @registry[:games] = @games
\r
135 @registry[:wordcache] = @wordcache
\r
139 return unless m.kind_of?(PrivMessage)
\r
140 return if m.channel.nil? or m.address?
\r
141 k = m.channel.downcase.to_s # to_sym?
\r
142 return unless @games.key?(k)
\r
144 word = m.plugin.downcase
\r
145 return unless word =~ @games[k].listener
\r
146 word_check(m, k, word)
\r
149 def word_check(m, k, word)
\r
150 isit = @games[k].check(word)
\r
153 m.reply _("%{bold}BINGO!%{bold} the word was %{underline}%{word}%{underline}. Congrats, %{bold}%{player}%{bold}!") % {:bold => Bold, :underline => Underline, :word => word, :player => m.sourcenick}
\r
154 @games[k].total_tries += 1
\r
155 @games[k].tries[m.source] += 1
\r
156 @games[k].winner = m.source
\r
157 ar = @games[k].score.inject([]) { |res, kv|
\r
158 res.push("%s: %d (%s)" % kv.flatten)
\r
160 m.reply _("The game was won after %{tries} tries. Scores for this game: %{scores}") % {:tries => @games[k].total_tries, :scores => ar.join('; ')}
\r
163 m.reply _("%{word} is not in the range %{bold}%{range}%{bold}") % {:word => word, :bold => Bold, :range => isit.last} if m.address?
\r
165 m.reply _("%{word} doesn't exist or is not acceptable for the game") % {:word => word}
\r
166 @games[k].total_failed += 1
\r
167 @games[k].failed[m.source] += 1
\r
169 m.reply _("close, but no cigar. New range: %{bold}%{range}%{bold}") % {:bold => Bold, :range => isit.last}
\r
170 @games[k].total_tries += 1
\r
171 @games[k].tries[m.source] += 1
\r
173 m.reply _("%{word} is already one of the range extrema: %{range}") % {:word => word, :range => isit.last} if m.address?
\r
175 m.reply _("hm, something went wrong while verifying %{word}")
\r
179 def manual_word_check(m, params)
\r
180 k = m.channel.downcase.to_s
\r
181 word = params[:word].downcase
\r
182 if not @games.key?(k)
\r
183 m.reply _("no A-Z game running here, can't check if %{word} is valid, can I?")
\r
187 m.reply _("I only accept single words composed by letters only, sorry")
\r
190 word_check(m, k, word)
\r
193 def stop_game(m, params)
\r
194 return if m.channel.nil? # Shouldn't happen, but you never know
\r
195 k = m.channel.downcase.to_s # to_sym?
\r
197 m.reply _("the word in %{bold}%{range}%{bold} was: %{bold}%{word}%{bold}") % {:bold => Bold, :range => @games[k].range, :word => @games[k].word}
\r
198 ar = @games[k].score.inject([]) { |res, kv|
\r
199 res.push("%s: %d (%s)" % kv.flatten)
\r
201 m.reply _("The game was cancelled after %{tries} tries. Scores for this game would have been: %{scores}") % {:tries => @games[k].total_tries, :scores => ar.join('; ')}
\r
204 m.reply _("no A-Z game running in this channel ...")
\r
208 def start_game(m, params)
\r
209 return if m.channel.nil? # Shouldn't happen, but you never know
\r
210 k = m.channel.downcase.to_s # to_sym?
\r
211 unless @games.key?(k)
\r
212 lang = (params[:lang] || @bot.config['core.language']).to_sym
\r
213 method = 'random_pick_'+lang.to_s
\r
214 m.reply _("let me think ...")
\r
215 if @rules.has_key?(lang) and self.respond_to?(method)
\r
216 word = self.send(method)
\r
218 m.reply _("couldn't think of anything ...")
\r
222 m.reply _("I can't play A-Z in %{lang}, sorry") % {:lang => lang}
\r
225 m.reply _("got it!")
\r
226 @games[k] = AzGame.new(self, lang, @rules[lang], word)
\r
228 tr = @games[k].total_tries
\r
229 # this message building code is rewritten to make translation easier
\r
233 f_tr = @games[k].total_failed
\r
235 tr_msg = _(" (after %{total_tries} and %{invalid_tries})") %
\r
236 { :total_tries => n_("%{count} try", "%{count} tries", tr) %
\r
238 :invalid_tries => n_("%{count} invalid try", "%{count} invalid tries", tr) %
\r
241 tr_msg = _(" (after %{total_tries}") %
\r
242 { :total_tries => n_("%{count} try", "%{count} tries", tr) %
\r
247 m.reply _("A-Z: %{bold}%{range}%{bold}") % {:bold => Bold, :range => @games[k].range} + tr_msg
\r
251 def wordlist(m, params)
\r
252 pars = params[:params]
\r
253 lang = (params[:lang] || @bot.config['core.language']).to_sym
\r
254 wc = @wordcache[lang] || Hash.new rescue Hash.new
\r
255 cmd = params[:cmd].to_sym rescue :count
\r
258 m.reply n_("I have %{count} %{lang} word in my cache", "I have %{count} %{lang} words in my cache", wc.size) % {:count => wc.size, :lang => lang}
\r
261 m.reply _("provide a regexp to match")
\r
265 regex = /#{pars[0]}/
\r
266 matches = wc.keys.map { |k|
\r
272 if matches.size == 0
\r
273 m.reply _("no %{lang} word I know match %{pattern}") % {:lang => lang, :pattern => pars[0]}
\r
274 elsif matches.size > 25
\r
275 m.reply _("more than 25 %{lang} words I know match %{pattern}, try a stricter matching") % {:lang => lang, :pattern => pars[0]}
\r
277 m.reply "#{matches.join(', ')}"
\r
281 m.reply _("provide a word")
\r
284 word = pars[0].downcase.to_sym
\r
285 if not wc.key?(word)
\r
286 m.reply _("I don't know any %{lang} word %{word}") % {:lang => lang, :word => word}
\r
289 if wc[word].key?(:when)
\r
290 tr = _("%{word} learned from %{user} on %{date}") % {:word => word, :user => wc[word][:who], :date => wc[word][:when]}
\r
292 tr = _("%{word} learned from %{user}") % {:word => word, :user => wc[word][:who]}
\r
297 m.reply _("provide a word")
\r
300 word = pars[0].downcase.to_sym
\r
301 if not wc.key?(word)
\r
302 m.reply _("I don't know any %{lang} word %{word}") % {:lang => lang, :word => word}
\r
306 @bot.okay m.replyto
\r
309 m.reply _("provide a word")
\r
312 word = pars[0].downcase.to_sym
\r
314 m.reply _("I already know the %{lang} word %{word}")
\r
317 wc[word] = { :who => m.sourcenick, :when => Time.now }
\r
318 @bot.okay m.replyto
\r
323 def is_japanese?(word)
\r
324 @rules[:japanese][:list].include?(word)
\r
327 # return integer between min and max, inclusive
\r
328 def rand_between(min, max)
\r
329 rand(max - min + 1) + min
\r
332 def random_pick_japanese(min=nil, max=nil)
\r
333 rules = @rules[:japanese]
\r
334 min = rules[:first] if min.nil_or_empty?
\r
335 max = rules[:last] if max.nil_or_empty?
\r
336 debug "Randomly picking word between #{min} and #{max}"
\r
337 min_index = rules[:list].index(min)
\r
338 max_index = rules[:list].index(max)
\r
339 debug "Index between #{min_index} and #{max_index}"
\r
340 index = rand_between(min_index + 1, max_index - 1)
\r
341 debug "Index generated: #{index}"
\r
342 word = rules[:list][index]
\r
343 debug "Randomly picked #{word}"
\r
347 def is_italian?(word)
\r
348 unless @wordcache.key?(:italian)
\r
349 @wordcache[:italian] = Hash.new
\r
351 wc = @wordcache[:italian]
\r
352 return true if wc.key?(word.to_sym)
\r
353 rules = @rules[:italian]
\r
354 p = @bot.httputil.get(rules[:wapurl] % word)
\r
356 error "could not connect!"
\r
360 p.scan(/<anchor>#{word} - (.*?)<go href="lemma.php\?ID=([^"]*?)"/) { |qual, url|
\r
361 debug "new word #{word} of type #{qual}"
\r
362 if qual =~ rules[:good] and qual !~ rules[:bad]
\r
363 wc[word.to_sym] = {:who => :dict}
\r
371 def random_pick_italian(min=nil,max=nil)
\r
372 # Try to pick a random word between min and max
\r
377 m.reply "#{min} > #{max}"
\r
380 rules = @rules[:italian]
\r
381 min = rules[:first] if min.empty?
\r
382 max = rules[:last] if max.empty?
\r
383 debug "looking for word between #{min.inspect} and #{max.inspect}"
\r
384 return word if min.empty? or max.empty?
\r
386 while (word <= min or word >= max or word !~ /^[a-z]+$/)
\r
387 debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"
\r
388 # TODO for the time being, skip words with extended characters
\r
389 unless @wordcache.key?(:italian)
\r
390 @wordcache[:italian] = Hash.new
\r
392 wc = @wordcache[:italian]
\r
395 cache_or_url = rand(2)
\r
396 if cache_or_url == 0
\r
397 debug "getting word from wordcache"
\r
398 word = wc.keys[rand(wc.size)].to_s
\r
403 # TODO when doing ranges, adapt this choice
\r
404 l = ('a'..'z').to_a[rand(26)]
\r
405 debug "getting random word from dictionary, starting with letter #{l}"
\r
406 first = rules[:url] % "lettera_#{l}_0_50"
\r
407 p = @bot.httputil.get(first)
\r
408 max_page = p.match(/ \/ (\d+)<\/label>/)[1].to_i
\r
409 pp = rand(max_page)+1
\r
410 debug "getting random word from dictionary, starting with letter #{l}, page #{pp}"
\r
411 p = @bot.httputil.get(first+"&pagina=#{pp}") if pp > 1
\r
413 good = rules[:good]
\r
415 # We look for a lemma composed by a single word and of length at least two
\r
416 p.scan(/<li><a href="([^"]+?)" title="consulta il lemma ([^ "][^ "]+?)">.*? (.+?)<\/li>/) { |url, prelemma, tipo|
\r
417 lemma = prelemma.downcase.to_sym
\r
418 debug "checking lemma #{lemma} (#{prelemma}) of type #{tipo} from url #{url}"
\r
419 next if wc.key?(lemma)
\r
423 debug "refusing, #{bad}"
\r
428 wc[lemma] = {:who => :dict}
\r
430 debug "refusing, not #{good}"
\r
433 word = lemmi[rand(lemmi.length)].to_s
\r
436 error "error #{e.inspect} while looking up a word"
\r
437 error e.backtrace.join("\n")
\r
442 def is_english?(word)
\r
443 unless @wordcache.key?(:english)
\r
444 @wordcache[:english] = Hash.new
\r
446 wc = @wordcache[:english]
\r
447 return true if wc.key?(word.to_sym)
\r
448 rules = @rules[:english]
\r
449 p = @bot.httputil.get(rules[:url] % CGI.escape(word))
\r
451 error "could not connect!"
\r
455 if p =~ /<span class="(?:hwd|srch)">#{word}<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i
\r
456 debug "new word #{word}"
\r
457 wc[word.to_sym] = {:who => :dict}
\r
463 def random_pick_english(min=nil,max=nil)
\r
464 # Try to pick a random word between min and max
\r
469 m.reply "#{min} > #{max}"
\r
472 rules = @rules[:english]
\r
473 min = rules[:first] if min.empty?
\r
474 max = rules[:last] if max.empty?
\r
475 debug "looking for word between #{min.inspect} and #{max.inspect}"
\r
476 return word if min.empty? or max.empty?
\r
478 while (word <= min or word >= max or word !~ /^[a-z]+$/)
\r
479 debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"
\r
480 # TODO for the time being, skip words with extended characters
\r
481 unless @wordcache.key?(:english)
\r
482 @wordcache[:english] = Hash.new
\r
484 wc = @wordcache[:english]
\r
487 cache_or_url = rand(2)
\r
488 if cache_or_url == 0
\r
489 debug "getting word from wordcache"
\r
490 word = wc.keys[rand(wc.size)].to_s
\r
495 # TODO when doing ranges, adapt this choice
\r
496 l = ('a'..'z').to_a[rand(26)]
\r
497 ll = ('a'..'z').to_a[rand(26)]
\r
498 random = [l,ll].join('*') + '*'
\r
499 debug "getting random word from dictionary, matching #{random}"
\r
500 p = @bot.httputil.get(rules[:url] % CGI.escape(random))
\r
503 good = rules[:good]
\r
504 # We look for a lemma composed by a single word and of length at least two
\r
505 p.scan(/<span class="(?:hwd|srch)">(.*?)<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i) { |prelemma, discard|
\r
506 lemma = prelemma.downcase
\r
507 debug "checking lemma #{lemma} (#{prelemma}) and discarding #{discard}"
\r
508 next if wc.key?(lemma.to_sym)
\r
509 if lemma =~ /^[a-z]+$/
\r
512 wc[lemma.to_sym] = {:who => :dict}
\r
514 debug "funky characters, not good"
\r
517 next if lemmi.empty?
\r
518 word = lemmi[rand(lemmi.length)]
\r
521 error "error #{e.inspect} while looking up a word"
\r
522 error e.backtrace.join("\n")
\r
527 def help(plugin, topic="")
\r
530 return _("az [lang] word [count|list|add|delete] => manage the az wordlist for language lang (defaults to current bot language)")
\r
532 return _("az cancel => abort current game")
\r
534 return _('az check <word> => checks <word> against current game')
\r
536 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
538 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
540 return _("az topics: play, rules, cancel, manage, check")
\r
545 plugin = AzGamePlugin.new
\r
546 plugin.map 'az [:lang] word :cmd *params', :action=>'wordlist', :defaults => { :lang => nil, :cmd => 'count', :params => [] }, :auth_path => '!az::edit!'
\r
547 plugin.map 'az cancel', :action=>'stop_game', :private => false
\r
548 plugin.map 'az check :word', :action => 'manual_word_check', :private => false
\r
549 plugin.map 'az [play] [:lang]', :action=>'start_game', :private => false, :defaults => { :lang => nil }
\r