]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - data/rbot/plugins/games/hangman.rb
plugin(hangman): fixes word generator closes #9
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / games / hangman.rb
index 423cfe0c13e906ba131e4c74144f52cbf2c143f2..0177a3481177f0699c315fe2e5aec47ab36c3d86 100644 (file)
@@ -9,38 +9,84 @@
 #
 # Description:: Hangman game for rbot
 #
-# TODO:: scoring and stats
-#        some sort of turn-basedness, maybe
+# TODO:: some sort of turn-basedness, maybe
+
+# https://www.wordgenerator.net/application/p.php?type=2&id=dictionary_words&spaceflag=false
 
 module RandomWord
-  SITE = "http://coyotecult.com/tools/randomwordgenerator.php"
+  SITE = 'https://www.wordgenerator.net/random-word-generator.php'
+  BASE_URL = 'https://www.wordgenerator.net/application/p.php'
+
+  # we could allow to specify by word types:  (defaults to all)
+  TYPES = {
+    all: 'dictionary_words',
+    noun: 'nouns',
+    adj: 'adjectives',
+    verb: 'action_verbs'
+  }
+
+  def self.get(bot, type)
+    bot.httputil.get("#{BASE_URL}?type=1&id=#{TYPES[type]}&spaceflag=false", cache: false).split(',')
+  end
+end
+
+class Hangman
+  LETTER_VALUE = 5
+
+  module Scoring
+    def self.correct_word_guess(game)
+      # (2 - (amt of visible chars / word length)) * (amt of chars not visible * 5)
+      length    = game.word.size
+      visible   = game.visible_characters.size
+      invisible = length - visible
+      score     = (2-(visible/length.to_f))*(invisible*5)
+      score    *= 1.5
+      score.round
+    end
 
-  def self.get(count=1)
-    res = Net::HTTP.post_form(URI.parse(SITE), {'numwords' => count})
-    words = res.body.scan(%r{<a.*?\?w=(.*?)\n}).flatten
+    def self.incorrect_word_guess(game)
+      incorrect_letter(game)
+    end
+
+    def self.correct_letter(game)
+      ((1/Math.log(game.word.size))+1) * LETTER_VALUE
+    end
 
-    count == 1 ? words.first : words
+    def self.incorrect_letter(game)
+      Math.log(game.word.size) * -LETTER_VALUE
+    end
   end
 end
 
 class Hangman
-  attr_reader :misses, :guesses, :word, :letters
+  attr_reader :misses, :guesses, :word, :scores
 
   STAGES = [' (x_x) ', ' (;_;) ', ' (>_<) ', ' (-_-) ', ' (o_~) ', ' (^_^) ', '\(^o^)/']
   HEALTH = STAGES.size-1
   LETTER = /[^\W0-9_]/u
 
-  def initialize(word, channel=nil)
-    @word    = word
-    @guesses = []
-    @misses  = []
-    @health  = HEALTH
-    @solved  = false
+  def initialize(word)
+    @word     = word
+    @guesses  = []
+    @misses   = []
+    @health   = HEALTH
+    @canceled = false
+    @solved   = false
+    @scores   = {}
+  end
+
+  def visible_characters
+    # array of visible characters
+    characters.reject { |c| !@guesses.include?(c) && c =~ LETTER }
   end
 
   def letters
     # array of the letters in the word
