]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - data/rbot/plugins/games/azgame.rb
refactor: wordlist shouldn't use bot singleton #35
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / games / azgame.rb
index 12cdf724196d8f9566403d3cf793603d67c5ef65..b4dbabc1b73a03573c8883112b89c3e8246119df 100644 (file)
-#-- vim:sw=2:et\r
-#++\r
-#\r
-# :title: A-Z Game Plugin for rbot\r
-#\r
-# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>\r
-# Author:: Yaohan Chen <yaohan.chen@gmail.com>: Japanese support\r
-#\r
-# Copyright:: (C) 2006 Giuseppe Bilotta\r
-# Copyright:: (C) 2007 GIuseppe Bilotta, Yaohan Chen\r
-#\r
-# License:: GPL v2\r
-#\r
-# A-Z Game: guess the word by reducing the interval of allowed ones\r
-#\r
-# TODO allow manual addition of words\r
-\r
-class AzGame\r
-\r
-  attr_reader :range, :word\r
-  attr_reader :lang, :rules, :listener\r
-  attr_accessor :tries, :total_tries, :total_failed, :failed, :winner\r
-  def initialize(plugin, lang, rules, word)\r
-    @plugin = plugin\r
-    @lang = lang.to_sym\r
-    @word = word.downcase\r
-    @rules = rules\r
-    @range = [@rules[:first].dup, @rules[:last].dup]\r
-    @listener = @rules[:listener]\r
-    @total_tries = 0\r
-    @total_failed = 0 # not used, reported, updated\r
-    @tries = Hash.new(0)\r
-    @failed = Hash.new(0) # not used, not reported, updated\r
-    @winner = nil\r
-    def @range.to_s\r
-      return "%s -- %s" % self\r
-    end\r
-  end\r
-\r
-  def check(word)\r
-    w = word.downcase\r
-    debug "checking #{w} for #{@word} in #{@range}"\r
-    return [:bingo, nil] if w == @word\r
-    return [:out, @range] if w < @range.first or w > @range.last\r
-    return [:ignore, @range] if w == @range.first or w == @range.last\r
-    return [:noexist, @range] unless @plugin.send("is_#{@lang}?", w)\r
-    debug "we like it"\r
-    if w < @word and w > @range.first\r
-      @range.first.replace(w)\r
-      return [:in, @range]\r
-    elsif w > @word and w < @range.last\r
-      @range.last.replace(w)\r
-      return [:in, @range]\r
-    end\r
-    return [:out, @range]\r
-  end\r
-\r
-# TODO scoring: base score is t = ceil(100*exp(-((n-1)^2)/(50^2)))+p for n attempts\r
-#               done by p players; players that didn't win but contributed\r
-#               with a attempts will get t*a/n points\r
-\r
-  include Math\r
-\r
-  def score\r
-    n = @total_tries\r
-    p = @tries.keys.length\r
-    t = (100*exp(-((n-1)**2)/(50.0**2))).ceil + p\r
-    debug "Total score: #{t}"\r
-    ret = Hash.new\r
-    @tries.each { |k, a|\r
-      ret[k] = [t*a/n, n_("%{count} try", "%{count} tries", a) % {:count => a}]\r
-    }\r
-    if @winner\r
-      debug "replacing winner score of %d with %d" % [ret[@winner].first, t]\r
-      tries = ret[@winner].last\r
-      ret[@winner] = [t, _("winner, %{tries}") % {:tries => tries}]\r
-    end\r
-    return ret.sort_by { |h| h.last.first }.reverse\r
-  end\r
-\r
-end\r
-\r
-class AzGamePlugin < Plugin\r
-\r
-  def initialize\r
-    super\r
-    # if @registry.has_key?(:games)\r
-    #   @games = @registry[:games]\r
-    # else\r
-      @games = Hash.new\r
-    # end\r
-    if @registry.has_key?(:wordcache) and @registry[:wordcache]\r
-      @wordcache = @registry[:wordcache]\r
-    else\r
-      @wordcache = Hash.new\r
-    end\r
-    debug "A-Z wordcache: #{@wordcache.pretty_inspect}"\r
-\r
-    @rules = {\r
-      :italian => {\r
-      :good => /s\.f\.|s\.m\.|agg\.|v\.tr\.|v\.(pronom\.)?intr\./, # avv\.|pron\.|cong\.\r
-      :bad => /var\./,\r
-      :first => 'abaco',\r
-      :last => 'zuzzurellone',\r
-      :url => "http://www.demauroparavia.it/%s",\r
-      :wapurl => "http://wap.demauroparavia.it/index.php?lemma=%s",\r
-      :listener => /^[a-z]+$/\r
-    },\r
-    :english => {\r
-      :good => /(?:singular )?noun|verb|adj/,\r
-      :first => 'abacus',\r
-      :last => 'zuni',\r
-      :url => "http://www.chambersharrap.co.uk/chambers/features/chref/chref.py/main?query=%s&title=21st",\r
-      :listener => /^[a-z]+$/\r
-    },\r
-    }\r
-    \r
-    japanese_wordlist = "#{@bot.botclass}/azgame/wordlist-japanese"\r
-    if File.exist?(japanese_wordlist)\r
-      words = File.readlines(japanese_wordlist) \\r
-                             .map {|line| line.strip} .uniq\r
-      if(words.length >= 4) # something to guess\r
-        @rules[:japanese] = {\r
-            :good => /^\S+$/,\r
-            :list => words,\r
-            :first => words[0],\r
-            :last => words[-1],\r
-            :listener => /^\S+$/\r
-        }\r
-        debug "Japanese wordlist loaded, #{@rules[:japanese][:list].length} lines; first word: #{@rules[:japanese][:first]}, last word: #{@rules[:japanese][:last]}"\r
-      end\r
-    end\r
-  end\r
-\r
-  def save\r
-    # @registry[:games] = @games\r
-    @registry[:wordcache] = @wordcache\r
-  end\r
-\r
-  def listen(m)\r
-    return unless m.kind_of?(PrivMessage)\r
-    return if m.channel.nil? or m.address?\r
-    k = m.channel.downcase.to_s # to_sym?\r
-    return unless @games.key?(k)\r
-    return if m.params\r
-    word = m.plugin.downcase\r
-    return unless word =~ @games[k].listener\r
-    word_check(m, k, word)\r
-  end\r
-\r
-  def word_check(m, k, word)\r
-    # Not really safe ... what happens\r
-    Thread.new {\r
-      isit = @games[k].check(word)\r
-      case isit.first\r
-      when :bingo\r
-        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
-        @games[k].total_tries += 1\r
-        @games[k].tries[m.source] += 1\r
-        @games[k].winner = m.source\r
-        ar = @games[k].score.inject([]) { |res, kv|\r
-          res.push("%s: %d (%s)" % kv.flatten)\r
-        }\r
-        m.reply _("The game was won after %{tries} tries. Scores for this game:    %{scores}") % {:tries => @games[k].total_tries, :scores => ar.join('; ')}\r
-        @games.delete(k)\r
-      when :out\r
-        m.reply _("%{word} is not in the range %{bold}%{range}%{bold}") % {:word => word, :bold => Bold, :range => isit.last} if m.address?\r
-      when :noexist\r
-        m.reply _("%{word} doesn't exist or is not acceptable for the game") % {:word => word}\r
-        @games[k].total_failed += 1\r
-        @games[k].failed[m.source] += 1\r
-      when :in\r
-        m.reply _("close, but no cigar. New range: %{bold}%{range}%{bold}") % {:bold => Bold, :range => isit.last}\r
-        @games[k].total_tries += 1\r
-        @games[k].tries[m.source] += 1\r
-      when :ignore\r
-        m.reply _("%{word} is already one of the range extrema: %{range}") % {:word => word, :range => isit.last} if m.address?\r
-      else\r
-        m.reply _("hm, something went wrong while verifying %{word}")\r
-      end\r
-    }\r
-  end\r
-\r
-  def manual_word_check(m, params)\r
-    k = m.channel.downcase.to_s\r
-    word = params[:word].downcase\r
-    if not @games.key?(k)\r
-      m.reply _("no A-Z game running here, can't check if %{word} is valid, can I?")\r
-      return\r
-    end\r
-    if word !~ /^\S+$/\r
-      m.reply _("I only accept single words composed by letters only, sorry")\r
-      return\r
-    end\r
-    word_check(m, k, word)\r
-  end\r
-\r
-  def stop_game(m, params)\r
-    return if m.channel.nil? # Shouldn't happen, but you never know\r
-    k = m.channel.downcase.to_s # to_sym?\r
-    if @games.key?(k)\r
-      m.reply _("the word in %{bold}%{range}%{bold} was:   %{bold}%{word}%{bold}") % {:bold => Bold, :range => @games[k].range, :word => @games[k].word}\r
-      ar = @games[k].score.inject([]) { |res, kv|\r
-        res.push("%s: %d (%s)" % kv.flatten)\r
-      }\r
-      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
-      @games.delete(k)\r
-    else\r
-      m.reply _("no A-Z game running in this channel ...")\r
-    end\r
-  end\r
-\r
-  def start_game(m, params)\r
-    return if m.channel.nil? # Shouldn't happen, but you never know\r
-    k = m.channel.downcase.to_s # to_sym?\r
-    unless @games.key?(k)\r
-      lang = (params[:lang] || @bot.config['core.language']).to_sym\r
-      method = 'random_pick_'+lang.to_s\r
-      m.reply _("let me think ...")\r
-      if @rules.has_key?(lang) and self.respond_to?(method)\r
-        word = self.send(method)\r
-        if word.empty?\r
-          m.reply _("couldn't think of anything ...")\r
-          return\r
-        end\r
-      else\r
-        m.reply _("I can't play A-Z in %{lang}, sorry") % {:lang => lang}\r
-        return\r
-      end\r
-      m.reply _("got it!")\r
-      @games[k] = AzGame.new(self, lang, @rules[lang], word)\r
-    end\r
-    tr = @games[k].total_tries\r
-    # this message building code is rewritten to make translation easier\r
-    if tr == 0\r
-      tr_msg = ''\r
-    else\r
-      f_tr = @games[k].total_failed\r
-      if f_tr > 0\r
-        tr_msg = _(" (after %{total_tries} and %{invalid_tries})") %\r
-           { :total_tries => n_("%{count} try", "%{count} tries", tr) %\r
-                             {:count => tr},\r
-             :invalid_tries => n_("%{count} invalid try", "%{count} invalid tries", tr) %\r
-                               {:count => f_tr} }\r
-      else\r
-        tr_msg = _(" (after %{total_tries}") %\r
-                 { :total_tries => n_("%{count} try", "%{count} tries", tr) %\r
-                             {:count => tr}}\r
-      end\r
-    end\r
-\r
-    m.reply _("A-Z: %{bold}%{range}%{bold}") % {:bold => Bold, :range => @games[k].range} + tr_msg\r
-    return\r
-  end\r
-\r
-  def wordlist(m, params)\r
-    pars = params[:params]\r
-    lang = (params[:lang] || @bot.config['core.language']).to_sym\r
-    wc = @wordcache[lang] || Hash.new rescue Hash.new\r
-    cmd = params[:cmd].to_sym rescue :count\r
-    case cmd\r
-    when :count\r
-      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
-    when :show, :list\r
-      if pars.empty?\r
-        m.reply _("provide a regexp to match")\r
-        return\r
-      end\r
-      begin\r
-        regex = /#{pars[0]}/\r
-        matches = wc.keys.map { |k|\r
-          k.to_s\r
-        }.grep(regex)\r
-      rescue\r
-        matches = []\r
-      end\r
-      if matches.size == 0\r
-        m.reply _("no %{lang} word I know match %{pattern}") % {:lang => lang, :pattern => pars[0]}\r
-      elsif matches.size > 25\r
-        m.reply _("more than 25 %{lang} words I know match %{pattern}, try a stricter matching") % {:lang => lang, :pattern => pars[0]}\r
-      else\r
-        m.reply "#{matches.join(', ')}"\r
-      end\r
-    when :info\r
-      if pars.empty?\r
-        m.reply _("provide a word")\r
-        return\r
-      end\r
-      word = pars[0].downcase.to_sym\r
-      if not wc.key?(word)\r
-        m.reply _("I don't know any %{lang} word %{word}") % {:lang => lang, :word => word}\r
-        return\r
-      end\r
-      if wc[word].key?(:when)\r
-        tr = _("%{word} learned from %{user} on %{date}") % {:word => word, :user => wc[word][:who], :date => wc[word][:when]}\r
-      else\r
-        tr = _("%{word} learned from %{user}") % {:word => word, :user => wc[word][:who]} \r
-      end\r
-      m.reply tr\r
-    when :delete \r
-      if pars.empty?\r
-        m.reply _("provide a word")\r
-        return\r
-      end\r
-      word = pars[0].downcase.to_sym\r
-      if not wc.key?(word)\r
-        m.reply _("I don't know any %{lang} word %{word}") % {:lang => lang, :word => word}\r
-        return\r
-      end\r
-      wc.delete(word)\r
-      @bot.okay m.replyto\r
-    when :add\r
-      if pars.empty?\r
-        m.reply _("provide a word")\r
-        return\r
-      end\r
-      word = pars[0].downcase.to_sym\r
-      if wc.key?(word)\r
-        m.reply _("I already know the %{lang} word %{word}")\r
-        return\r
-      end\r
-      wc[word] = { :who => m.sourcenick, :when => Time.now }\r
-      @bot.okay m.replyto\r
-    else\r
-    end\r
-  end\r
-\r
-  def is_japanese?(word)\r
-    @rules[:japanese][:list].include?(word)\r
-  end\r
-  \r
-  # return integer between min and max, inclusive\r
-  def rand_between(min, max)\r
-    rand(max - min + 1) + min\r
-  end\r
-  \r
-  def random_pick_japanese(min=nil, max=nil)\r
-    rules = @rules[:japanese]\r
-    min = rules[:first] if min.nil_or_empty?\r
-    max = rules[:last]  if max.nil_or_empty?\r
-    debug "Randomly picking word between #{min} and #{max}"\r
-    min_index = rules[:list].index(min)\r
-    max_index = rules[:list].index(max)\r
-    debug "Index between #{min_index} and #{max_index}"\r
-    index = rand_between(min_index + 1, max_index - 1)\r
-    debug "Index generated: #{index}"\r
-    word = rules[:list][index]\r
-    debug "Randomly picked #{word}"\r
-    word\r
-  end\r
-\r
-  def is_italian?(word)\r
-    unless @wordcache.key?(:italian)\r
-      @wordcache[:italian] = Hash.new\r
-    end\r
-    wc = @wordcache[:italian]\r
-    return true if wc.key?(word.to_sym)\r
-    rules = @rules[:italian]\r
-    p = @bot.httputil.get(rules[:wapurl] % word)\r
-    if not p\r
-      error "could not connect!"\r
-      return false\r
-    end\r
-    debug p\r
-    p.scan(/<anchor>#{word} - (.*?)<go href="lemma.php\?ID=([^"]*?)"/) { |qual, url|\r
-      debug "new word #{word} of type #{qual}"\r
-      if qual =~ rules[:good] and qual !~ rules[:bad]\r
-        wc[word.to_sym] = {:who => :dict}\r
-        return true\r
-      end\r
-      next\r
-    }\r
-    return false\r
-  end\r
-\r
-  def random_pick_italian(min=nil,max=nil)\r
-    # Try to pick a random word between min and max\r
-    word = String.new\r
-    min = min.to_s\r
-    max = max.to_s\r
-    if min > max\r
-      m.reply "#{min} > #{max}"\r
-      return word\r
-    end\r
-    rules = @rules[:italian]\r
-    min = rules[:first] if min.empty?\r
-    max = rules[:last]  if max.empty?\r
-    debug "looking for word between #{min.inspect} and #{max.inspect}"\r
-    return word if min.empty? or max.empty?\r
-    begin\r
-      while (word <= min or word >= max or word !~ /^[a-z]+$/)\r
-        debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"\r
-        # TODO for the time being, skip words with extended characters\r
-        unless @wordcache.key?(:italian)\r
-          @wordcache[:italian] = Hash.new\r
-        end\r
-        wc = @wordcache[:italian]\r
-\r
-        if wc.size > 0\r
-          cache_or_url = rand(2)\r
-          if cache_or_url == 0\r
-            debug "getting word from wordcache"\r
-            word = wc.keys[rand(wc.size)].to_s\r
-            next\r
-          end\r
-        end\r
-\r
-        # TODO when doing ranges, adapt this choice\r
-        l = ('a'..'z').to_a[rand(26)]\r
-        debug "getting random word from dictionary, starting with letter #{l}"\r
-        first = rules[:url] % "lettera_#{l}_0_50"\r
-        p = @bot.httputil.get(first)\r
-        max_page = p.match(/ \/ (\d+)<\/label>/)[1].to_i\r
-        pp = rand(max_page)+1\r
-        debug "getting random word from dictionary, starting with letter #{l}, page #{pp}"\r
-        p = @bot.httputil.get(first+"&pagina=#{pp}") if pp > 1\r
-        lemmi = Array.new\r
-        good = rules[:good]\r
-        bad =  rules[:bad]\r
-        # We look for a lemma composed by a single word and of length at least two\r
-        p.scan(/<li><a href="([^"]+?)" title="consulta il lemma ([^ "][^ "]+?)">.*?&nbsp;(.+?)<\/li>/) { |url, prelemma, tipo|\r
-          lemma = prelemma.downcase.to_sym\r
-          debug "checking lemma #{lemma} (#{prelemma}) of type #{tipo} from url #{url}"\r
-          next if wc.key?(lemma)\r
-          case tipo\r
-          when good\r
-            if tipo =~ bad\r
-              debug "refusing, #{bad}"\r
-              next\r
-            end\r
-            debug "good one"\r
-            lemmi << lemma\r
-            wc[lemma] = {:who => :dict}\r
-          else\r
-            debug "refusing, not #{good}"\r
-          end\r
-        }\r
-        word = lemmi[rand(lemmi.length)].to_s\r
-      end\r
-    rescue => e\r
-      error "error #{e.inspect} while looking up a word"\r
-      error e.backtrace.join("\n")\r
-    end\r
-    return word\r
-  end\r
-\r
-  def is_english?(word)\r
-    unless @wordcache.key?(:english)\r
-      @wordcache[:english] = Hash.new\r
-    end\r
-    wc = @wordcache[:english]\r
-    return true if wc.key?(word.to_sym)\r
-    rules = @rules[:english]\r
-    p = @bot.httputil.get(rules[:url] % CGI.escape(word))\r
-    if not p\r
-      error "could not connect!"\r
-      return false\r
-    end\r
-    debug p\r
-    if p =~ /<span class="(?:hwd|srch)">#{word}<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i\r
-      debug "new word #{word}"\r
-        wc[word.to_sym] = {:who => :dict}\r
-        return true\r
-    end\r
-    return false\r
-  end\r
-\r
-  def random_pick_english(min=nil,max=nil)\r
-    # Try to pick a random word between min and max\r
-    word = String.new\r
-    min = min.to_s\r
-    max = max.to_s\r
-    if min > max\r
-      m.reply "#{min} > #{max}"\r
-      return word\r
-    end\r
-    rules = @rules[:english]\r
-    min = rules[:first] if min.empty?\r
-    max = rules[:last]  if max.empty?\r
-    debug "looking for word between #{min.inspect} and #{max.inspect}"\r
-    return word if min.empty? or max.empty?\r
-    begin\r
-      while (word <= min or word >= max or word !~ /^[a-z]+$/)\r
-        debug "looking for word between #{min} and #{max} (prev: #{word.inspect})"\r
-        # TODO for the time being, skip words with extended characters\r
-        unless @wordcache.key?(:english)\r
-          @wordcache[:english] = Hash.new\r
-        end\r
-        wc = @wordcache[:english]\r
-\r
-        if wc.size > 0\r
-          cache_or_url = rand(2)\r
-          if cache_or_url == 0\r
-            debug "getting word from wordcache"\r
-            word = wc.keys[rand(wc.size)].to_s\r
-            next\r
-          end\r
-        end\r
-\r
-        # TODO when doing ranges, adapt this choice\r
-        l = ('a'..'z').to_a[rand(26)]\r
-        ll = ('a'..'z').to_a[rand(26)]\r
-        random = [l,ll].join('*') + '*'\r
-        debug "getting random word from dictionary, matching #{random}"\r
-        p = @bot.httputil.get(rules[:url] % CGI.escape(random))\r
-        debug p\r
-        lemmi = Array.new\r
-        good = rules[:good]\r
-        # We look for a lemma composed by a single word and of length at least two\r
-        p.scan(/<span class="(?:hwd|srch)">(.*?)<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i) { |prelemma, discard|\r
-          lemma = prelemma.downcase\r
-          debug "checking lemma #{lemma} (#{prelemma}) and discarding #{discard}"\r
-          next if wc.key?(lemma.to_sym)\r
-          if lemma =~ /^[a-z]+$/\r
-            debug "good one"\r
-            lemmi << lemma\r
-            wc[lemma.to_sym] = {:who => :dict}\r
-          else\r
-            debug "funky characters, not good"\r
-          end\r
-        }\r
-        next if lemmi.empty?\r
-        word = lemmi[rand(lemmi.length)]\r
-      end\r
-    rescue => e\r
-      error "error #{e.inspect} while looking up a word"\r
-      error e.backtrace.join("\n")\r
-    end\r
-    return word\r
-  end\r
-\r
-  def help(plugin, topic="")\r
-    case topic\r
-    when 'manage'\r
-      return _("az [lang] word [count|list|add|delete] => manage the az wordlist for language lang (defaults to current bot language)")\r
-    when 'cancel'\r
-      return _("az cancel => abort current game")\r
-    when 'check'\r
-      return _('az check <word> => checks <word> against current game')\r
-    when 'rules'\r
-      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
-    when 'play'\r
-      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
-    end\r
-    return _("az topics: play, rules, cancel, manage, check")\r
-  end\r
-\r
-end\r
-\r
-plugin = AzGamePlugin.new\r
-plugin.map 'az [:lang] word :cmd *params', :action=>'wordlist', :defaults => { :lang => nil, :cmd => 'count', :params => [] }, :auth_path => '!az::edit!'\r
-plugin.map 'az cancel', :action=>'stop_game', :private => false\r
-plugin.map 'az check :word', :action => 'manual_word_check', :private => false\r
-plugin.map 'az [play] [:lang]', :action=>'start_game', :private => false, :defaults => { :lang => nil }\r
-\r
+#-- vim:sw=2:et
+#++
+#
+# :title: A-Z Game Plugin for rbot
+#
+# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
+# Author:: Yaohan Chen <yaohan.chen@gmail.com>: Japanese support
+#
+# Copyright:: (C) 2006 Giuseppe Bilotta
+# Copyright:: (C) 2007 GIuseppe Bilotta, Yaohan Chen
+#
+# License:: GPL v2
+#
+# A-Z Game: guess the word by reducing the interval of allowed ones
+#
+# TODO allow manual addition of words
+
+class AzGame
+
+  attr_reader :range, :word
+  attr_reader :lang, :rules, :listener
+  attr_accessor :tries, :total_tries, :total_failed, :failed, :winner
+  def initialize(plugin, lang, rules, word)
+    @plugin = plugin
+    @lang = lang.to_sym
+    @word = word.downcase
+    @rules = rules
+    @range = [@rules[:first].dup, @rules[:last].dup]
+    @listener = @rules[:listener]
+    @total_tries = 0
+    @total_failed = 0 # not used, reported, updated
+    @tries = Hash.new(0)
+    @failed = Hash.new(0) # not used, not reported, updated
+    @winner = nil
+    def @range.to_s
+      return "%s -- %s" % self
+    end
+    if @rules[:list]
+      @check_method = "is_#{@rules[:addlang]}?"
+      # trick: if addlang was not in rules, this will be is_? which is
+      # not a method of the plugin
+      if @check_method and not @plugin.respond_to? @check_method
+        @check_method = nil
+      end
+      @check = Proc.new do |w|
+        wl = @rules[:list].include?(w)
+        if !wl and @check_method
+          if wl = @plugin.send(@check_method, w)
+            debug "adding #{w} to #{@rules[:addfile]}"
+            begin
+              File.open(@rules[:addfile], "a") do |f|
+                f.puts w
+              end
+            rescue Exception => e
+              error "failed to add #{w} to #{@rules[:addfile]}"
+              error e
+            end
+          end
+        end
+        wl
+      end
+    else
+      @check_method = "is_#{@lang}?"
+      @check = Proc.new { |w| @plugin.send(@check_method, w) }
+    end
+  end
+
+  def check(word)
+    w = word.downcase
+    debug "checking #{w} for #{@word} in #{@range}"
+    # Since we're called threaded, bail out early if a winner
+    # was assigned already
+    return [:ignore, nil] if @winner
+    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
+    # This is potentially slow (for languages that check online)
+    return [:noexist, @range] unless @check.call(w)
+    debug "we like it"
+    # Check again if there was a winner in the mean time,
+    # and bail out if there was
+    return [:ignore, nil] if @winner
+    if w < @word and w > @range.first
+      @range.first.replace(w)
+      return [:in, @range]
+    elsif w > @word and w < @range.last
+      @range.last.replace(w)
+      return [:in, @range]
+    end
+    return [:out, @range]
+  end
+
+# TODO scoring: base score is t = ceil(100*exp(-((n-1)^2)/(50^2)))+p for n attempts
+#               done by p players; players that didn't win but contributed
+#               with a attempts will get t*a/n points
+
+  include Math
+
+  def score
+    n = @total_tries
+    p = @tries.keys.length
+    t = (100*exp(-((n-1)**2)/(50.0**2))).ceil + p
+    debug "Total score: #{t}"
+    ret = Hash.new
+    @tries.each { |k, a|
+      ret[k] = [t*a/n, n_("%{count} try", "%{count} tries", a) % {:count => a}]
+    }
+    if @winner
+      debug "replacing winner score of %d with %d" % [ret[@winner].first, t]
+      tries = ret[@winner].last
+      ret[@winner] = [t, _("winner, %{tries}") % {:tries => tries}]
+    end
+    return ret.sort_by { |h| h.last.first }.reverse
+  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 "A-Z wordcache: #{@wordcache.pretty_inspect}"
+
+    @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",
+      :listener => /^[a-z]+$/
+    },
+    :english => {
+      :good => /(?:singular )?noun|verb|adj/,
+      :first => 'abacus',
+      :last => 'zuni',
+      :url => "http://www.chambers.co.uk/search.php?query=%s&title=21st",
+      :listener => /^[a-z]+$/
+    },
+    }
+
+    @autoadd_base = datafile "autoadd-"
+  end
+
+  def initialize_wordlist(params)
+    lang = params[:lang]
+    addlang = params[:addlang]
+    autoadd = @autoadd_base + addlang.to_s
+    if Wordlist.exist?(@bot, lang)
+      # wordlists are assumed to be UTF-8, but we need to strip the BOM, if present
+      words = Wordlist.get(@bot, lang)
+      if addlang and File.exist?(autoadd)
+        word += File.readlines(autoadd).map {|line| line.sub("\xef\xbb\xbf",'').strip}
+      end
+      words.uniq!
+      words.sort!
+      if(words.length >= 4) # something to guess
+        rules = {
+            :good => /^\S+$/,
+            :list => words,
+            :first => words[0],
+            :last => words[-1],
+            :addlang => addlang,
+            :addfile => autoadd,
+            :listener => /^\S+$/
+        }
+        debug "#{lang} wordlist loaded, #{rules[:list].length} lines; first word: #{rules[:first]}, last word: #{rules[:last]}"
+        return rules
+      end
+    end
+    return false
+  end
+
+  def save
+    # @registry[:games] = @games
+    @registry[:wordcache] = @wordcache
+  end
+
+  def message(m)
+    return if m.channel.nil? or m.address?
+    k = m.channel.downcase.to_s # to_sym?
+    return unless @games.key?(k)
+    return if m.params
+    word = m.plugin.downcase
+    return unless word =~ @games[k].listener
+    word_check(m, k, word)
+  end
+
+  def word_check(m, k, word)
+    # Not really safe ... what happens
+    Thread.new {
+      isit = @games[k].check(word)
+      case isit.first
+      when :bingo
+        m.reply _("%{bold}BINGO!%{bold} the word was %{underline}%{word}%{underline}. Congrats, %{bold}%{player}%{bold}!") % {:bold => Bold, :underline => Underline, :word => word, :player => m.sourcenick}
+        @games[k].total_tries += 1
+        @games[k].tries[m.source] += 1
+        @games[k].winner = m.source
+        ar = @games[k].score.inject([]) { |res, kv|
+          res.push("%s: %d (%s)" % kv.flatten)
+        }
+        m.reply _("The game was won after %{tries} tries. Scores for this game:    %{scores}") % {:tries => @games[k].total_tries, :scores => ar.join('; ')}
+        @games.delete(k)
+      when :out
+        m.reply _("%{word} is not in the range %{bold}%{range}%{bold}") % {:word => word, :bold => Bold, :range => isit.last} if m.address?
+      when :noexist
+        # bail out early if the game was won in the mean time
+        return if !@games[k] or @games[k].winner
+        m.reply _("%{word} doesn't exist or is not acceptable for the game") % {:word => word}
+        @games[k].total_failed += 1
+        @games[k].failed[m.source] += 1
+      when :in
+        # bail out early if the game was won in the mean time
+        return if !@games[k] or @games[k].winner
+        m.reply _("close, but no cigar. New range: %{bold}%{range}%{bold}") % {:bold => Bold, :range => isit.last}
+        @games[k].total_tries += 1
+        @games[k].tries[m.source] += 1
+      when :ignore
+        m.reply _("%{word} is already one of the range extrema: %{range}") % {:word => word, :range => 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.downcase.to_s
+    word = params[:word].downcase
+    if not @games.key?(k)
+      m.reply _("no A-Z game running here, can't check if %{word} is valid, can I?")
+      return
+    end
+    if word !~ /^\S+$/
+      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.downcase.to_s # to_sym?
+    if @games.key?(k)
+      m.reply _("the word in %{bold}%{range}%{bold} was:   %{bold}%{word}%{bold}") % {:bold => Bold, :range => @games[k].range, :word => @games[k].word}
+      ar = @games[k].score.inject([]) { |res, kv|
+        res.push("%s: %d (%s)" % kv.flatten)
+      }
+      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('; ')}
+      @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.downcase.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 @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
+        m.reply _("got it!")
+        @games[k] = AzGame.new(self, lang, @rules[lang], word)
+      elsif !@rules.has_key?(lang) and rules = initialize_wordlist(params)
+        word = random_pick_wordlist(rules)
+        if word.empty?
+          m.reply _("couldn't think of anything ...")
+          return
+        end
+        m.reply _("got it!")
+        @games[k] = AzGame.new(self, lang, rules, word)
+      else
+        m.reply _("I can't play A-Z in %{lang}, sorry") % {:lang => lang}
+        return
+      end
+    end
+    tr = @games[k].total_tries
+    # this message building code is rewritten to make translation easier
+    if tr == 0
+      tr_msg = ''
+    else
+      f_tr = @games[k].total_failed
+      if f_tr > 0
+        tr_msg = _(" (after %{total_tries} and %{invalid_tries})") %
+           { :total_tries => n_("%{count} try", "%{count} tries", tr) %
+                             {:count => tr},
+             :invalid_tries => n_("%{count} invalid try", "%{count} invalid tries", tr) %
+                               {:count => f_tr} }
+      else
+        tr_msg = _(" (after %{total_tries})") %
+                 { :total_tries => n_("%{count} try", "%{count} tries", tr) %
+                             {:count => tr}}
+      end
+    end
+
+    m.reply _("A-Z: %{bold}%{range}%{bold}") % {:bold => Bold, :range => @games[k].range} + tr_msg
+    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 n_("I have %{count} %{lang} word in my cache", "I have %{count} %{lang} words in my cache", wc.size) % {:count => wc.size, :lang => lang}
+    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 %{pattern}") % {:lang => lang, :pattern => pars[0]}
+      elsif matches.size > 25
+        m.reply _("more than 25 %{lang} words I know match %{pattern}, try a stricter matching") % {:lang => lang, :pattern => pars[0]}
+      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}") % {:lang => lang, :word => word}
+        return
+      end
+      if wc[word].key?(:when)
+        tr = _("%{word} learned from %{user} on %{date}") % {:word => word, :user => wc[word][:who], :date => wc[word][:when]}
+      else
+        tr = _("%{word} learned from %{user}") % {:word => word, :user => wc[word][:who]}
+      end
+      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}") % {:lang => 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
+
+  # return integer between min and max, inclusive
+  def rand_between(min, max)
+    rand(max - min + 1) + min
+  end
+
+  def random_pick_wordlist(rules, min=nil, max=nil)
+    min = rules[:first] if min.nil_or_empty?
+    max = rules[:last]  if max.nil_or_empty?
+    debug "Randomly picking word between #{min} and #{max}"
+    min_index = rules[:list].index(min)
+    max_index = rules[:list].index(max)
+    debug "Index between #{min_index} and #{max_index}"
+    index = rand_between(min_index + 1, max_index - 1)
+    debug "Index generated: #{index}"
+    word = rules[:list][index]
+    debug "Randomly picked #{word}"
+    word
+  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 = @rules[:italian]
+    p = @bot.httputil.get(rules[:wapurl] % word, :open_timeout => 60, :read_timeout => 60)
+    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 = @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(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(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 = @rules[:english]
+    p = @bot.httputil.get(rules[:url] % CGI.escape(word))
+    if not p
+      error "could not connect!"
+      return false
+    end
+    debug p
+    if p =~ /<span class="(?:hwd|srch)">#{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 = @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(rules[:url] % CGI.escape(random))
+        debug p
+        raise 'unable to get search results' if not p.match /id="fullsearchresults"/i
+        lemmi = Array.new
+        good = rules[:good]
+        # We look for a lemma composed by a single word and of length at least two
+        p.scan(/<span class="(?:hwd|srch)">(.*?)<\/span>([^\n]+?)<span class="psa">#{rules[:good]}<\/span>/i) { |prelemma, discard|
+          lemma = prelemma.downcase
+          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
+        }
+        next if lemmi.empty?
+        word = lemmi[rand(lemmi.length)]
+      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
+    langs = @rules.keys
+    wls = Wordlist.list(@bot)
+    return [
+      _("az topics: play, rules, cancel, manage, check"),
+      _("available languages: %{langs}") % { :langs => langs.join(", ") },
+      wls.empty? ? nil : _("available wordlists: %{wls}") % { :wls => wls.join(", ") },
+    ].compact.join(". ")
+
+  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] [autoadd :addlang]', :action=>'start_game', :private => false, :defaults => { :lang => nil, :addlang => nil }
+