From 394b1ddf3ed866e20e0f67d2b4d9431587c9c1de Mon Sep 17 00:00:00 2001 From: Giuseppe Bilotta Date: Sat, 28 Oct 2006 13:18:07 +0000 Subject: A-Z game --- ChangeLog | 6 + data/rbot/plugins/azgame.rb | 439 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 445 insertions(+) create mode 100644 data/rbot/plugins/azgame.rb diff --git a/ChangeLog b/ChangeLog index fae97f54..32145d5f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2006-10-28 Giuseppe Bilotta + + * A-Z game: try to guess the word the bot is thinking of: every miss + helps by reducing the range of allowed words, until it's so small + around the correct one that you can't miss it. + 2006-10-27 Giuseppe Bilotta * Flood protection: first attempt at penalty-based flood protection. diff --git a/data/rbot/plugins/azgame.rb b/data/rbot/plugins/azgame.rb new file mode 100644 index 00000000..f2da4fa2 --- /dev/null +++ b/data/rbot/plugins/azgame.rb @@ -0,0 +1,439 @@ +# vim: set et sw=2: +# A-Z Game: guess the word by reducing the interval of allowed ones +# +# Author: Giuseppe "Oblomov" Bilotta +# +# (C) 2006 Giuseppe Bilotta +# +# TODO allow manual addition of words + +AZ_RULES = { + :italian => { + :good => /s\.f\.|s\.m\.|agg\.|v\.tr\.|v\.(pronom\.)?intr\./, # avv\.|pron\.|cong\. + :bad => /var\./, + :first => 'abaco', + :last => 'zuzzurellone', + :url => "http://www.demauroparavia.it/%s", + :wapurl => "http://wap.demauroparavia.it/index.php?lemma=%s" + }, + :english => { + :good => /(?:singular )?noun|verb|adj/, + :first => 'abacus', + :last => 'zuni', + :url => "http://www.chambersharrap.co.uk/chambers/features/chref/chref.py/main?query=%s&title=21st" + } +} + +class AzGame + + attr_reader :range, :word + def initialize(plugin, lang, word) + @plugin = plugin + @lang = lang.to_sym + @word = word.downcase + @range = [AZ_RULES[lang][:first], AZ_RULES[lang][:last]] + def @range.to_s + return "%s -- %s" % self + end + end + + def check(word) + w = word.downcase + debug "checking #{w} for #{@word} in #{@range}" + return [:bingo, nil] if w == @word + return [:out, @range] if w < @range.first or w > @range.last + return [:ignore, @range] if w == @range.first or w == @range.last + return [:noexist, @range] unless @plugin.send("is_#{@lang}?", w) + debug "we like it" + if w < @word + @range.first.replace(w) + else + @range.last.replace(w) + end + return [:in, @range] + end + +end + +class AzGamePlugin < Plugin + + def initialize + super + # if @registry.has_key?(:games) + # @games = @registry[:games] + # else + @games = Hash.new + # end + if @registry.has_key?(:wordcache) and @registry[:wordcache] + @wordcache = @registry[:wordcache] + else + @wordcache = Hash.new + end + debug "\n\n\nA-Z wordcache: #{@wordcache.inspect}\n\n\n" + end + + def save + # @registry[:games] = @games + @registry[:wordcache] = @wordcache + end + + def listen(m) + return unless m.kind_of?(PrivMessage) + return if m.channel.nil? or m.address? + k = m.channel.to_s # to_sym? + return unless @games.key?(k) + return if m.params + word = m.plugin.downcase + return unless word =~ /^[a-z]+$/ + word_check(m, k, word) + end + + def word_check(m, k, word) + isit = @games[k].check(word) + case isit.first + when :bingo + m.reply "#{Bold}BINGO!#{Bold}: the word was #{Underline}#{word}#{Underline}. Congrats, #{Bold}#{m.sourcenick}#{Bold}!" + @games.delete(k) + when :out + m.reply "#{word} is not in the range #{Bold}#{isit.last}#{Bold}" if m.address? + when :noexist + m.reply "#{word} doesn't exist or is not acceptable for the game" + when :in + m.reply "close, but no cigar. New range: #{Bold}#{isit.last}#{Bold}" + when :ignore + m.reply "#{word} is already one of the range extrema: #{isit.last}" if m.address? + else + m.reply "hm, something went wrong while verifying #{word}" + end + end + + def manual_word_check(m, params) + k = m.channel.to_s + word = params[:word].downcase + if not @games.key?(k) + m.reply "no A-Z game running here, can't check for #{word}, can I?" + return + end + if word !~ /^[a-z]+$/ + m.reply "I only accept single words composed by letters only, sorry" + return + end + word_check(m, k, word) + end + + def stop_game(m, params) + return if m.channel.nil? # Shouldn't happen, but you never know + k = m.channel.to_s # to_sym? + if @games.key?(k) + m.reply "the word in #{Bold}#{@games[k].range}#{Bold} was: #{Bold}#{@games[k].word}" + @games.delete(k) + else + m.reply "no A-Z game running in this channel ..." + end + end + + def start_game(m, params) + return if m.channel.nil? # Shouldn't happen, but you never know + k = m.channel.to_s # to_sym? + unless @games.key?(k) + lang = (params[:lang] || @bot.config['core.language']).to_sym + method = 'random_pick_'+lang.to_s + m.reply "let me think ..." + if AZ_RULES.has_key?(lang) and self.respond_to?(method) + word = self.send(method) + if word.empty? + m.reply "couldn't think of anything ..." + return + end + else + m.reply "I can't play A-Z in #{lang}, sorry" + return + end + m.reply "got it!" + @games[k] = AzGame.new(self, lang, word) + end + m.reply "A-Z: #{Bold}#{@games[k].range}#{Bold}" + return + end + + def wordlist(m, params) + pars = params[:params] + lang = (params[:lang] || @bot.config['core.language']).to_sym + wc = @wordcache[lang] || Hash.new rescue Hash.new + cmd = params[:cmd].to_sym rescue :count + case cmd + when :count + m.reply "I have #{wc.size > 0 ? wc.size : 'no'} #{lang} words in my cache" + when :show, :list + if pars.empty? + m.reply "provide a regexp to match" + return + end + begin + regex = /#{pars[0]}/ + matches = wc.keys.map { |k| + k.to_s + }.grep(regex) + rescue + matches = [] + end + if matches.size == 0 + m.reply "no #{lang} word I know match #{pars[0]}" + elsif matches.size > 25 + m.reply "more than 25 #{lang} words I know match #{pars[0]}, try a stricter matching" + else + m.reply "#{matches.join(', ')}" + end + when :info + if pars.empty? + m.reply "provide a word" + return + end + word = pars[0].downcase.to_sym + if not wc.key?(word) + m.reply "I don't know any #{lang} word #{word}" + return + end + tr = "#{word} learned from #{wc[word][:who]}" + (tr << " on #{wc[word][:when]}") if wc[word].key?(:when) + m.reply tr + when :delete + if pars.empty? + m.reply "provide a word" + return + end + word = pars[0].downcase.to_sym + if not wc.key?(word) + m.reply "I don't know any #{lang} word #{word}" + return + end + wc.delete(word) + @bot.okay m.replyto + when :add + if pars.empty? + m.reply "provide a word" + return + end + word = pars[0].downcase.to_sym + if wc.key?(word) + m.reply "I already know the #{lang} word #{word}" + return + end + wc[word] = { :who => m.sourcenick, :when => Time.now } + @bot.okay m.replyto + else + end + end + + def is_italian?(word) + unless @wordcache.key?(:italian) + @wordcache[:italian] = Hash.new + end + wc = @wordcache[:italian] + return true if wc.key?(word.to_sym) + rules = AZ_RULES[:italian] + p = @bot.httputil.get_cached(rules[:wapurl] % word) + if not p + error "could not connect!" + return false + end + debug p + p.scan(/#{word} - (.*?) :dict} + return true + end + next + } + return false + end + + def random_pick_italian(min=nil,max=nil) + # Try to pick a random word between min and max + word = String.new + min = min.to_s + max = max.to_s + if min > max + m.reply "#{min} > #{max}" + return word + end + rules = AZ_RULES[:italian] + min = rules[:first] if min.empty? + max = rules[:last] if max.empty? + debug "looking for word between #{min.inspect} and #{max.inspect}" + return word if min.empty? or max.empty? + begin + while (word <= min or word >= max or word !~ /^[a-z]+$/) + debug "looking for word between #{min} and #{max} (prev: #{word.inspect})" + # TODO for the time being, skip words with extended characters + unless @wordcache.key?(:italian) + @wordcache[:italian] = Hash.new + end + wc = @wordcache[:italian] + + if wc.size > 0 + cache_or_url = rand(2) + if cache_or_url == 0 + debug "getting word from wordcache" + word = wc.keys[rand(wc.size)].to_s + next + end + end + + # TODO when doing ranges, adapt this choice + l = ('a'..'z').to_a[rand(26)] + debug "getting random word from dictionary, starting with letter #{l}" + first = rules[:url] % "lettera_#{l}_0_50" + p = @bot.httputil.get_cached(first) + max_page = p.match(/ \/ (\d+)<\/label>/)[1].to_i + pp = rand(max_page)+1 + debug "getting random word from dictionary, starting with letter #{l}, page #{pp}" + p = @bot.httputil.get_cached(first+"&pagina=#{pp}") if pp > 1 + lemmi = Array.new + good = rules[:good] + bad = rules[:bad] + # We look for a lemma composed by a single word and of length at least two + p.scan(/
  • .*? (.+?)<\/li>/) { |url, prelemma, tipo| + lemma = prelemma.downcase.to_sym + debug "checking lemma #{lemma} (#{prelemma}) of type #{tipo} from url #{url}" + next if wc.key?(lemma) + case tipo + when good + if tipo =~ bad + debug "refusing, #{bad}" + next + end + debug "good one" + lemmi << lemma + wc[lemma] = {:who => :dict} + else + debug "refusing, not #{good}" + end + } + word = lemmi[rand(lemmi.length)].to_s + end + rescue => e + error "error #{e.inspect} while looking up a word" + error e.backtrace.join("\n") + end + return word + end + + def is_english?(word) + unless @wordcache.key?(:english) + @wordcache[:english] = Hash.new + end + wc = @wordcache[:english] + return true if wc.key?(word.to_sym) + rules = AZ_RULES[:english] + p = @bot.httputil.get_cached(rules[:url] % URI.escape(word)) + if not p + error "could not connect!" + return false + end + debug p + if p =~ /#{word}<\/span>([^\n]+?)#{rules[:good]}<\/span>/i + debug "new word #{word}" + wc[word.to_sym] = {:who => :dict} + return true + end + return false + end + + def random_pick_english(min=nil,max=nil) + # Try to pick a random word between min and max + word = String.new + min = min.to_s + max = max.to_s + if min > max + m.reply "#{min} > #{max}" + return word + end + rules = AZ_RULES[:english] + min = rules[:first] if min.empty? + max = rules[:last] if max.empty? + debug "looking for word between #{min.inspect} and #{max.inspect}" + return word if min.empty? or max.empty? + begin + while (word <= min or word >= max or word !~ /^[a-z]+$/) + debug "looking for word between #{min} and #{max} (prev: #{word.inspect})" + # TODO for the time being, skip words with extended characters + unless @wordcache.key?(:english) + @wordcache[:english] = Hash.new + end + wc = @wordcache[:english] + + if wc.size > 0 + cache_or_url = rand(2) + if cache_or_url == 0 + debug "getting word from wordcache" + word = wc.keys[rand(wc.size)].to_s + next + end + end + + # TODO when doing ranges, adapt this choice + l = ('a'..'z').to_a[rand(26)] + ll = ('a'..'z').to_a[rand(26)] + random = [l,ll].join('*') + '*' + debug "getting random word from dictionary, matching #{random}" + p = @bot.httputil.get_cached(rules[:url] % URI.escape(random)) + debug p + debug "here 1" + lemmi = Array.new + debug "here 2" + good = rules[:good] + debug "here 3" + # We look for a lemma composed by a single word and of length at least two + p.scan(/(.*?)<\/span>([^\n]+?)#{rules[:good]}<\/span>/i) { |prelemma, discard| + debug "here 4" + lemma = prelemma.downcase + debug "here 5" + debug "checking lemma #{lemma} (#{prelemma}) and discarding #{discard}" + next if wc.key?(lemma.to_sym) + if lemma =~ /^[a-z]+$/ + debug "good one" + lemmi << lemma + wc[lemma.to_sym] = {:who => :dict} + else + debug "funky characters, not good" + end + } + debug "here 6" + next if lemmi.empty? + debug "here 7" + word = lemmi[rand(lemmi.length)] + debug "here 8" + end + rescue => e + error "error #{e.inspect} while looking up a word" + error e.backtrace.join("\n") + end + return word + end + + def help(plugin, topic="") + case topic + when 'manage' + return "az [lang] word [count|list|add|delete] => manage the az wordlist for language lang (defaults to current bot language)" + when 'cancel' + return "az cancel => abort current game" + when 'check' + return 'az check => checks against current game' + when 'rules' + 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" + when 'play' + return "az => start a game if none is running, show the current word range otherwise; you can say 'az ' if you want to play in a language different from the current bot default" + end + return "az topics: play, rules, cancel, manage, check" + end + +end + +plugin = AzGamePlugin.new +plugin.map 'az [:lang] word :cmd *params', :action=>'wordlist', :defaults => { :lang => nil, :cmd => 'count', :params => [] }, :auth_path => '!az::edit!' +plugin.map 'az cancel', :action=>'stop_game', :private => false +plugin.map 'az check :word', :action => 'manual_word_check', :private => false +plugin.map 'az [play] [:lang]', :action=>'start_game', :private => false, :defaults => { :lang => nil } + -- cgit v1.2.3