gameworks 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|