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 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
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
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
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
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,2 @@
1
+ require_relative "gameworks/game"
2
+ require_relative "gameworks/server"
@@ -0,0 +1,16 @@
1
+ require 'eventmachine'
2
+
3
+ module Gameworks
4
+ class Fuse
5
+ def initialize(seconds)
6
+ @aborted = false
7
+ EventMachine::add_timer(seconds) do
8
+ yield unless @aborted
9
+ end
10
+ end
11
+
12
+ def abort
13
+ @aborted = true
14
+ end
15
+ end
16
+ end
@@ -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,13 @@
1
+ module Gameworks
2
+ class Game
3
+ class Snapshot
4
+ attr_reader :player_scores, :state
5
+
6
+ def initialize(game)
7
+ @player_scores = {}
8
+ game.players.each{ |p| @player_scores[p] = p.score }
9
+ @state = game.state
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,3 @@
1
+ module Gameworks
2
+ VERSION = "1.0.0"
3
+ 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: