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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +63 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +40 -0
- data/Rakefile +6 -0
- data/bandnames.txt +131 -0
- data/bin/hangbot +10 -0
- data/example.hangbot.yaml +21 -0
- data/hangbot.gemspec +25 -0
- data/lib/hangbot.rb +213 -0
- data/lib/hangman_game.rb +167 -0
- data/lib/hangman_wordlist.rb +36 -0
- data/lib/hipchat_party.rb +76 -0
- data/spec/hangman_game_spec.rb +78 -0
- metadata +161 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
hangbot.yaml
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p451
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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
|
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
data/bandnames.txt
ADDED
@@ -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
|
data/bin/hangbot
ADDED
@@ -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
|
data/hangbot.gemspec
ADDED
@@ -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
|
data/lib/hangbot.rb
ADDED
@@ -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
|
data/lib/hangman_game.rb
ADDED
@@ -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
|