4 # :title: Hangman Plugin
6 # Author:: Raine Virta <rane@kapsi.fi>
7 # Copyright:: (C) 2009 Raine Virta
10 # Description:: Hangman game for rbot
12 # TODO:: some sort of turn-basedness, maybe
14 # https://www.wordgenerator.net/application/p.php?type=2&id=dictionary_words&spaceflag=false
17 SITE = 'https://www.wordgenerator.net/random-word-generator.php'
18 BASE_URL = 'https://www.wordgenerator.net/application/p.php'
20 # we could allow to specify by word types: (defaults to all)
22 all: 'dictionary_words',
28 def self.get(bot, type)
29 bot.httputil.get("#{BASE_URL}?type=1&id=#{TYPES[type]}&spaceflag=false", cache: false).split(',')
37 def self.correct_word_guess(game)
38 # (2 - (amt of visible chars / word length)) * (amt of chars not visible * 5)
39 length = game.word.size
40 visible = game.visible_characters.size
41 invisible = length - visible
42 score = (2-(visible/length.to_f))*(invisible*5)
47 def self.incorrect_word_guess(game)
48 incorrect_letter(game)
51 def self.correct_letter(game)
52 ((1/Math.log(game.word.size))+1) * LETTER_VALUE
55 def self.incorrect_letter(game)
56 Math.log(game.word.size) * -LETTER_VALUE
62 attr_reader :misses, :guesses, :word, :scores
64 STAGES = [' (x_x) ', ' (;_;) ', ' (>_<) ', ' (-_-) ', ' (o_~) ', ' (^_^) ', '\(^o^)/']
65 HEALTH = STAGES.size-1
78 def visible_characters
79 # array of visible characters
80 characters.reject { |c| !@guesses.include?(c) && c =~ LETTER }
84 # array of the letters in the word
85 characters.reject { |c| c !~ LETTER }.map { |c| c.downcase }
97 # creates a string that presents the word with unknown letters shown as underscores
99 @guesses.include?(c.downcase) || c !~ LETTER ? c : "_"
103 def guess(player, str)
104 @scores[player] ||= 0
108 if str !~ /^#{LETTER}$/u
109 if word.downcase == str
110 @scores[player] += Scoring::correct_word_guess(self)
113 @scores[player] += Scoring::incorrect_word_guess(self)
116 else # single letter guess
117 return false if @guesses.include?(str) # letter has been guessed before
119 unless letters.include?(str)
120 @scores[player] += Scoring::incorrect_letter(self)
124 @scores[player] += Scoring::correct_letter(self)
134 won? || lost? || @canceled
138 (letters - @guesses).empty? || @solved
160 game = all(target).last
161 game if game && !game.over?
165 @games[target] || @games[target] = []
169 all(target).select { |game| game.over? }.last
173 all(game.channel) << game
177 define_structure :HangmanPlayerStats, :played, :score
178 define_structure :HangmanPrivateStats, :played, :score
181 def initialize(registry)
185 def save_gamestats(game)
186 target = game.channel
188 if target.is_a?(User)
189 stats = priv_reg[target]
191 stats.score += game.scores.values.last.round
192 priv_reg[target] = stats
193 elsif target.is_a?(Channel)
194 stats = chan_stats(target)
197 reg = player_stats(target)
198 game.scores.each do |user, score|
201 pstats.score += score.round
207 def player_stats(channel)
208 reg = chan_reg(channel).sub_registry('player')
209 reg.set_default(HangmanPlayerStats.new(0,0))
213 def chan_stats(channel)
214 reg = chan_reg(channel).sub_registry('stats')
219 def chan_reg(channel)
220 @registry.sub_registry(channel.downcase)
224 reg = @registry.sub_registry('private')
225 reg.set_default(HangmanPrivateStats.new(0,0))
230 class HangmanPlugin < Plugin
233 @games = GameManager.new
234 @stats = StatsHandler.new(@registry)
238 def help(plugin, topic="")
241 return [_("hangman play on <channel> with word <word> => use in private chat with the bot to start a game with custom word\n"),
242 _("hangman play random [with [max|min] length [<|>|== <length>]] => hangman with a random word from %{site}\n"),
243 _("hangman play with wordlist <wordlist> => hangman with random word from <wordlist>")].join % { :site => RandomWord::SITE }
245 return _("hangman stop => quits the current game")
247 return _("hangman game plugin - topics: play, stop")
253 params[:word].join(" ")
254 elsif params[:wordlist]
256 wordlist = Wordlist.get(@bot, params[:wordlist].join("/"), :spaces => true)
258 raise _("no such wordlist")
261 wordlist[rand(wordlist.size)]
262 else # getting a random word
263 words = RandomWord::get(@bot, :all)
265 if adj = params[:adj]
266 words = words.sort_by { |e| e.size }
273 elsif params[:relation] && params[:size]
274 words = words.select { |w| w.size.send(params[:relation], params[:size].to_i) }
279 m.reply _("suitable word not found in the set")
290 word = get_word(params) || return
296 if params[:channel] || m.public?
297 target = if m.public?
300 @bot.server.channel(params[:channel])
303 # is the bot on the channel?
304 unless @bot.myself.channels.include?(target)
305 m.reply _("i'm not on that channel")
309 if @games.current(target)
310 m.reply _("there's already a hangman game in progress on the channel")
314 @bot.say target, _("%{nick} has started a hangman -- join the fun!") % {
321 game = Hangman.new(word)
323 class << game = Hangman.new(word)
324 attr_accessor :channel
327 game.channel = target
330 @settings[target] = params
332 @bot.say target, game_status(@games.current(target))
337 if game = @games.current(target)
338 @bot.say target, _("oh well, the answer would've been %{answer}") % {
339 :answer => Bold + game.word + Bold
343 @stats.save_gamestats(game)
345 @bot.say target, _("no ongoing game")
352 if game = @games.current(target)
353 return unless m.message =~ /^[^\W0-9_]$/u || m.message =~ prepare_guess_regex(game)
355 if game.guess(m.source, m.message)
356 m.reply game_status(game)
362 sentence = if game.won?
365 _("you've killed the poor guy :(")
368 again = _("go %{b}again%{b}?") % { :b => Bold }
371 game.scores.each do |user, score|
372 str = "#{user.nick}: "
374 Irc.color(:green)+'+'
379 str << score.round.to_s
385 m.reply _("%{sentence} %{again} %{scores}") % {
386 :sentence => sentence, :again => again, :scores => scores.join(' ')
390 m.reply _("wondering what that means? try ´%{prefix}oxford <word>´") % {
391 :prefix => @bot.config['core.address_prefix'].first
395 @stats.save_gamestats(game)
397 elsif @settings[target] && m.message =~ /^(?:again|more!?$)/i
398 start(m, @settings[target])
402 def prepare_guess_regex(game)
403 Regexp.new("^#{game.characters.map { |c|
404 game.guesses.include?(c) || c !~ Hangman::LETTER ? c : '[^\W0-9_]'
408 def game_status(game)
409 str = "%{word} %{face}" % {
410 :word => game.over? ? "#{Bold}#{game.word}#{Bold}" : game.to_s,
412 :misses => game.misses.map { |e| e.upcase }.join(" ")
415 str << " %{misses}" % {
416 :misses => game.misses.map { |e| e.upcase }.join(" ")
417 } unless game.misses.empty?
426 stats = if m.private?
427 @stats.priv_reg[target]
429 @stats.player_stats(target)[m.source]
432 unless stats.played.zero?
433 m.reply _("you got %{score} points after %{games} games") % {
434 :score => stats.score.round,
435 :games => stats.played
438 m.reply _("you haven't played hangman, how about playing it right now? :)")
441 return unless m.public?
444 stats = @stats.player_stats(target)[nick]
446 unless stats.played.zero?
447 m.reply _("%{nick} has %{score} points after %{games} games") % {
449 :score => stats.score.round,
450 :games => stats.played
453 m.reply _("%{nick} hasn't played hangman :(") % {
462 stats = @stats.chan_stats(target)
465 m.reply _("%{games} games have been played on %{channel}") % {
466 :games => stats['played'],
467 :channel => target.to_s
475 plugin = HangmanPlugin.new
476 plugin.map "hangman [play] with wordlist *wordlist", :action => 'start'
477 plugin.map "hangman [play] on :channel with word *word", :action => 'start'
478 plugin.map "hangman [play] [random] [with [:adj] length [:relation :size]]",
480 :requirements => { :adj => /min|max/, :relation => /<|<=|>=|>|==/, :size => /\d+/ }
482 plugin.map "hangman stop", :action => 'stop'
484 plugin.map "hangman score [:nick]", :action => 'score'
485 plugin.map "hangman stats", :action => 'stats'