gameworks 1.0.0

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