X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=data%2Frbot%2Fplugins%2Fgames%2Fhangman.rb;h=890f821acfe1b123a71df7423bf42a2b66dca522;hb=ff6d7e5d043da99b2da36f628d52b7b5df47119d;hp=423cfe0c13e906ba131e4c74144f52cbf2c143f2;hpb=106501b21d8dd11ae976d646c0dcc0bc856a392c;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/data/rbot/plugins/games/hangman.rb b/data/rbot/plugins/games/hangman.rb index 423cfe0c..890f821a 100644 --- a/data/rbot/plugins/games/hangman.rb +++ b/data/rbot/plugins/games/hangman.rb @@ -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{_<) ', ' (-_-) ', ' (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 with word => use in private chat with the bot to start a game with custom word\n"+ - "hangman play random [with [max|min] length [<|>|== ]] => hangman with a random word from #{RandomWord::SITE}\n"+ - "hangman play with wordlist => hangman with random word from " + return [_("hangman play on with word => use in private chat with the bot to start a game with custom word\n"), + _("hangman play random [with [max|min] length [<|>|== ]] => hangman with a random word from %{site}\n"), + _("hangman play with wordlist => hangman with random word from ")].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 @@ -114,14 +253,14 @@ class HangmanPlugin < Plugin params[:word].join(" ") elsif params[:wordlist] begin - wordlist = Wordlist.get(params[:wordlist].join("/"), :spaces => true) + wordlist = Wordlist.get(@bot, 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 ´") % { + :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'