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
15 SITE = "http://coyotecult.com/tools/randomwordgenerator.php"
18 res = Net::HTTP.post_form(URI.parse(SITE), {'numwords' => count})
19 raise _("random word generator site failed with #{res.code} - #{res.message}") unless Net::HTTPSuccess === res
20 words = res.body.scan(%r{<a.*?\?w=(.*?)\n}).flatten
22 count == 1 ? words.first : words
27 URL = "http://www.google.com/wml/search?hl=en&q=define:"
28 REGEX = %r{Web definitions for .*?<br/>(.*?)<br/>}
30 def self.define(phrase)
31 raw = Net::HTTP.get(URI.parse(URL+CGI.escape(phrase)))
32 res = raw.scan(REGEX).flatten.map { |e| e.ircify_html }
34 res.empty? ? false : res.last
42 def self.correct_word_guess(game)
43 # (2 - (amt of visible chars / word length)) * (amt of chars not visible * 5)
44 length = game.word.size
45 visible = game.visible_characters.size
46 invisible = length - visible
47 score = (2-(visible/length.to_f))*(invisible*5)
52 def self.incorrect_word_guess(game)
53 incorrect_letter(game)
56 def self.correct_letter(game)
57 ((1/Math.log(game.word.size))+1) * LETTER_VALUE
60 def self.incorrect_letter(game)
61 Math.log(game.word.size) * -LETTER_VALUE
67 attr_reader :misses, :guesses, :word, :scores
69 STAGES = [' (x_x) ', ' (;_;) ', ' (>_<) ', ' (-_-) ', ' (o_~) ', ' (^_^) ', '\(^o^)/']
70 HEALTH = STAGES.size-1
83 def visible_characters
84 # array of visible characters
85 characters.reject { |c| !@guesses.include?(c) && c =~ LETTER }
89 # array of the letters in the word
90 characters.reject { |c| c !~ LETTER }.map { |c| c.downcase }
102 # creates a string that presents the word with unknown letters shown as underscores
104 @guesses.include?(c.downcase) || c !~ LETTER ? c : "_"
108 def guess(player, str)
109 @scores[player] ||= 0
113 if str !~ /^#{LETTER}$/u
114 if word.downcase == str
115 @scores[player] += Scoring::correct_word_guess(self)
118 @scores[player] += Scoring::incorrect_word_guess(self)
121 else # single letter guess
122 return false if @guesses.include?(str) # letter has been guessed before
124 unless letters.include?(str)
125 @scores[player] += Scoring::incorrect_letter(self)
129 @scores[player] += Scoring::correct_letter(self)
139 won? || lost? || @canceled
143 (letters - @guesses).empty? || @solved
165 game = all(target).last
166 game if game && !game.over?
170 @games[target] || @games[target] = []
174 all(target).select { |game| game.over? }.last
178 all(game.channel) << game
182 define_structure :HangmanPlayerStats, :played, :score
183 define_structure :HangmanPrivateStats, :played, :score
186 def initialize(registry)
190 def save_gamestats(game)
191 target = game.channel
193 if target.is_a?(User)
194 stats = priv_reg[target]
196 stats.score += game.scores.values.last.round
197 priv_reg[target] = stats
198 elsif target.is_a?(Channel)
199 stats = chan_stats(target)
202 reg = player_stats(target)
203 game.scores.each do |user, score|
206 pstats.score += score.round
212 def player_stats(channel)
213 reg = chan_reg(channel).sub_registry('player')
214 reg.set_default(HangmanPlayerStats.new(0,0))
218 def chan_stats(channel)
219 reg = chan_reg(channel).sub_registry('stats')
224 def chan_reg(channel)
225 @registry.sub_registry(channel.downcase)
229 reg = @registry.sub_registry('private')
230 reg.set_default(HangmanPrivateStats.new(0,0))
235 class HangmanPlugin < Plugin
238 @games = GameManager.new
239 @stats = StatsHandler.new(@registry)
243 def help(plugin, topic="")
246 return [_("hangman play on <channel> with word <word> => use in private chat with the bot to start a game with custom word\n"),
247 _("hangman play random [with [max|min] length [<|>|== <length>]] => hangman with a random word from %{site}\n"),
248 _("hangman play with wordlist <wordlist> => hangman with random word from <wordlist>")].join % { :site => RandomWord::SITE }
250 return _("hangman stop => quits the current game")
252 return _("hangman define => seeks a definition for the previous answer using google")
254 return _("hangman game plugin - topics: play, stop, define")
260 params[:word].join(" ")
261 elsif params[:wordlist]
263 wordlist = Wordlist.get(params[:wordlist].join("/"), :spaces => true)
265 raise _("no such wordlist")
268 wordlist[rand(wordlist.size)]
269 else # getting a random word
270 words = RandomWord::get(100)
272 if adj = params[:adj]
273 words = words.sort_by { |e| e.size }
280 elsif params[:relation] && params[:size]
281 words = words.select { |w| w.size.send(params[:relation], params[:size].to_i) }
286 m.reply _("suitable word not found in the set")
297 word = get_word(params) || return
303 if params[:channel] || m.public?
304 target = if m.public?
307 @bot.server.channel(params[:channel])
310 # is the bot on the channel?
311 unless @bot.myself.channels.include?(target)
312 m.reply _("i'm not on that channel")
316 if @games.current(target)
317 m.reply _("there's already a hangman game in progress on the channel")
321 @bot.say target, _("%{nick} has started a hangman -- join the fun!") % {
328 game = Hangman.new(word)
330 class << game = Hangman.new(word)
331 attr_accessor :channel
334 game.channel = target
337 @settings[target] = params
339 @bot.say target, game_status(@games.current(target))
344 if game = @games.current(target)
345 @bot.say target, _("oh well, the answer would've been %{answer}") % {
346 :answer => Bold + game.word + Bold
350 @stats.save_gamestats(game)
352 @bot.say target, _("no ongoing game")
359 if game = @games.current(target)
360 return unless m.message =~ /^[^\W0-9_]$/u || m.message =~ prepare_guess_regex(game)
362 if game.guess(m.source, m.message)
363 m.reply game_status(game)
369 sentence = if game.won?
372 _("you've killed the poor guy :(")
375 again = _("go %{b}again%{b}?") % { :b => Bold }
378 game.scores.each do |user, score|
379 str = "#{user.nick}: "
381 Irc.color(:green)+'+'
386 str << score.round.to_s
392 m.reply _("%{sentence} %{again} %{scores}") % {
393 :sentence => sentence, :again => again, :scores => scores.join(' ')
397 m.reply _("wondering what that means? try ´%{prefix}hangman define´") % {
398 :prefix => @bot.config['core.address_prefix'].first
402 @stats.save_gamestats(game)
404 elsif @settings[target] && m.message =~ /^(?:again|more!?$)/i
405 start(m, @settings[target])
409 def prepare_guess_regex(game)
410 Regexp.new("^#{game.characters.map { |c|
411 game.guesses.include?(c) || c !~ Hangman::LETTER ? c : '[^\W0-9_]'
415 def game_status(game)
416 str = "%{word} %{face}" % {
417 :word => game.over? ? "#{Bold}#{game.word}#{Bold}" : game.to_s,
419 :misses => game.misses.map { |e| e.upcase }.join(" ")
422 str << " %{misses}" % {
423 :misses => game.misses.map { |e| e.upcase }.join(" ")
424 } unless game.misses.empty?
433 stats = if m.private?
434 @stats.priv_reg[target]
436 @stats.player_stats(target)[m.source]
439 unless stats.played.zero?
440 m.reply _("you got %{score} points after %{games} games") % {
441 :score => stats.score.round,
442 :games => stats.played
445 m.reply _("you haven't played hangman, how about playing it right now? :)")
448 return unless m.public?
451 stats = @stats.player_stats(target)[nick]
453 unless stats.played.zero?
454 m.reply _("%{nick} has %{score} points after %{games} games") % {
456 :score => stats.score.round,
457 :games => stats.played
460 m.reply _("%{nick} hasn't played hangman :(") % {
469 stats = @stats.chan_stats(target)
472 m.reply _("%{games} games have been played on %{channel}") % {
473 :games => stats['played'],
474 :channel => target.to_s
481 def define(m, params)
482 if game = @games.previous(m.replyto)
483 if res = Google.define(game.word)
484 m.reply "#{Bold}#{game.word}#{Bold} -- #{res}"
486 m.reply _("looks like google has no definition for %{word}") % { :word => game.word }
489 m.reply _("no hangman game was played here recently, what do you want me to define?")
494 plugin = HangmanPlugin.new
495 plugin.map "hangman [play] with wordlist *wordlist", :action => 'start'
496 plugin.map "hangman [play] on :channel with word *word", :action => 'start'
497 plugin.map "hangman [play] [random] [with [:adj] length [:relation :size]]",
499 :requirements => { :adj => /min|max/, :relation => /<|<=|>=|>|==/, :size => /\d+/ }
501 plugin.map "hangman stop", :action => 'stop'
503 plugin.map "hangman score [:nick]", :action => 'score'
504 plugin.map "hangman stats", :action => 'stats'
505 plugin.map "hangman define", :action => 'define'