-    @word.split(//u).reject { |c| c !~ LETTER  }.map { |c| c.downcase }
+    characters.reject { |c| c !~ LETTER  }.map { |c| c.downcase }
+  end
+
+  def characters
+    @word.split(//u)
   end
 
   def face
@@ -49,31 +95,43 @@ class Hangman
 
   def to_s
     # creates a string that presents the word with unknown letters shown as underscores
-    @word.split(//).map { |c|
+    characters.map { |c|
       @guesses.include?(c.downcase) || c !~ LETTER  ? c : "_"
     }.join
   end
 
-  def guess(str)
-    str.downcase!
+  def guess(player, str)
+    @scores[player] ||= 0
 
+    str.downcase!
     # full word guess
     if str !~ /^#{LETTER}$/u
-      word.downcase == str ? @solved = true : punish
+      if word.downcase == str
+        @scores[player] += Scoring::correct_word_guess(self)
+        @solved = true
+      else
+        @scores[player] += Scoring::incorrect_word_guess(self)
+        punish
+      end
     else # single letter guess
       return false if @guesses.include?(str) # letter has been guessed before
 
       unless letters.include?(str)
+        @scores[player] += Scoring::incorrect_letter(self)
         @misses << str
         punish
+      else
+        @scores[player] += Scoring::correct_letter(self)
       end
 
       @guesses << str
     end
+
+    return true
   end
 
   def over?
-    won? || lost?
+    won? || lost? || @canceled
   end
 
   def won?
@@ -87,25 +145,106 @@ class Hangman
   def punish
     @health -= 1
   end
+
+  def cancel
+    @canceled = true
+  end
+end
+
+class GameManager
+  def initialize
+    @games = {}
+  end
+
+  def current(target)
+    game = all(target).last
+    game if game && !game.over?
+  end
+
+  def all(target)
+    @games[target] || @games[target] = []
+  end
+
+  def previous(target)
+    all(target).select { |game| game.over? }.last
+  end
+
+  def new(game)
+    all(game.channel) << game
+  end
+end
+
+define_structure :HangmanPlayerStats, :played, :score
+define_structure :HangmanPrivateStats, :played, :score
+
+class StatsHandler
+  def initialize(registry)
+    @registry = registry
+  end
+
+  def save_gamestats(game)
+    target = game.channel
+
+    if target.is_a?(User)
+      stats = priv_reg[target]
+      stats.played += 1
+      stats.score  += game.scores.values.last.round
+      priv_reg[target] = stats
+    elsif target.is_a?(Channel)
+      stats = chan_stats(target)
+      stats['played'] += 1
+
+      reg = player_stats(target)
+      game.scores.each do |user, score|
+        pstats = reg[user]
+        pstats.played += 1
+        pstats.score  += score.round
+        reg[user] = pstats
+      end
+    end
+  end
+
+  def player_stats(channel)
+    reg = chan_reg(channel).sub_registry('player')
+    reg.set_default(HangmanPlayerStats.new(0,0))
+    reg
+  end
+
+  def chan_stats(channel)
+    reg = chan_reg(channel).sub_registry('stats')
+    reg.set_default(0)
+    reg
+  end
+
+  def chan_reg(channel)
+    @registry.sub_registry(channel.downcase)
+  end
+
+  def priv_reg
+    reg = @registry.sub_registry('private')
+    reg.set_default(HangmanPrivateStats.new(0,0))
+    reg
+  end
 end
 
 class HangmanPlugin < Plugin
   def initialize
     super
-    @games = {}
+    @games = GameManager.new
+    @stats = StatsHandler.new(@registry)
     @settings = {}
   end
 
   def help(plugin, topic="")
     case topic
-    when ""
-      return "hangman game plugin - topics: play, stop"
     when "play"
-      return "hangman play on <channel> with word <word> => use in private chat with the bot to start a game with custom word\n"+
-             "hangman play random [with [max|min] length [<|>|== <length>]] => hangman with a random word from #{RandomWord::SITE}\n"+
-             "hangman play with wordlist <wordlist> => hangman with random word from <wordlist>"
+      return [_("hangman play on <channel> with word <word> => use in private chat with the bot to start a game with custom word\n"),
+              _("hangman play random [with [max|min] length [<|>|== <length>]] => hangman with a random word from %{site}\n"),
+              _("hangman play with wordlist <wordlist> => hangman with random word from <wordlist>")].join % { :site => RandomWord::SITE }
     when "stop"
-      return "hangman stop => quits the current game"
+      return _("hangman stop => quits the current game")
+    else
+      return _("hangman game plugin - topics: play, stop")
     end
   end
 
@@ -116,12 +255,12 @@ class HangmanPlugin < Plugin
       begin
         wordlist = Wordlist.get(params[:wordlist].join("/"), :spaces => true)
       rescue
-        raise "no such wordlist"
+        raise _("no such wordlist")
       end
 
       wordlist[rand(wordlist.size)]
     else # getting a random word
-      words = RandomWord::get(100)
+      words = RandomWord::get(@bot, :all)
 
       if adj = params[:adj]
         words = words.sort_by { |e| e.size }
@@ -137,7 +276,7 @@ class HangmanPlugin < Plugin
         unless words.empty?
           words.first
         else
-          m.reply "suitable word not found in the set"
+          m.reply _("suitable word not found in the set")
           nil
         end
       else
@@ -154,90 +293,182 @@ class HangmanPlugin < Plugin
       return
     end
 
-    if (params[:channel] || m.public?)
+    if params[:channel] || m.public?
       target = if m.public?
-        m.channel.to_s
+        m.channel
       else
-        params[:channel]
+        @bot.server.channel(params[:channel])
       end
 
       # is the bot on the channel?
-      unless @bot.server.channels.names.include?(target.to_s)
-        m.reply "i'm not on that channel"
+      unless @bot.myself.channels.include?(target)
+        m.reply _("i'm not on that channel")
         return
       end
 
-      if @games.has_key?(target)
-        m.reply "there's already a hangman game in progress on the channel"
+      if @games.current(target)
+        m.reply _("there's already a hangman game in progress on the channel")
         return
       end
 
-      @bot.say target, "#{m.source} has started a hangman -- join the fun!"
+      @bot.say target, _("%{nick} has started a hangman -- join the fun!") % {
+        :nick => m.source
+      }
     else
-      target = m.source.to_s
+      target = m.source
+    end
+
+    game = Hangman.new(word)
+
+    class << game = Hangman.new(word)
+      attr_accessor :channel
     end
 
-    @games[target]    = Hangman.new(word)
+    game.channel = target
+
+    @games.new(game)
     @settings[target] = params
 
-    @bot.say target, game_status(@games[target])
+    @bot.say target, game_status(@games.current(target))
   end
 
   def stop(m, params)
-    source = if m.public?
-      m.channel
+    target = m.replyto
+    if game = @games.current(target)
+      @bot.say target, _("oh well, the answer would've been %{answer}") % {
+        :answer => Bold + game.word + Bold
+      }
+
+      game.cancel
+      @stats.save_gamestats(game)
     else
-      m.source
-    end
-
-    if @games.has_key?(source.to_s)
-      @bot.say source, "oh well, the answer would've been #{Bold}#{@games[source.to_s].word}#{Bold}"
-      @games.delete(source.to_s)
+      @bot.say target, _("no ongoing game")
     end
   end
 
   def message(m)
-    source = if m.public?
-      m.channel.to_s
-    else
-      m.source.to_s
-    end
+    target = m.replyto
 
-    if game = @games[source]
-      if m.message =~ /^[^\W0-9_]$/u || m.message =~ prepare_guess_regex(game)
-        return unless game.guess(m.message)
+    if game = @games.current(target)
+      return unless m.message =~ /^[^\W0-9_]$/u || m.message =~ prepare_guess_regex(game)
 
+      if game.guess(m.source, m.message)
         m.reply game_status(game)
+      else
+        return
       end
 
       if game.over?
-        if game.won?
-          str = "you nailed it!"
+        sentence = if game.won?
+          _("you nailed it!")
         elsif game.lost?
-          str = "you've killed the poor guy :("
+          _("you've killed the poor guy :(")
         end
 
-        m.reply "#{str} go #{Bold}again#{Bold}?", :nick => true
+        again = _("go %{b}again%{b}?") % { :b => Bold }
+
+        scores = []
+        game.scores.each do |user, score|
+          str = "#{user.nick}: "
+          str << if score > 0
+            Irc.color(:green)+'+'
+          elsif score < 0
+            Irc.color(:brown)
+          end.to_s
+
+          str << score.round.to_s
+          str << Irc.color
+
+          scores << str
+        end
+
+        m.reply _("%{sentence} %{again} %{scores}") % {
+          :sentence => sentence, :again => again, :scores => scores.join(' ')
+        }, :nick => true
+
+        if rand(5).zero?
+          m.reply _("wondering what that means? try ´%{prefix}oxford <word>´") % {
+            :prefix => @bot.config['core.address_prefix'].first
+          }
+        end
 
-        @games.delete(source)
+        @stats.save_gamestats(game)
       end
-    elsif @settings[source] && m.message =~ /^(?:again|more!?$)/i
-      start(m, @settings[source])
+    elsif @settings[target] && m.message =~ /^(?:again|more!?$)/i
+      start(m, @settings[target])
     end
   end
 
   def prepare_guess_regex(game)
-    Regexp.new("^#{game.word.split(//).map { |c|
+    Regexp.new("^#{game.characters.map { |c|
       game.guesses.include?(c) || c !~ Hangman::LETTER ? c : '[^\W0-9_]'
     }.join("")}$")
   end
 
   def game_status(game)
-    "%{word} %{face} %{misses}" % {
+    str = "%{word} %{face}" % {
       :word   => game.over? ? "#{Bold}#{game.word}#{Bold}" : game.to_s,
       :face   => game.face,
       :misses => game.misses.map { |e| e.upcase }.join(" ")
     }
+
+    str << " %{misses}" % {
+      :misses => game.misses.map { |e| e.upcase }.join(" ")
+    } unless game.misses.empty?
+
+    str
+  end
+
+  def score(m, params)
+    target = m.replyto
+
+    unless params[:nick]
+      stats = if m.private?
+        @stats.priv_reg[target]
+      else
+        @stats.player_stats(target)[m.source]
+      end
+
+      unless stats.played.zero?
+        m.reply _("you got %{score} points after %{games} games") % {
+          :score => stats.score.round,
+          :games => stats.played
+        }
+      else
+        m.reply _("you haven't played hangman, how about playing it right now? :)")
+      end
+    else
+      return unless m.public?
+
+      nick = params[:nick]
+      stats = @stats.player_stats(target)[nick]
+
+      unless stats.played.zero?
+        m.reply _("%{nick} has %{score} points after %{games} games") % {
+          :nick  => nick,
+          :score => stats.score.round,
+          :games => stats.played
+        }
+      else
+        m.reply _("%{nick} hasn't played hangman :(") % {
+          :nick => nick
+        }
+      end
+    end
+  end
+
+  def stats(m, params)
+    target = m.replyto
+    stats  = @stats.chan_stats(target)
+
+    if m.public?
+      m.reply _("%{games} games have been played on %{channel}") % {
+        :games   => stats['played'],
+        :channel => target.to_s
+      }
+    else
+      score(m, params)
+    end
   end
 end
 
@@ -250,3 +481,5 @@ plugin.map "hangman [play] [random] [with [:adj] length [:relation :size]]",
 
 plugin.map "hangman stop", :action => 'stop'
 
+plugin.map "hangman score [:nick]", :action => 'score'
+plugin.map "hangman stats", :action => 'stats'