hangbot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f969e132a38464aae0acc5df407ea42e65211774
4
+ data.tar.gz: e7a1c16248085b07d4a54d705703babf5eb75c58
5
+ SHA512:
6
+ metadata.gz: 98296a0e5329e0231a62d1d57caa8f97bcc0e5f7ce01d7d3954902b120fcb3e50e2b2722608b5adf650841db63b9f4f35928b794f10742f5a9514c0e08d5a994
7
+ data.tar.gz: c8edfab2fb1eb49984bca434be6f0043e4da37e76a0097219371e200223df64ed81a17b4ca388b74ae551d6e46d85da87eeb76bfa647afbdc86c6903e1450bd7
@@ -0,0 +1 @@
1
+ hangbot.yaml
@@ -0,0 +1 @@
1
+ 2.0.0-p451
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,63 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ hangbot (0.1.0)
5
+ configliere (~> 0.4)
6
+ httparty (~> 0.13)
7
+ json (~> 1.8)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ celluloid (0.15.2)
13
+ timers (~> 1.1.0)
14
+ coderay (1.1.0)
15
+ configliere (0.4.18)
16
+ highline (>= 1.5.2)
17
+ multi_json (>= 1.1)
18
+ ffi (1.9.3)
19
+ formatador (0.2.5)
20
+ guard (2.6.1)
21
+ formatador (>= 0.2.4)
22
+ listen (~> 2.7)
23
+ lumberjack (~> 1.0)
24
+ pry (>= 0.9.12)
25
+ thor (>= 0.18.1)
26
+ guard-minitest (2.3.1)
27
+ guard (~> 2.0)
28
+ minitest (>= 3.0)
29
+ highline (1.6.21)
30
+ httparty (0.13.1)
31
+ json (~> 1.8)
32
+ multi_xml (>= 0.5.2)
33
+ json (1.8.1)
34
+ listen (2.7.9)
35
+ celluloid (>= 0.15.2)
36
+ rb-fsevent (>= 0.9.3)
37
+ rb-inotify (>= 0.9)
38
+ lumberjack (1.0.9)
39
+ method_source (0.8.2)
40
+ minitest (5.4.0)
41
+ multi_json (1.10.1)
42
+ multi_xml (0.5.5)
43
+ pry (0.10.0)
44
+ coderay (~> 1.1.0)
45
+ method_source (~> 0.8.1)
46
+ slop (~> 3.4)
47
+ rake (10.1.1)
48
+ rb-fsevent (0.9.4)
49
+ rb-inotify (0.9.5)
50
+ ffi (>= 0.5.0)
51
+ slop (3.6.0)
52
+ thor (0.19.1)
53
+ timers (1.1.0)
54
+
55
+ PLATFORMS
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ bundler (~> 1.6)
60
+ guard
61
+ guard-minitest
62
+ hangbot!
63
+ rake
@@ -0,0 +1,7 @@
1
+ # More info at https://github.com/guard/guard#readme
2
+
3
+ guard :minitest, include: ['lib'], cli: '--pride' do
4
+ watch(%r{^spec/(.*)_spec\.rb$})
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
6
+ watch(%r{^spec/spec_helper\.rb$}) { 'spec' }
7
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Josh Strater
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,40 @@
1
+ **hangbot** is
2
+
3
+ 1. a way for me to try out the HipChat v2 API & webhooks
4
+ 2. a shared hangman-style game for HipChat rooms
5
+
6
+ ## Setup
7
+
8
+ ### From Rubygems
9
+
10
+ Requires Ruby >= 2.0.
11
+
12
+ 1. `$ gem install hangbot`
13
+
14
+ ### From GitHub
15
+
16
+ Requires Ruby >= 2.0 and Bundler.
17
+
18
+ 1. `$ git clone git@github.com:jstrater/hangbot.git`
19
+ 2. `$ cd hangbot`
20
+ 3. `$ bundle install`
21
+
22
+ ## Configuration
23
+
24
+ hangbot's configuration lives in a YAML file. You can use `example.hangbot.yaml` as a template for your config. Fill in the appropriate values for your environment, including your HipChat API key, room name, and externally visible server URL.
25
+
26
+ ### Tips
27
+
28
+ - You'll need a HipChat API token with `admin_room` and `send_notification` scopes for the selected room.
29
+ - Pay attention to the `local_server.base_url` field -- this is the address that HipChat's webhooks will use to notify the local server about new messages, and if you're behind a firewall or router, you'll want to make sure that it's reachable from the outside world.
30
+
31
+ ## How to play
32
+
33
+ Start the server by running `$ hangbot hangbot.yaml` (assuming that you put your configuration in `hangbot.yaml`.)
34
+
35
+ Once hangbot is running, type `/hangbot` in your HipChat room to start a new game. Use `/guess L` to guess a letter (where `L` is any letter of the alphabet.)
36
+
37
+ ## TODO
38
+
39
+ - Automatic router traversal
40
+ - Put in friendlier error messages
@@ -0,0 +1,6 @@
1
+ require 'rake/testtask'
2
+ require 'bundler/gem_tasks'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.test_files = FileList['spec/*_spec.rb']
6
+ end
@@ -0,0 +1,131 @@
1
+ Abba
2
+ Ace of Base
3
+ Adele
4
+ Aerosmith
5
+ Al Green
6
+ Alanis Morissette
7
+ Avril Lavigne
8
+ Barbra Streisand
9
+ Bee Gees
10
+ Beyonce
11
+ Billy Ocean
12
+ Blondie
13
+ Boney M
14
+ Boyz II Men
15
+ Britney Spears
16
+ Bruce Springsteen
17
+ Bruno Mars
18
+ Bryan Adams
19
+ Celine Dion
20
+ Cher
21
+ Chic
22
+ Chicago
23
+ Christina Aguilera
24
+ Coldplay
25
+ Color Me Badd
26
+ Coolio
27
+ Creedence Clearwater Revival
28
+ Culture Club
29
+ Cyndi Lauper
30
+ David Bowie
31
+ Destiny's Child
32
+ Diana Ross
33
+ Don McLean
34
+ Donna Summer
35
+ Eagles
36
+ Earth Wind and Fire
37
+ Elton John
38
+ Eminem
39
+ Eric Clapton
40
+ Falco
41
+ Fergie
42
+ Fine Young Cannibals
43
+ Fleetwood Mac
44
+ Frankie Goes To Hollywood
45
+ George Harrison
46
+ George McCrae
47
+ George Michael
48
+ Gilbert O'Sullivan
49
+ Glee Cast
50
+ Green Day
51
+ Gwen Stefani
52
+ Haddaway
53
+ Hanson
54
+ Harry Nilsson
55
+ INXS
56
+ Irene Cara
57
+ J Geils Band
58
+ Janet Jackson
59
+ Jennifer Lopez
60
+ John Lennon
61
+ Justin Timberlake
62
+ Katy Perry
63
+ Kim Carnes
64
+ Kylie Minogue
65
+ Lady GaGa
66
+ Leona Lewis
67
+ Linkin Park
68
+ MC Hammer
69
+ Madonna
70
+ Mariah Carey
71
+ Men At Work
72
+ Michael Jackson
73
+ Mika
74
+ N Sync
75
+ Nelly
76
+ Nelly Furtado
77
+ New Kids On The Block
78
+ Nickelback
79
+ Nirvana
80
+ Oasis
81
+ Offspring
82
+ Paula Abdul
83
+ Pink
84
+ Pink Floyd
85
+ Prince
86
+ Pussycat
87
+ Queen
88
+ REM
89
+ Red Hot Chili Peppers
90
+ Rick Astley
91
+ Ricky Martin
92
+ Rihanna
93
+ Rod Stewart
94
+ Roxette
95
+ Shakira
96
+ Sheena Easton
97
+ Shocking Blue
98
+ Sinead O'Connor
99
+ Slade
100
+ Smokie
101
+ Snap
102
+ Spice Girls
103
+ Starsound
104
+ Stevie Wonder
105
+ Survivor
106
+ T Rex
107
+ TLC
108
+ Taylor Swift
109
+ Tears For Fears
110
+ Terry Jacks
111
+ The Backstreet Boys
112
+ The Bangles
113
+ The Bay City Rollers
114
+ The Beatles
115
+ The Black Eyed Peas
116
+ The Fugees
117
+ The Human League
118
+ The Pet Shop Boys
119
+ The Police
120
+ The Pussycat Dolls
121
+ The Rolling Stones
122
+ The Rubettes
123
+ The Sweet
124
+ The Village People
125
+ Tiffany
126
+ USA For Africa
127
+ Usher
128
+ Wham
129
+ Whitney Houston
130
+ Wings
131
+ tATu
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ # Make sure lib/ is in the include path
7
+ lib_path = File.expand_path '../lib', File.dirname(__FILE__)
8
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
9
+
10
+ require 'hangbot'
@@ -0,0 +1,21 @@
1
+ # By default, hangbot will use its own default wordlist. If you'd like to use
2
+ # your own, set word_list.
3
+ #word_list: bandnames.txt
4
+
5
+ local_server:
6
+ # The address for the local webhook listening server to bind to.
7
+ bind_address: 0.0.0.0
8
+ port: 4567
9
+
10
+ # This is the URI to access your local server from the outside world
11
+ # (accounting for NAT, port forwarding, etc.)
12
+ base_url: http://1.2.3.4:4567
13
+
14
+ hipchat:
15
+ room_name: my_hipchat_room
16
+
17
+ # The token must have admin_room and send_notification scopes for the room.
18
+ api_token: mYH1pCh47t0k3N
19
+
20
+ # Seconds before an API request times out.
21
+ timeout: 2
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ Gem::Specification.new do |spec|
3
+ spec.name = "hangbot"
4
+ spec.version = "0.1.0"
5
+ spec.authors = ["Josh Strater"]
6
+ spec.email = ["jstrater@gmail.com"]
7
+ spec.summary = %q{Hangman for HipChat}
8
+ spec.description = %q{Small Hangman game for HipChat. Uses the v2 API and webhooks.}
9
+ spec.homepage = "https://github.com/jstrater/hangbot"
10
+ spec.license = "MIT"
11
+
12
+ spec.files = `git ls-files -z`.split("\x0")
13
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
14
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
15
+ spec.require_paths = ["lib"]
16
+
17
+ spec.add_development_dependency "bundler", "~> 1.6"
18
+ spec.add_development_dependency "rake", "~> 10.0"
19
+ spec.add_development_dependency "guard", "~> 2.6"
20
+ spec.add_development_dependency "guard-minitest", "~> 2.3"
21
+
22
+ spec.add_runtime_dependency "httparty", "~> 0.13"
23
+ spec.add_runtime_dependency "json", "~> 1.8"
24
+ spec.add_runtime_dependency "configliere", "~> 0.4"
25
+ end
@@ -0,0 +1,213 @@
1
+ require 'webrick'
2
+ require 'json'
3
+ require 'hipchat_party'
4
+ require 'uri'
5
+ require 'pp'
6
+ require 'configliere'
7
+ require 'logger'
8
+ require 'hangman_game'
9
+ require 'hangman_wordlist'
10
+
11
+
12
+ # Set up a webhook and make sure any old leftover hooks are cleaned out
13
+ def init_hipchat_webhook hipchat, room_name, webhook_name, url
14
+ remove_hipchat_webhooks hipchat, room_name, webhook_name
15
+
16
+ return hipchat.create_webhook(
17
+ room_name,
18
+ webhook_name,
19
+ 'room_message',
20
+ url,
21
+ pattern:'^\/(hangman|guess).*'
22
+ )
23
+ end
24
+
25
+ # Clean out all HipChat webhooks with webhook_name.
26
+ def remove_hipchat_webhooks hipchat, room_name, webhook_name
27
+ hipchat.delete_webhooks_by_name room_name, webhook_name
28
+ end
29
+
30
+ # Create a web server that listens on the given address & port for HipChat
31
+ # message notifications. The webhook ID on the notifications will be checked
32
+ # against +webhook_id+.
33
+ #
34
+ # Message contents will be passed to the block, and the block's return value
35
+ # will be posted as a new HipChat room notification.
36
+ def respond_to_hipchat_messages port:4567, address:'0.0.0.0', room_name:nil,
37
+ webhook_url:nil, api_token:nil, timeout:30, logger:nil
38
+ webhook_name = 'hangbot'
39
+
40
+ # Tell HipChat to let us know when there's a new message in the room
41
+ hipchat = HipChatParty.new api_token, timeout:timeout
42
+ logger.info "Setting up a new HipChat webhook and clearing out old ones"
43
+ webhook_id = init_hipchat_webhook hipchat, room_name, webhook_name, webhook_url
44
+
45
+ # Create a WEBrick server to listen for HipChat notifications.
46
+ #
47
+ # For better security, you could get a cert and use it to run WEBrick in HTTPS mode.
48
+ server = WEBrick::HTTPServer.new Port:port, BindAddress:address
49
+ server.mount_proc '/' do |req, res|
50
+ body = JSON.parse(req.body)
51
+
52
+ # Check to see if this is the hook that we're expecting
53
+ unless body['event'] == 'room_message' && body['webhook_id'] == webhook_id
54
+ logger.warn "Unexpected webhook callback from #{req.remote_ip}"
55
+ logger.debug "#{body.inspect}"
56
+ next
57
+ end
58
+
59
+ # Let the block come up with a response to the message
60
+ sender = body['item']['message']['from']['mention_name']
61
+ message = body['item']['message']['message']
62
+ logger.info "message: #{sender}: #{message}"
63
+ response_message = yield message
64
+
65
+ # Send the block's response back to the room
66
+ if response_message
67
+ logger.info "response: #{response_message}"
68
+ hipchat.send_room_notification room_name, response_message
69
+ else
70
+ logger.info "no response"
71
+ end
72
+ end
73
+
74
+ # Shut down cleanly on ctrl-c
75
+ trap 'INT' do
76
+ # Reset the signal handler so that two SIGINTs shut things down immediately
77
+ trap 'INT', 'DEFAULT'
78
+
79
+ server.shutdown
80
+ end
81
+
82
+ # Start the server here. Any code after server.start will be executed after
83
+ # the server shuts down.
84
+ logger.info "Starting web server at #{address}:#{port} (CTRL-C to stop)"
85
+ server.start
86
+
87
+ # Clean up the hooks, otherwise HipChat will keep trying to call us.
88
+ logger.info "Cleaning up webhooks"
89
+ remove_hipchat_webhooks hipchat, room_name, webhook_name
90
+ end
91
+
92
+ def load_settings!
93
+ config_file_argument = ARGV[0] ? File.absolute_path(ARGV[0]) : nil
94
+ config_file = config_file_argument || './hangbot.yaml'
95
+ default_word_list = File.expand_path(
96
+ File.join(File.dirname(__FILE__), '../bandnames.txt')
97
+ )
98
+
99
+ # Use Configliere to define and read in our settings from hangbot.yaml
100
+ Settings.define 'word_list',
101
+ description:"Word list file"
102
+ Settings.define 'local_server.base_url', required:true,
103
+ description:"URL for your local server, as you would access it from the outside world (accounting for NAT, port forwarding, etc.)"
104
+ Settings.define 'local_server.bind_address',
105
+ description:"Address for the local server to bind to"
106
+ Settings.define 'local_server.port',
107
+ description:"Port to listen on"
108
+ Settings.define 'hipchat.room_name', required:true,
109
+ description:"HipChat room to join"
110
+ Settings.define 'hipchat.api_token', required:true,
111
+ description:"OAuth bearer token. Must have admin_room and send_notification scopes for the room."
112
+ Settings.define 'hipchat.timeout',
113
+ description:"Seconds before a HipChat API request times out."
114
+ Settings({ # defaults
115
+ 'word_list' => default_word_list,
116
+ 'local_server' => {
117
+ 'bind_address' => '0.0.0.0',
118
+ 'port' => 4567
119
+ },
120
+ 'hipchat' => {
121
+ 'timeout' => 30
122
+ }
123
+ })
124
+ Settings.read config_file
125
+ Settings.resolve!
126
+
127
+ # The word_list's path is relative to the config file. Expand it so that we
128
+ # have the right path later on.
129
+ Settings['word_list_full_path'] =
130
+ File.expand_path(Settings['word_list'], File.dirname(config_file))
131
+ end
132
+
133
+
134
+ def partial_solution_string game
135
+ game.partial_solution.map{ |char| char || '_' }.join(' ')
136
+ end
137
+
138
+ def game_status game
139
+ misses_list = game.incorrect_guesses.empty? ? "" : ": #{game.incorrect_guesses.to_a.join(', ')}"
140
+ misses_message = "[#{game.incorrect_guesses.size}/#{game.guess_limit} misses#{misses_list}]"
141
+
142
+ "#{partial_solution_string game} #{misses_message}"
143
+ end
144
+
145
+ def main
146
+ # Set up a logger with the same output format as WEBrick
147
+ logger = Logger.new STDERR
148
+ logger.formatter = proc do |severity, datetime, progname, msg|
149
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} #{msg}\n"
150
+ end
151
+
152
+ load_settings!
153
+ webhook_url = URI.join Settings['local_server']['base_url'], '/'
154
+ wordlist = HangmanWordlist.new Settings['word_list_full_path']
155
+ game = nil
156
+
157
+ respond_to_hipchat_messages(
158
+ address:Settings['local_server']['bind_address'],
159
+ port:Settings['local_server']['port'],
160
+ webhook_url:webhook_url,
161
+ room_name:Settings['hipchat']['room_name'],
162
+ api_token:Settings['hipchat']['api_token'],
163
+ timeout:Settings['hipchat']['timeout'],
164
+ logger:logger
165
+ ) do |message|
166
+ # Split up a "/command [args...]" message into its parts
167
+ message_parts = /^\/(?<command>\w+)(\s+(?<args>.*))?$/.match message
168
+ if message_parts
169
+ command = message_parts['command']
170
+ args = message_parts['args']
171
+
172
+ case command
173
+ # New game command
174
+ when 'hangman'
175
+ game = HangmanGame.new wordlist.random_word
176
+ "Starting a new game. Fill in the blanks: #{partial_solution_string game}\nMake guesses with \"/guess LETTER\". #{game.remaining_misses} mistakes and you're toast."
177
+
178
+ # Command to guess a letter
179
+ when 'guess'
180
+ unless game
181
+ next "No game in progress. Use /hangman to start a new one."
182
+ end
183
+
184
+ if game.finished?
185
+ next "Game's over! Use /hangman to start a new one."
186
+ end
187
+
188
+ # See if the command included a valid guess
189
+ guess = args.strip.upcase
190
+ unless HangmanGame.acceptable_guess? guess
191
+ next "The /guess command takes a single letter of the alphabet."
192
+ end
193
+
194
+ if game.guessed? guess
195
+ next "#{guess} has already been guessed."
196
+ end
197
+
198
+ # Make the guess and let the user know how it turned out
199
+ game.guess! guess
200
+ if game.won?
201
+ "You got it! The answer was #{game.word}. (success)"
202
+ elsif game.lost?
203
+ "(sadpanda) Aww, you lost. The correct answer was #{game.word}."
204
+ else
205
+ # No win or loss yet -- just print out the game state.
206
+ game_status game
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ main
@@ -0,0 +1,167 @@
1
+ require 'set'
2
+
3
+ # Game logic & state. Create an instance to start a game, then use the #guess!
4
+ # method to progress. The #won? and #lost? methods will tell when the game is
5
+ # over, and #partial_solution shows the parts of the target word that have been
6
+ # guessed correctly so far.
7
+ class HangmanGame
8
+ attr_reader :word, :guess_limit
9
+
10
+ # Creates a new game with +word+ as the target.
11
+ #
12
+ # Raises an ArgumentError if +word+ is invalid.
13
+ def initialize word, guess_limit=6
14
+ @word = HangmanGame.guard_word(word).freeze
15
+
16
+ # Letters used in the target word. These are the letters the user needs
17
+ # to guess correctly to win.
18
+ @correct_letters = Set.new(
19
+ @word.chars.find_all {|char| HangmanGame.acceptable_guess? char }
20
+ ).freeze
21
+
22
+ @guessed_letters = Set.new
23
+
24
+ if guess_limit <= 0
25
+ raise ArgumentError, "guess_limit must be higher than 0"
26
+ end
27
+
28
+ @guess_limit = guess_limit.freeze
29
+ end
30
+
31
+ # Returns a Set containing all of the incorrect guesses so far.
32
+ def incorrect_guesses
33
+ @guessed_letters - @correct_letters
34
+ end
35
+
36
+ # Returns a Set of all the correct guesses so far.
37
+ def correct_guesses
38
+ @guessed_letters & @correct_letters
39
+ end
40
+
41
+ # Returns a Set of all guesses so far.
42
+ def guesses
43
+ @guessed_letters.clone
44
+ end
45
+
46
+ # Returns the remaining number of missed guesses before the game is lost.
47
+ def remaining_misses
48
+ @guess_limit - incorrect_guesses.size
49
+ end
50
+
51
+ # Returns an array representing the target word. Letters that have already
52
+ # been guessed are included; letters that have not yet been guessed are nil.
53
+ # Any other characters (spaces, dashes) are included.
54
+ def partial_solution
55
+ @word.chars.map do |letter|
56
+ if HangmanGame::ACCEPTABLE_LETTER =~ letter
57
+ # Letters of the alphabet
58
+ if @guessed_letters.include? letter
59
+ letter
60
+ else
61
+ nil
62
+ end
63
+ else
64
+ # Non-alpha characters
65
+ letter
66
+ end
67
+ end
68
+ end
69
+
70
+ # Guesses a letter. Returns true if the letter is in the target word, false
71
+ # if not.
72
+ #
73
+ # Raises HangmanGame::StateError if the game has already ended or the letter
74
+ # has already been guessed.
75
+ def guess! raw_letter
76
+ letter = HangmanGame.guard_guess(raw_letter)
77
+
78
+ # Make sure this guess is valid for the current game state
79
+ if self.finished?
80
+ raise HangmanGame::StateError, "guess! called after game ended"
81
+ end
82
+ if self.guessed? letter
83
+ raise HangmanGame::StateError, "letter already guessed: #{letter}"
84
+ end
85
+
86
+ @guessed_letters.add letter
87
+ @correct_letters.include? letter
88
+ end
89
+
90
+ # Returns true if the letter has already been guessed.
91
+ def guessed? raw_letter
92
+ letter = HangmanGame.guard_guess(raw_letter)
93
+
94
+ @guessed_letters.include? letter
95
+ end
96
+
97
+ # True if the game has been won.
98
+ def won?
99
+ !self.lost? && @guessed_letters >= @correct_letters
100
+ end
101
+
102
+ # True if the game has been lost.
103
+ def lost?
104
+ self.incorrect_guesses.size >= @guess_limit
105
+ end
106
+
107
+ # True if the game has concluded.
108
+ def finished?
109
+ self.lost? || self.won?
110
+ end
111
+
112
+ # Checks to see if a string constitutes a valid target word. That means any
113
+ # string of plain letters (A-Z), optionally separated by hyphens, spaces, and apostrophes.
114
+ #
115
+ # Note that this doesn't check any kind of dictionary. Right now we're just
116
+ # interested in limiting the character set.
117
+ def self.acceptable_word? word
118
+ HangmanGame::ACCEPTABLE_WORD =~ HangmanGame.normalize(word)
119
+ end
120
+
121
+ def self.acceptable_guess? letter
122
+ HangmanGame::ACCEPTABLE_LETTER =~ HangmanGame.normalize(letter)
123
+ end
124
+
125
+ # Readable dump of the game state for debugging and that sort of thing
126
+ def dump
127
+ word_with_blanks = self.partial_solution.map{ |c| c || '_' }.join('')
128
+ <<-END
129
+ target word: #{@word}
130
+ guessed: #{word_with_blanks}
131
+ incorrect: [#{self.incorrect_guesses.to_a.join ', '}]
132
+ END
133
+ end
134
+
135
+ # Indicates that something was done at the wrong time in the game, like
136
+ # guessing a letter after winning.
137
+ class StateError < StandardError; end
138
+
139
+ private
140
+
141
+ ACCEPTABLE_LETTER = /^[A-Z]$/
142
+ ACCEPTABLE_WORD = /^[A-Z]([- ']?[A-Z])*$/
143
+
144
+ def self.normalize word_or_letter
145
+ word_or_letter.strip.upcase
146
+ end
147
+
148
+ def self.guard_guess raw_letter
149
+ letter = HangmanGame.normalize raw_letter
150
+
151
+ unless HangmanGame.acceptable_guess? letter
152
+ raise ArgumentError, "Invalid guess: \"#{letter}\" (only plain letters allowed)"
153
+ end
154
+
155
+ letter
156
+ end
157
+
158
+ def self.guard_word raw_word
159
+ word = HangmanGame.normalize raw_word
160
+
161
+ unless HangmanGame.acceptable_word? word
162
+ raise ArgumentError, "Invalid word: \"#{word}\" (only plain letters A-Z separated by spaces and hyphens allowed)"
163
+ end
164
+
165
+ word
166
+ end
167
+ end
@@ -0,0 +1,36 @@
1
+ require 'set'
2
+ require 'hangman_game'
3
+
4
+ # List of hangman words loaded from a text file (one word per line.)
5
+ class HangmanWordlist
6
+ attr_reader :words
7
+
8
+ # Creates a word list from the given file. The file should have one word per
9
+ # line, and all of the words must pass HangmanGame.acceptable_word?. Those
10
+ # that don't pass will be left out.
11
+ def initialize file
12
+ word_set = Set.new
13
+
14
+ File.open(file, "r") do |f|
15
+ f.each_line do |raw_line|
16
+ line = raw_line.strip.upcase
17
+
18
+ if HangmanGame.acceptable_word? line
19
+ word_set.add line
20
+ end
21
+ end
22
+ end
23
+
24
+ @words = word_set.to_a.freeze
25
+
26
+ Random.srand
27
+ end
28
+
29
+ def size
30
+ @words.size
31
+ end
32
+
33
+ def random_word
34
+ @words[Random.rand(self.size)]
35
+ end
36
+ end
@@ -0,0 +1,76 @@
1
+ require 'httparty'
2
+ require 'json'
3
+
4
+
5
+ # Thin abstraction layer for the HipChat v2 API.
6
+ #
7
+ # The official API's module name is "hipchat", hence the disambiguating "party" suffix.
8
+ #
9
+ # TODO: error handling for requests
10
+ class HipChatParty
11
+ include HTTParty
12
+ base_uri 'https://api.hipchat.com/v2/'
13
+ headers 'Content-Type' => "application/json"
14
+ format :json
15
+
16
+ # +api_token+ must have admin_room and send_notification scopes.
17
+ #
18
+ # +timeout+ is specified in seconds.
19
+ def initialize api_token, timeout:30
20
+ self.class.headers 'Authorization' => "Bearer #{api_token.strip}"
21
+ end
22
+
23
+ # Delete all webhooks with the specified name in a room
24
+ def delete_webhooks_by_name room_name, hook_name
25
+ # We can't delete webhooks by name; we have to use IDs. First we look up
26
+ # the IDs, then we use them to delete the hooks.
27
+ webhooks_response = self.get_webhooks room_name
28
+ webhooks = webhooks_response.parsed_response['items']
29
+ webhooks_to_delete = webhooks.find_all { |wh| wh['name'] == hook_name }
30
+ webhooks_to_delete.each do |webhook|
31
+ self.delete_webhook_by_id room_name, webhook['id']
32
+ end
33
+ end
34
+
35
+ # https://www.hipchat.com/docs/apiv2/method/delete_webhook
36
+ def delete_webhook_by_id room_name, hook_id
37
+ self.class.delete "/room/#{room_name}/webhook/#{hook_id}"
38
+ end
39
+
40
+ # List all webhooks for the room
41
+ #
42
+ # https://www.hipchat.com/docs/apiv2/method/get_all_webhooks
43
+ def get_webhooks room_name
44
+ self.class.get "/room/#{room_name}/webhook"
45
+ end
46
+
47
+ # Sets a webhook to call back to +url+ whenever +event+ occurs in
48
+ # +room_name+. If +pattern+ is set, and the event type is "room_message",
49
+ # only matching messages will trigger a callback.
50
+ #
51
+ # https://www.hipchat.com/docs/apiv2/method/create_webhook
52
+ def create_webhook room_name, hook_name, event, url, pattern:nil
53
+ post_body = {
54
+ url: url,
55
+ event: event,
56
+ name: hook_name
57
+ }
58
+
59
+ if event == 'room_message' && pattern
60
+ post_body['pattern'] = pattern
61
+ end
62
+
63
+ response = self.class.post "/room/#{room_name}/webhook", body: post_body.to_json
64
+ response.parsed_response['id']
65
+ end
66
+
67
+ # https://www.hipchat.com/docs/apiv2/method/send_room_notification
68
+ def send_room_notification room_name, message, color:'yellow', notify:false, format:'text'
69
+ self.class.post "/room/#{room_name}/notification", body: {
70
+ color: color,
71
+ message: message,
72
+ notify: notify,
73
+ message_format: format
74
+ }.to_json
75
+ end
76
+ end
@@ -0,0 +1,78 @@
1
+ require 'minitest/autorun'
2
+ require 'hangman_game'
3
+
4
+ describe HangmanGame do
5
+ before do
6
+ @game = HangmanGame.new('pinkerton', 3)
7
+ end
8
+
9
+ describe "when initialized" do
10
+ it "must reject invalid words" do
11
+ ['', ' ', '- -', 'af2', 'wnmkl.df'].each do |word|
12
+ proc { HangmanGame.new(word) }.must_raise ArgumentError
13
+ end
14
+ end
15
+
16
+ it "must accept valid words" do
17
+ ['toothless', ' turtle\'s ', 'tor-pe-do', 'torte truck'].each do |word|
18
+ HangmanGame.new(word).wont_be_nil
19
+ end
20
+ end
21
+
22
+ it "must allow at least one guess" do
23
+ proc { HangmanGame.new('foo', 0) }.must_raise ArgumentError
24
+ end
25
+ end
26
+
27
+ describe "when a player guesses" do
28
+ it "must detect correct guesses" do
29
+ @game.guess! 'p'
30
+ @game.correct_guesses.must_include 'P'
31
+ @game.guess! 'o'
32
+ @game.correct_guesses.must_include 'O'
33
+ @game.incorrect_guesses.must_be_empty
34
+ end
35
+
36
+ it "must detect incorrect guesses" do
37
+ @game.guess! 'z'
38
+ @game.incorrect_guesses.must_include 'Z'
39
+ @game.correct_guesses.must_be_empty
40
+ @game.remaining_misses.must_equal 2
41
+ end
42
+
43
+ it "must detect a win" do
44
+ 'plinkerdto'.each_char{ |c| @game.guess! c }
45
+
46
+ assert @game.won?
47
+ assert @game.finished?
48
+ assert !@game.lost?
49
+ end
50
+
51
+ it "must detect a loss" do
52
+ 'plinkerdtu'.each_char{ |c| @game.guess! c }
53
+
54
+ assert !@game.won?
55
+ assert @game.finished?
56
+ assert @game.lost?
57
+ end
58
+
59
+ it "must complain if the game's already ended" do
60
+ proc {
61
+ 'plinkerdtug'.each_char{ |c| @game.guess! c }
62
+ }.must_raise HangmanGame::StateError
63
+ end
64
+
65
+ it "must reject invalid guesses" do
66
+ ['', '-', ' ', 'fw'].each do |guess|
67
+ proc { @game.guess! guess }.must_raise ArgumentError
68
+ end
69
+ end
70
+
71
+ it "must reject duplicate guesses" do
72
+ proc {
73
+ @game.guess! 'z'
74
+ @game.guess! 'Z'
75
+ }.must_raise HangmanGame::StateError
76
+ end
77
+ end
78
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hangbot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Strater
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: guard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '2.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard-minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '2.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: httparty
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '0.13'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '0.13'
83
+ - !ruby/object:Gem::Dependency
84
+ name: json
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.8'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '1.8'
97
+ - !ruby/object:Gem::Dependency
98
+ name: configliere
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '0.4'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '0.4'
111
+ description: Small Hangman game for HipChat. Uses the v2 API and webhooks.
112
+ email:
113
+ - jstrater@gmail.com
114
+ executables:
115
+ - hangbot
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - .gitignore
120
+ - .ruby-version
121
+ - Gemfile
122
+ - Gemfile.lock
123
+ - Guardfile
124
+ - LICENSE.txt
125
+ - README.md
126
+ - Rakefile
127
+ - bandnames.txt
128
+ - bin/hangbot
129
+ - example.hangbot.yaml
130
+ - hangbot.gemspec
131
+ - lib/hangbot.rb
132
+ - lib/hangman_game.rb
133
+ - lib/hangman_wordlist.rb
134
+ - lib/hipchat_party.rb
135
+ - spec/hangman_game_spec.rb
136
+ homepage: https://github.com/jstrater/hangbot
137
+ licenses:
138
+ - MIT
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - '>='
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 2.2.2
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Hangman for HipChat
160
+ test_files:
161
+ - spec/hangman_game_spec.rb