hangbot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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