gameworks 1.0.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/Gemfile +3 -0
- data/Gemfile.lock +50 -0
- data/README +155 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/gameworks.gemspec +30 -0
- data/lib/gameworks.rb +2 -0
- data/lib/gameworks/fuse.rb +16 -0
- data/lib/gameworks/game.rb +175 -0
- data/lib/gameworks/game_registry.rb +25 -0
- data/lib/gameworks/game_snapshot.rb +13 -0
- data/lib/gameworks/player.rb +50 -0
- data/lib/gameworks/server.rb +67 -0
- data/lib/gameworks/servlet/add_move.rb +47 -0
- data/lib/gameworks/servlet/add_observer.rb +44 -0
- data/lib/gameworks/servlet/add_player.rb +30 -0
- data/lib/gameworks/servlet/base.rb +22 -0
- data/lib/gameworks/servlet/game_list.rb +21 -0
- data/lib/gameworks/servlet/game_view.rb +24 -0
- data/lib/gameworks/servlet/match_maker.rb +70 -0
- data/lib/gameworks/version.rb +3 -0
- metadata +198 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ffa430b13432be40c5d7012161ca9ae9d3cc2461
|
4
|
+
data.tar.gz: bd766b49c63da9fbf51686c611371ed979a8e3c2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 914f509a5940360aaa8d33e1762e6624d07055c5b1e8d511c7c0726bc88dd6d869237242c0ced9d6ae90f9873bab9923256c9b4f9ce003fc7efd123daa1941d0
|
7
|
+
data.tar.gz: 6b90a75243aad00713e1d8dac62331dda700a1673637a40900bb4de718d6ef4548de42d777264a234628e56659a857de586ca37df65f84ac13e1c7ea0e229271
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
gameworks (1.0.0)
|
5
|
+
eventmachine (~> 1.0.7)
|
6
|
+
json (~> 1.8.3)
|
7
|
+
rake (~> 10.0)
|
8
|
+
thin (~> 1.6.3)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
daemons (1.2.3)
|
14
|
+
diff-lcs (1.2.5)
|
15
|
+
eventmachine (1.0.7)
|
16
|
+
json (1.8.3)
|
17
|
+
rack (1.6.4)
|
18
|
+
rake (10.4.2)
|
19
|
+
rspec (3.3.0)
|
20
|
+
rspec-core (~> 3.3.0)
|
21
|
+
rspec-expectations (~> 3.3.0)
|
22
|
+
rspec-mocks (~> 3.3.0)
|
23
|
+
rspec-core (3.3.1)
|
24
|
+
rspec-support (~> 3.3.0)
|
25
|
+
rspec-eventmachine (0.2.0)
|
26
|
+
eventmachine (>= 0.12.0)
|
27
|
+
rspec (>= 2.0, < 4.0)
|
28
|
+
rspec-expectations (3.3.0)
|
29
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
30
|
+
rspec-support (~> 3.3.0)
|
31
|
+
rspec-mocks (3.3.1)
|
32
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
+
rspec-support (~> 3.3.0)
|
34
|
+
rspec-support (3.3.0)
|
35
|
+
thin (1.6.3)
|
36
|
+
daemons (~> 1.0, >= 1.0.9)
|
37
|
+
eventmachine (~> 1.0)
|
38
|
+
rack (~> 1.0)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
bundler (~> 1.8)
|
45
|
+
gameworks!
|
46
|
+
rspec (~> 3.3.0)
|
47
|
+
rspec-eventmachine (~> 0.2.0)
|
48
|
+
|
49
|
+
BUNDLED WITH
|
50
|
+
1.10.3
|
data/README
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
= Quick start server
|
2
|
+
|
3
|
+
Install required gems and run server:
|
4
|
+
bundle install
|
5
|
+
bundle exec thin start
|
6
|
+
|
7
|
+
To run specs:
|
8
|
+
bundle exec spec spec
|
9
|
+
|
10
|
+
(You will probably already have the gems installed so you probably don't need the 'bundle exec')
|
11
|
+
All request and response payloads are serialized JSON data.
|
12
|
+
|
13
|
+
= DATA TYPES
|
14
|
+
|
15
|
+
== New Player (request)
|
16
|
+
|
17
|
+
{name: <string>}
|
18
|
+
|
19
|
+
name of the new player. the game engine may extend this.
|
20
|
+
|
21
|
+
== Player (response)
|
22
|
+
|
23
|
+
{name: <string>,
|
24
|
+
score: <integer> or 'disqualified'}
|
25
|
+
|
26
|
+
name and current score of the player. the game engine may extend this.
|
27
|
+
|
28
|
+
== New Game (request)
|
29
|
+
|
30
|
+
determined by the game engine
|
31
|
+
|
32
|
+
== Game (response)
|
33
|
+
|
34
|
+
{players: [<Player>*],
|
35
|
+
state: <Game State>}
|
36
|
+
|
37
|
+
players is the set of Players joined to the game.
|
38
|
+
|
39
|
+
state is the current Game State.
|
40
|
+
|
41
|
+
the game engine may extend this.
|
42
|
+
|
43
|
+
== Game Delta (response)
|
44
|
+
|
45
|
+
{players: [<Player>*],
|
46
|
+
state: <Game State>}
|
47
|
+
|
48
|
+
players is the set of Players whose scores have changed since the request
|
49
|
+
began.
|
50
|
+
|
51
|
+
state is the current Game State.
|
52
|
+
|
53
|
+
the game engine may extend this.
|
54
|
+
|
55
|
+
== Game State (response)
|
56
|
+
|
57
|
+
'initiating', 'in play', or 'completed'
|
58
|
+
|
59
|
+
See descriptions of game states below.
|
60
|
+
|
61
|
+
== New Move (request)
|
62
|
+
|
63
|
+
determined by the game engine.
|
64
|
+
|
65
|
+
= STARTING A GAME
|
66
|
+
|
67
|
+
To spawn a new game on the server, POST to / with a New Game object as payload.
|
68
|
+
You will receive a 201 Created response with the game's root path (henceforth
|
69
|
+
<game path>) in the Location HTTP response header.
|
70
|
+
|
71
|
+
= GAME STATES
|
72
|
+
|
73
|
+
== INITIATING
|
74
|
+
|
75
|
+
The game begins in this state. You join a game by POSTing to
|
76
|
+
<game path>/players with a Player object as payload.
|
77
|
+
|
78
|
+
If you successfully join the game, you will receive a 200 OK response at the
|
79
|
+
beginning of your first turn, with a Game object as response payload. This
|
80
|
+
response will include an X-Turn-Token HTTP response header.
|
81
|
+
|
82
|
+
If the game is no longer in this state (i.e. the game is full and has begun)
|
83
|
+
when you POST, you will receive a 410 Gone response indicating the game can no
|
84
|
+
longer be joined.
|
85
|
+
|
86
|
+
Once the game is full the game moves to the in play state.
|
87
|
+
|
88
|
+
== IN PLAY
|
89
|
+
|
90
|
+
In this state, players POST to <game path>/moves with Line objects as payload.
|
91
|
+
These requests must include an X-Turn-Token HTTP request header with the value
|
92
|
+
of the last turn token they received from the server.
|
93
|
+
|
94
|
+
After receiving a 200 OK response to a <game path>/players or <game path>/moves
|
95
|
+
POST request (indicating it is your turn) you are required to POST your next
|
96
|
+
move within {{time limit}}. If you fail to POST in that time, you are
|
97
|
+
disqualified and the game moves to the completed state.
|
98
|
+
|
99
|
+
If the move POSTed is illegal, you will receive an immediate 403 Forbidden
|
100
|
+
response describing the violation. You are disqualified and the game moves to
|
101
|
+
the completed state.
|
102
|
+
|
103
|
+
Otherwise, you will receive a 200 OK response at the beginning of your next
|
104
|
+
turn, with a Game Delta object. This response will include an X-Turn-Token
|
105
|
+
response header to be used in your next move.
|
106
|
+
|
107
|
+
After a 200 OK response, the game may have moved to the completed state. This
|
108
|
+
state change will be indicated in the Game Delta object. Once the game is
|
109
|
+
completed, further moves are ignored with a 410 Gone response.
|
110
|
+
|
111
|
+
== COMPLETED
|
112
|
+
|
113
|
+
Play is complete. Game state can still be pulled, but no new POSTs are
|
114
|
+
accepted.
|
115
|
+
|
116
|
+
= IDEMPOTENT VIEW
|
117
|
+
|
118
|
+
At any point, the full game state can be requested with a GET request to
|
119
|
+
<game path>. You will receive an immediate 200 OK response with a Game object.
|
120
|
+
|
121
|
+
= OBSERVERS
|
122
|
+
|
123
|
+
(Intended for use by game viewer, but may be used by others.)
|
124
|
+
|
125
|
+
At any point, an observer may be registered by POSTing to <game
|
126
|
+
path>/observers. This will begin a long-running unbounded HTTP response of
|
127
|
+
type application/x-multi-json , which is a series of one-line json
|
128
|
+
documents separated by newlines.
|
129
|
+
|
130
|
+
You will receive an immediate 200 OK response with the first document, a
|
131
|
+
Game object. After this, the response will remain open and new Game Delta
|
132
|
+
objects will be returned as events occur.
|
133
|
+
|
134
|
+
The HTTP response will end when the game ends, after the final Game Delta
|
135
|
+
object is sent.
|
136
|
+
|
137
|
+
The POST request requires no payload. However, if the payload includes a
|
138
|
+
"wrapper" key, then each JSON document will be fprint'ed into that text,
|
139
|
+
replacing the first %s. For instance, a HTML comet technique:
|
140
|
+
|
141
|
+
{ "wrapper": "<script type='text/javascript'>myUpdater(%s);</script>\n" }
|
142
|
+
|
143
|
+
= MATCHMAKING
|
144
|
+
|
145
|
+
When you have a bot you'd like to try against others, but you don't want to set
|
146
|
+
up a game yourself and you don't care who your opponent is, you can connect
|
147
|
+
your bot to our dirt-simple (and stupid) matchmaking system.
|
148
|
+
|
149
|
+
Simply GET /match and wait for a response. You will be added to a matchmaking
|
150
|
+
queue, and once there are at least two people in the queue, the front two will
|
151
|
+
be popped and each given a 302 Redirect with a game's url in the Location HTTP
|
152
|
+
response header.
|
153
|
+
|
154
|
+
If no one else is joining, feel free to request a match against any of our
|
155
|
+
bots! (Our bots are not eligible to win, so don't worry.)
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "gameworks"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/gameworks.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'gameworks/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "gameworks"
|
8
|
+
spec.version = Gameworks::VERSION
|
9
|
+
spec.authors = ["Brian Palmer", "Jacob Fugal", "Travis Elnicky", "Dan Dorman", "Simon Williams"]
|
10
|
+
spec.email = ["brianp@instructure.com", "jacob@instructure.com", "telnicky@instructure.com", "ddorman@instructure.com", "simon@instructure.com"]
|
11
|
+
spec.summary = "A game framework"
|
12
|
+
spec.description = "A ruby game framework used for final problems in instructure mebipenny competitions."
|
13
|
+
spec.homepage = "https://instructure.github.io"
|
14
|
+
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "bin"
|
19
|
+
spec.executables = []
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_runtime_dependency 'eventmachine', '~> 1.0', '>= 1.0.7'
|
24
|
+
spec.add_runtime_dependency 'json', '~> 1.8', '>= 1.8.3'
|
25
|
+
spec.add_runtime_dependency 'thin', '~> 1.6', '>= 1.6.3'
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.8"
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.3', '>= 3.3.0'
|
29
|
+
spec.add_development_dependency "rspec-eventmachine", "~> 0.2"
|
30
|
+
end
|
data/lib/gameworks.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
require_relative 'player'
|
2
|
+
require_relative 'game_snapshot'
|
3
|
+
require_relative 'fuse'
|
4
|
+
|
5
|
+
module Gameworks
|
6
|
+
class Game
|
7
|
+
TURN_TIME_LIMIT = 30
|
8
|
+
DEFAULT_DELAY = 0.3
|
9
|
+
|
10
|
+
STATE_INITIATING = 'initiating'
|
11
|
+
STATE_IN_PLAY = 'in play'
|
12
|
+
STATE_COMPLETED = 'completed'
|
13
|
+
|
14
|
+
attr_reader :players, :state, :id, :delay_time
|
15
|
+
|
16
|
+
def initialize(payload={})
|
17
|
+
@players = []
|
18
|
+
@state = STATE_INITIATING
|
19
|
+
@delay_time = payload['delay_time'] || DEFAULT_DELAY
|
20
|
+
@turns = {}
|
21
|
+
@observers = {}
|
22
|
+
@fuses = {}
|
23
|
+
@id = generate_uuid.split('-').first
|
24
|
+
end
|
25
|
+
|
26
|
+
def register_observer(cb)
|
27
|
+
token = generate_uuid
|
28
|
+
@observers[token] = [self.snapshot, cb]
|
29
|
+
token
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_observers(end_game = false)
|
33
|
+
@observers.each do |token, (_, cb)|
|
34
|
+
snapshot = snapshot_for_observer(token)
|
35
|
+
cb.call(delta(snapshot).to_json)
|
36
|
+
cb.succeed if end_game
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def snapshot_for_observer(token)
|
41
|
+
snapshot = nil
|
42
|
+
if @observers.has_key?(token)
|
43
|
+
snapshot, cb = @observers[token]
|
44
|
+
@observers[token][0] = self.snapshot
|
45
|
+
end
|
46
|
+
snapshot
|
47
|
+
end
|
48
|
+
|
49
|
+
def player_changed?(player, snapshot, for_player=nil)
|
50
|
+
player.score != snapshot.player_scores[player]
|
51
|
+
end
|
52
|
+
|
53
|
+
def delta(snapshot, for_player=nil)
|
54
|
+
{ :players => @players.select{ |p| player_changed?(p, snapshot, for_player) }.map{ |p| p.as_json(for_player) },
|
55
|
+
:state => @state }
|
56
|
+
end
|
57
|
+
|
58
|
+
def full?
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
|
62
|
+
def player_class
|
63
|
+
Gameworks::Player
|
64
|
+
end
|
65
|
+
|
66
|
+
def start_if_ready
|
67
|
+
if full?
|
68
|
+
init_game
|
69
|
+
signal_turns
|
70
|
+
update_observers
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_player(payload={})
|
75
|
+
return false if full?
|
76
|
+
|
77
|
+
player = player_class.new(payload)
|
78
|
+
return false unless player.valid?
|
79
|
+
|
80
|
+
@players << player
|
81
|
+
update_observers
|
82
|
+
player
|
83
|
+
end
|
84
|
+
|
85
|
+
def init_game
|
86
|
+
@players.shuffle!
|
87
|
+
@state = STATE_IN_PLAY
|
88
|
+
end
|
89
|
+
|
90
|
+
def generate_uuid
|
91
|
+
SecureRandom.uuid
|
92
|
+
end
|
93
|
+
|
94
|
+
def active_players
|
95
|
+
raise NotImplementedError
|
96
|
+
end
|
97
|
+
|
98
|
+
def ignore_disqualified_players
|
99
|
+
@players.rotate! while @players.first.disqualified?
|
100
|
+
end
|
101
|
+
|
102
|
+
def signal_turns
|
103
|
+
active_players.each do |player|
|
104
|
+
token = generate_uuid
|
105
|
+
@turns[token] = player
|
106
|
+
@fuses[player].abort if @fuses[player]
|
107
|
+
@fuses[player] = Gameworks::Fuse.new(TURN_TIME_LIMIT) { disqualify(player) }
|
108
|
+
player.signal_turn(token)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def player_for_token(token)
|
113
|
+
@turns.delete(token)
|
114
|
+
end
|
115
|
+
|
116
|
+
def add_move(payload, player)
|
117
|
+
move, error = build_move(payload, player)
|
118
|
+
return false, error unless move
|
119
|
+
legal, error = move_legal?(move, player)
|
120
|
+
return false, error unless legal
|
121
|
+
legal, error = process_move(move, player)
|
122
|
+
return false, error unless legal
|
123
|
+
ignore_disqualified_players
|
124
|
+
update_observers
|
125
|
+
return true
|
126
|
+
end
|
127
|
+
|
128
|
+
def build_move(payload, player)
|
129
|
+
raise NotImplementedError
|
130
|
+
end
|
131
|
+
|
132
|
+
def move_legal?(move, player)
|
133
|
+
raise NotImplementedError
|
134
|
+
end
|
135
|
+
|
136
|
+
def process_move(move, player)
|
137
|
+
raise NotImplementedError
|
138
|
+
end
|
139
|
+
|
140
|
+
def disqualify(player)
|
141
|
+
player.score = 'disqualified'
|
142
|
+
update_observers
|
143
|
+
if @players.select { |p| !p.disqualified? }.size < 2
|
144
|
+
end_game
|
145
|
+
else
|
146
|
+
update_observers
|
147
|
+
end_turn
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def end_game
|
152
|
+
@state = STATE_COMPLETED
|
153
|
+
@fuses.values.each(&:abort)
|
154
|
+
update_observers(true)
|
155
|
+
@players.each{ |p| p.signal_turn(nil) }
|
156
|
+
end
|
157
|
+
|
158
|
+
def snapshot_class
|
159
|
+
Gameworks::Game::Snapshot
|
160
|
+
end
|
161
|
+
|
162
|
+
def snapshot
|
163
|
+
snapshot_class.new(self)
|
164
|
+
end
|
165
|
+
|
166
|
+
def as_json(for_player=nil)
|
167
|
+
{ :players => @players.map{ |player| player.as_json(for_player) },
|
168
|
+
:state => @state }
|
169
|
+
end
|
170
|
+
|
171
|
+
def to_json(for_player=nil)
|
172
|
+
as_json(for_player).to_json
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Gameworks
|
2
|
+
class GameRegistry
|
3
|
+
def initialize
|
4
|
+
@instances = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def add(game)
|
8
|
+
@instances[game.id] = game
|
9
|
+
end
|
10
|
+
|
11
|
+
def instance(id)
|
12
|
+
@instances[id]
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_json
|
16
|
+
@instances.map do |id, game|
|
17
|
+
{ 'id' => game.id, 'state' => game.state }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_json
|
22
|
+
as_json.to_json
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Gameworks
|
4
|
+
class Player
|
5
|
+
attr_reader :name
|
6
|
+
attr_accessor :score
|
7
|
+
|
8
|
+
def initialize(payload={})
|
9
|
+
unless payload.is_a?(Hash) &&
|
10
|
+
payload.has_key?('name') &&
|
11
|
+
payload['name'].is_a?(String)
|
12
|
+
@invalid = true
|
13
|
+
return
|
14
|
+
end
|
15
|
+
|
16
|
+
@name = payload['name']
|
17
|
+
@score = 0
|
18
|
+
|
19
|
+
# used for evented "block until it's my turn". the game will push a token
|
20
|
+
# on this queue when it's the player's turn, or will push a nil when the
|
21
|
+
# game is completed
|
22
|
+
@em_queue = EventMachine::Queue.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid?
|
26
|
+
!@invalid
|
27
|
+
end
|
28
|
+
|
29
|
+
def disqualified?
|
30
|
+
@score == 'disqualified'
|
31
|
+
end
|
32
|
+
|
33
|
+
def signal_turn(token)
|
34
|
+
@em_queue.push(token)
|
35
|
+
end
|
36
|
+
|
37
|
+
def wait_for_turn
|
38
|
+
@em_queue.pop{ |token| yield token }
|
39
|
+
end
|
40
|
+
|
41
|
+
def as_json(for_player=nil)
|
42
|
+
{ :name => @name,
|
43
|
+
:score => @score }
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_json(for_player=nil)
|
47
|
+
as_json(for_player).to_json
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'game_registry'
|
3
|
+
require_relative 'servlet/add_player'
|
4
|
+
require_relative 'servlet/add_move'
|
5
|
+
require_relative 'servlet/add_observer'
|
6
|
+
require_relative 'servlet/game_view'
|
7
|
+
require_relative 'servlet/game_list'
|
8
|
+
require_relative 'servlet/match_maker'
|
9
|
+
|
10
|
+
module Gameworks
|
11
|
+
class Server
|
12
|
+
attr_reader :game_registry
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@game_registry = Gameworks::GameRegistry.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def game_class
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(env)
|
23
|
+
tokens = {}
|
24
|
+
env.each do |key,value|
|
25
|
+
case key
|
26
|
+
when "HTTP_X_TURN_TOKEN"
|
27
|
+
tokens[:turn] = value
|
28
|
+
when "HTTP_X_OBSERVER_TOKEN"
|
29
|
+
tokens[:observer] = value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
body = env["rack.input"].read
|
33
|
+
unless body.empty?
|
34
|
+
payload = JSON.parse("[#{body}]").first
|
35
|
+
end
|
36
|
+
process_request(
|
37
|
+
:method => env["REQUEST_METHOD"],
|
38
|
+
:path => env["REQUEST_PATH"],
|
39
|
+
:payload => payload,
|
40
|
+
:tokens => tokens,
|
41
|
+
:async_cb => env['async.callback'])
|
42
|
+
end
|
43
|
+
|
44
|
+
def process_request(request={})
|
45
|
+
handler = case request[:path]
|
46
|
+
when %r{^/$} then Gameworks::Servlet::GameList
|
47
|
+
when %r{^/match$} then Gameworks::Servlet::MatchMaker
|
48
|
+
when %r{^/[^/]+/?$} then Gameworks::Servlet::GameView
|
49
|
+
when %r{^/[^/]+/players$} then Gameworks::Servlet::AddPlayer
|
50
|
+
when %r{^/[^/]+/moves$} then Gameworks::Servlet::AddMove
|
51
|
+
when %r{^/[^/]+/observers$} then Gameworks::Servlet::AddObserver
|
52
|
+
end
|
53
|
+
|
54
|
+
if handler
|
55
|
+
begin
|
56
|
+
handler.process(self, request)
|
57
|
+
rescue Exception => e
|
58
|
+
puts e.inspect
|
59
|
+
puts e.backtrace
|
60
|
+
[ 500, {}, [] ]
|
61
|
+
end
|
62
|
+
else
|
63
|
+
[ 404, {}, [] ]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Gameworks
|
4
|
+
module Servlet
|
5
|
+
class AddMove < Gameworks::Servlet::Base
|
6
|
+
def POST(request)
|
7
|
+
game_id = request[:path].split('/')[1]
|
8
|
+
game = @server.game_registry.instance(game_id)
|
9
|
+
return [404, {}, ["no such game"]] unless game
|
10
|
+
|
11
|
+
if game.state == Gameworks::Game::STATE_IN_PLAY
|
12
|
+
player = game.player_for_token(request[:tokens][:turn])
|
13
|
+
if player
|
14
|
+
snapshot = game.snapshot
|
15
|
+
legal, error = game.add_move(request[:payload], player)
|
16
|
+
if legal
|
17
|
+
player.wait_for_turn do |turn_token|
|
18
|
+
EventMachine.add_timer(game.delay_time) do
|
19
|
+
request[:async_cb].call [ 200, {
|
20
|
+
'Content-Type' => 'application/json',
|
21
|
+
'X-Turn-Token' => turn_token
|
22
|
+
}, [game.delta(snapshot, player).to_json] ]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
[-1, {}, []]
|
26
|
+
else
|
27
|
+
# invalid move!
|
28
|
+
game.disqualify(player)
|
29
|
+
[403, {}, ["invalid move: #{error}. disqualified!"]]
|
30
|
+
end
|
31
|
+
else
|
32
|
+
# not your turn!
|
33
|
+
[403, {}, ["invalid/missing turn token"]]
|
34
|
+
end
|
35
|
+
else
|
36
|
+
if game.state == Gameworks::Game::STATE_INITIATING
|
37
|
+
# not yet allowed
|
38
|
+
[403, {}, ["game not yet started"]]
|
39
|
+
else
|
40
|
+
# no longer available
|
41
|
+
[410, {}, ["game finished"]]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Gameworks
|
4
|
+
module Servlet
|
5
|
+
class AddObserver < Gameworks::Servlet::Base
|
6
|
+
AsyncResponse = [-1, {}, []].freeze
|
7
|
+
|
8
|
+
class DeferrableBody
|
9
|
+
include EventMachine::Deferrable
|
10
|
+
|
11
|
+
def initialize(wrapper)
|
12
|
+
@wrapper = wrapper || "%s"
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(json_chunk)
|
16
|
+
document = json_chunk.gsub("\n", ' ') + "\n"
|
17
|
+
@body_callback.call(@wrapper % [document])
|
18
|
+
end
|
19
|
+
|
20
|
+
def each(&blk)
|
21
|
+
@body_callback = blk
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def POST(request)
|
26
|
+
game_id = request[:path].split('/')[1]
|
27
|
+
game = @server.game_registry.instance(game_id)
|
28
|
+
return [404, {}, ["no such game"]] unless game
|
29
|
+
wrapper = (request[:payload] || {})['wrapper']
|
30
|
+
body = DeferrableBody.new(wrapper)
|
31
|
+
token = game.register_observer(body)
|
32
|
+
|
33
|
+
# application/x-multi-json if a format i just made up,
|
34
|
+
# which is a series of json documents separated by newlines
|
35
|
+
# (there will be no newlines within the json document)
|
36
|
+
request[:async_cb].call [200,
|
37
|
+
{'Content-Type' => 'application/x-multi-json',
|
38
|
+
'X-Observer-Token' => token}, body]
|
39
|
+
body.call(game.to_json)
|
40
|
+
AsyncResponse
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Gameworks
|
4
|
+
module Servlet
|
5
|
+
class AddPlayer < Gameworks::Servlet::Base
|
6
|
+
def POST(request)
|
7
|
+
game_id = request[:path].split('/')[1]
|
8
|
+
game = @server.game_registry.instance(game_id)
|
9
|
+
return [404, {}, ["no such game"]] unless game
|
10
|
+
|
11
|
+
if game.state == Gameworks::Game::STATE_INITIATING
|
12
|
+
if player = game.add_player(request[:payload])
|
13
|
+
game.start_if_ready
|
14
|
+
player.wait_for_turn do |turn_token|
|
15
|
+
request[:async_cb].call [ 200, {
|
16
|
+
'Content-Type' => 'application/json',
|
17
|
+
'X-Turn-Token' => turn_token
|
18
|
+
}, [game.to_json(player)] ]
|
19
|
+
end
|
20
|
+
[-1, {}, []]
|
21
|
+
else
|
22
|
+
[403, {}, ["invalid player data"]]
|
23
|
+
end
|
24
|
+
else
|
25
|
+
[410, {}, ["game already started"]]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Gameworks
|
2
|
+
module Servlet
|
3
|
+
class Base
|
4
|
+
def initialize(server)
|
5
|
+
@server = server
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.process(server, request)
|
9
|
+
servlet = self.new(server)
|
10
|
+
if servlet.respond_to?(request[:method])
|
11
|
+
servlet.send(request[:method], request)
|
12
|
+
else
|
13
|
+
servlet.method_not_allowed
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_not_allowed
|
18
|
+
[ 405, { 'Allow' => ['GET', 'POST'].select{ |m| respond_to?(m) }.join(', ') }, [] ]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Gameworks
|
4
|
+
module Servlet
|
5
|
+
class GameList < Gameworks::Servlet::Base
|
6
|
+
def POST(request)
|
7
|
+
game = @server.game_class.new(request[:payload])
|
8
|
+
if game.valid?
|
9
|
+
@server.game_registry.add(game)
|
10
|
+
[201, {'Location' => "/#{game.id}"}, []]
|
11
|
+
else
|
12
|
+
return [403, {}, ["invalid game data"]]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def GET(request)
|
17
|
+
[200, {}, [@server.game_registry.to_json]]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Gameworks
|
4
|
+
module Servlet
|
5
|
+
class GameView < Gameworks::Servlet::Base
|
6
|
+
def GET(request)
|
7
|
+
game_id = request[:path].split('/')[1]
|
8
|
+
game = @server.game_registry.instance(game_id)
|
9
|
+
return [404, {}, ["no such game"]] unless game
|
10
|
+
|
11
|
+
snapshot = nil
|
12
|
+
if request[:tokens][:observer]
|
13
|
+
snapshot = game.snapshot_for_observer(request[:tokens][:observer])
|
14
|
+
end
|
15
|
+
|
16
|
+
if snapshot
|
17
|
+
[ 200, { 'Content-Type' => 'application/json' }, [game.delta(snapshot).to_json] ]
|
18
|
+
else
|
19
|
+
[ 200, { 'Content-Type' => 'application/json' }, [game.to_json] ]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Gameworks
|
4
|
+
module Servlet
|
5
|
+
class MatchMaker < Gameworks::Servlet::Base
|
6
|
+
class Promise
|
7
|
+
def initialize
|
8
|
+
@em_queue = EventMachine::Queue.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def demand
|
12
|
+
@em_queue.pop{ |value| yield value }
|
13
|
+
end
|
14
|
+
|
15
|
+
def fulfill(value)
|
16
|
+
@em_queue.push(value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_accessor :server
|
22
|
+
|
23
|
+
SEATS = 2
|
24
|
+
|
25
|
+
# on the class because I want it persistent
|
26
|
+
def request_game(&blk)
|
27
|
+
promise = Promise.new
|
28
|
+
promise.demand(&blk)
|
29
|
+
queue.push(promise)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
def queue
|
34
|
+
unless @queue
|
35
|
+
@queue = EventMachine::Queue.new
|
36
|
+
find_match
|
37
|
+
end
|
38
|
+
@queue
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_match
|
42
|
+
match = []
|
43
|
+
SEATS.times do
|
44
|
+
queue.pop do |request|
|
45
|
+
match << request
|
46
|
+
if match.size == SEATS
|
47
|
+
run_match(match)
|
48
|
+
find_match
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def run_match(match)
|
55
|
+
game = server.game_class.random('seats' => SEATS)
|
56
|
+
server.game_registry.add(game)
|
57
|
+
match.each{ |request| request.fulfill(game) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def GET(request)
|
62
|
+
MatchMaker.server ||= @server
|
63
|
+
MatchMaker.request_game do |game|
|
64
|
+
request[:async_cb].call [201, {'Location' => "/#{game.id}"}, []]
|
65
|
+
end
|
66
|
+
[-1, {}, []]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
metadata
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gameworks
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Palmer
|
8
|
+
- Jacob Fugal
|
9
|
+
- Travis Elnicky
|
10
|
+
- Dan Dorman
|
11
|
+
- Simon Williams
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
date: 2015-07-07 00:00:00.000000000 Z
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: rake
|
19
|
+
requirement: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - "~>"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '10.0'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '10.0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: eventmachine
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - "~>"
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.0'
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.0.7
|
41
|
+
type: :runtime
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 1.0.7
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: json
|
53
|
+
requirement: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '1.8'
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.8.3
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.8'
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 1.8.3
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: thin
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - "~>"
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '1.6'
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 1.6.3
|
81
|
+
type: :runtime
|
82
|
+
prerelease: false
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '1.6'
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 1.6.3
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: bundler
|
93
|
+
requirement: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '1.8'
|
98
|
+
type: :development
|
99
|
+
prerelease: false
|
100
|
+
version_requirements: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '1.8'
|
105
|
+
- !ruby/object:Gem::Dependency
|
106
|
+
name: rspec
|
107
|
+
requirement: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '3.3'
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: 3.3.0
|
115
|
+
type: :development
|
116
|
+
prerelease: false
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - "~>"
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '3.3'
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 3.3.0
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec-eventmachine
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.2'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.2'
|
139
|
+
description: A ruby game framework used for final problems in instructure mebipenny
|
140
|
+
competitions.
|
141
|
+
email:
|
142
|
+
- brianp@instructure.com
|
143
|
+
- jacob@instructure.com
|
144
|
+
- telnicky@instructure.com
|
145
|
+
- ddorman@instructure.com
|
146
|
+
- simon@instructure.com
|
147
|
+
executables: []
|
148
|
+
extensions: []
|
149
|
+
extra_rdoc_files: []
|
150
|
+
files:
|
151
|
+
- Gemfile
|
152
|
+
- Gemfile.lock
|
153
|
+
- README
|
154
|
+
- Rakefile
|
155
|
+
- bin/console
|
156
|
+
- bin/setup
|
157
|
+
- gameworks.gemspec
|
158
|
+
- lib/gameworks.rb
|
159
|
+
- lib/gameworks/fuse.rb
|
160
|
+
- lib/gameworks/game.rb
|
161
|
+
- lib/gameworks/game_registry.rb
|
162
|
+
- lib/gameworks/game_snapshot.rb
|
163
|
+
- lib/gameworks/player.rb
|
164
|
+
- lib/gameworks/server.rb
|
165
|
+
- lib/gameworks/servlet/add_move.rb
|
166
|
+
- lib/gameworks/servlet/add_observer.rb
|
167
|
+
- lib/gameworks/servlet/add_player.rb
|
168
|
+
- lib/gameworks/servlet/base.rb
|
169
|
+
- lib/gameworks/servlet/game_list.rb
|
170
|
+
- lib/gameworks/servlet/game_view.rb
|
171
|
+
- lib/gameworks/servlet/match_maker.rb
|
172
|
+
- lib/gameworks/version.rb
|
173
|
+
homepage: https://instructure.github.io
|
174
|
+
licenses:
|
175
|
+
- MIT
|
176
|
+
metadata: {}
|
177
|
+
post_install_message:
|
178
|
+
rdoc_options: []
|
179
|
+
require_paths:
|
180
|
+
- lib
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - ">="
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
requirements: []
|
192
|
+
rubyforge_project:
|
193
|
+
rubygems_version: 2.4.6
|
194
|
+
signing_key:
|
195
|
+
specification_version: 4
|
196
|
+
summary: A game framework
|
197
|
+
test_files: []
|
198
|
+
has_rdoc:
|