]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/games/hangman.rb
hangman plugin: various improvements including support for wordlists
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / games / hangman.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Hangman Plugin
5 #
6 # Author:: Raine Virta <rane@kapsi.fi>
7 # Copyright:: (C) 2009 Raine Virta
8 # License:: GPL v2
9 #
10 # Description:: Hangman game for rbot
11 #
12 # TODO:: scoring and stats
13 #        some sort of turn-basedness, maybe
14
15 module RandomWord
16   SITE = "http://coyotecult.com/tools/randomwordgenerator.php"
17
18   def self.get(count=1)
19     res = Net::HTTP.post_form(URI.parse(SITE), {'numwords' => count})
20     words = res.body.scan(%r{<a.*?\?w=(.*?)\n}).flatten
21
22     count == 1 ? words.first : words
23   end
24 end
25
26 class Hangman
27   attr_reader :misses, :guesses, :word, :letters
28
29   STAGES = [' (x_x) ', ' (;_;) ', ' (>_<) ', ' (-_-) ', ' (o_~) ', ' (^_^) ', '\(^o^)/']
30   HEALTH = STAGES.size-1
31   LETTER = /[^\W0-9_]/u
32
33   def initialize(word, channel=nil)
34     @word    = word
35     @guesses = []
36     @misses  = []
37     @health  = HEALTH
38     @solved  = false
39   end
40
41   def letters
42     # array of the letters in the word
43     @word.split(//u).reject { |c| c !~ LETTER  }.map { |c| c.downcase }
44   end
45
46   def face
47     STAGES[@health]
48   end
49
50   def to_s
51     # creates a string that presents the word with unknown letters shown as underscores
52     @word.split(//).map { |c|
53       @guesses.include?(c.downcase) || c !~ LETTER  ? c : "_"
54     }.join
55   end
56
57   def guess(str)
58     str.downcase!
59
60     # full word guess
61     if str !~ /^#{LETTER}$/u
62       word.downcase == str ? @solved = true : punish
63     else # single letter guess
64       return false if @guesses.include?(str) # letter has been guessed before
65
66       unless letters.include?(str)
67         @misses << str
68         punish
69       end
70
71       @guesses << str
72     end
73   end
74
75   def over?
76     won? || lost?
77   end
78
79   def won?
80     (letters - @guesses).empty? || @solved
81   end
82
83   def lost?
84     @health.zero?
85   end
86
87   def punish
88     @health -= 1
89   end
90 end
91
92 class HangmanPlugin < Plugin
93   def initialize
94     super
95     @games = {}
96     @settings = {}
97   end
98
99   def help(plugin, topic="")
100     case topic
101     when ""
102       return "hangman game plugin - topics: play, stop"
103     when "play"
104       return "hangman play on <channel> with word <word> => use in private chat with the bot to start a game with custom word\n"+
105              "hangman play random [with [max|min] length [<|>|== <length>]] => hangman with a random word from #{RandomWord::SITE}\n"+
106              "hangman play with wordlist <wordlist> => hangman with random word from <wordlist>"
107     when "stop"
108       return "hangman stop => quits the current game"
109     end
110   end
111
112   def get_word(params)
113     if params[:word]
114       params[:word].join(" ")
115     elsif params[:wordlist]
116       begin
117         wordlist = Wordlist.get(params[:wordlist].join("/"), :spaces => true)
118       rescue
119         raise "no such wordlist"
120       end
121
122       wordlist[rand(wordlist.size)]
123     else # getting a random word
124       words = RandomWord::get(100)
125
126       if adj = params[:adj]
127         words = words.sort_by { |e| e.size }
128
129         if adj == "max"
130           words.last
131         else
132           words.first
133         end
134       elsif params[:relation] && params[:size]
135         words = words.select { |w| w.size.send(params[:relation], params[:size].to_i) }
136
137         unless words.empty?
138           words.first
139         else
140           m.reply "suitable word not found in the set"
141           nil
142         end
143       else
144         words.first
145       end
146     end
147   end
148
149   def start(m, params)
150     begin
151       word = get_word(params) || return
152     rescue => e
153       m.reply e.message
154       return
155     end
156
157     if (params[:channel] || m.public?)
158       target = if m.public?
159         m.channel.to_s
160       else
161         params[:channel]
162       end
163
164       # is the bot on the channel?
165       unless @bot.server.channels.names.include?(target.to_s)
166         m.reply "i'm not on that channel"
167         return
168       end
169
170       if @games.has_key?(target)
171         m.reply "there's already a hangman game in progress on the channel"
172         return
173       end
174
175       @bot.say target, "#{m.source} has started a hangman -- join the fun!"
176     else
177       target = m.source.to_s
178     end
179
180     @games[target]    = Hangman.new(word)
181     @settings[target] = params
182
183     @bot.say target, game_status(@games[target])
184   end
185
186   def stop(m, params)
187     source = if m.public?
188       m.channel
189     else
190       m.source
191     end
192
193     if @games.has_key?(source.to_s)
194       @bot.say source, "oh well, the answer would've been #{Bold}#{@games[source.to_s].word}#{Bold}"
195       @games.delete(source.to_s)
196     end
197   end
198
199   def message(m)
200     source = if m.public?
201       m.channel.to_s
202     else
203       m.source.to_s
204     end
205
206     if game = @games[source]
207       if m.message =~ /^[^\W0-9_]$/u || m.message =~ prepare_guess_regex(game)
208         return unless game.guess(m.message)
209
210         m.reply game_status(game)
211       end
212
213       if game.over?
214         if game.won?
215           str = "you nailed it!"
216         elsif game.lost?
217           str = "you've killed the poor guy :("
218         end
219
220         m.reply "#{str} go #{Bold}again#{Bold}?"
221
222         @games.delete(source)
223       end
224     elsif @settings[source] && m.message =~ /^(?:again|more!?$)/i
225       start(m, @settings[source])
226     end
227   end
228
229   def prepare_guess_regex(game)
230     Regexp.new("^#{game.word.split(//).map { |c|
231       game.guesses.include?(c) || c !~ Hangman::LETTER ? c : '[^\W0-9_]'
232     }.join("")}$")
233   end
234
235   def game_status(game)
236     "%{word} %{face} %{misses}" % {
237       :word   => game.over? ? "#{Bold}#{game.word}#{Bold}" : game.to_s,
238       :face   => game.face,
239       :misses => game.misses.map { |e| e.upcase }.join(" ")
240     }
241   end
242 end
243
244 plugin = HangmanPlugin.new
245 plugin.map "hangman [play] with wordlist *wordlist", :action => 'start'
246 plugin.map "hangman [play] on :channel with word *word", :action => 'start'
247 plugin.map "hangman [play] [random] [with [:adj] length [:relation :size]]",
248   :action => 'start',
249   :requirements => { :adj => /min|max/, :relation => /<|<=|>=|>|==/, :size => /\d+/ }
250
251 plugin.map "hangman stop", :action => 'stop'
252