summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2006-10-28 13:18:07 +0000
committerGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2006-10-28 13:18:07 +0000
commit394b1ddf3ed866e20e0f67d2b4d9431587c9c1de (patch)
tree66e35969f2478d5d1d3cbb9491f35acbac02abe0
parent9c33f608f0d150076f2acb869be1c390a0fade09 (diff)
A-Z game
-rw-r--r--ChangeLog6
-rw-r--r--data/rbot/plugins/azgame.rb439
2 files changed, 445 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index fae97f54..32145d5f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+2006-10-28 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+
+ * 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 <giuseppe.bilotta@gmail.com>
* 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 <giuseppe.bilotta@gmail.com>
+#
+# (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(/<anchor>#{word} - (.*?)<go href="lemma.php\?ID=([^"]*?)"/) { |qual, url|
+ debug "new word #{word} of type #{qual}"
+ if qual =~ rules[:good] and qual !~ rules[:bad]
+ wc[word.to_sym] = {:who => :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><a href="([^"]+?)" title="consulta il lemma ([^ "][^ "]+?)">.*?&nbsp;(.+?)<\/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 =~ /<span class="hwd">#{word}<\/span>([^\n]+?)<span class="psa">#{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 class="hwd">(.*?)<\/span>([^\n]+?)<span class="psa">#{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 <word> => checks <word> 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 <language>' 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 }
+