joyce 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +4 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +20 -0
- data/README.md +241 -0
- data/Rakefile +31 -0
- data/bin/joyce +14 -0
- data/dump.rdb +1 -0
- data/example/application.rb +203 -0
- data/example/models/game.rb +66 -0
- data/example/models/player.rb +6 -0
- data/features/.gitkeep +0 -0
- data/features/joyce.feature +1 -0
- data/features/step_definitions/.gitkeep +0 -0
- data/features/step_definitions/joyce_steps.rb +1 -0
- data/gemspec.yml +21 -0
- data/joyce.gemspec +60 -0
- data/lib/joyce.rb +137 -0
- data/lib/joyce/application.rb +61 -0
- data/lib/joyce/version.rb +4 -0
- data/spec/joyce_spec.rb +91 -0
- data/spec/spec_helper.rb +10 -0
- metadata +212 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 17b6e2acc99201cb764f1396588e676e9ea884d9
|
4
|
+
data.tar.gz: 8b762837b8aaffcfe6383e2634132e4050d3858e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 83f8879e432dfd00cfaaa771d71f55d87112d6ec631af93ce5bcb46bbc91d4415b73dcc47ef06cee20f07b32b36315723783357322441b052eec750df8f2f387
|
7
|
+
data.tar.gz: 36053b131206f044b7e6587f09c34e166d04df90c01648d068149da0373f7fc2988821116236821d8eb98abc4a6b47fc97f2cd8dcaa59fb1319c887a6eef7429
|
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour --format documentation --tag ~@flaky
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown --title "joyce Documentation" --protected
|
data/ChangeLog.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2016 Joseph Weissman
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
# joyce
|
2
|
+
|
3
|
+
|
4
|
+
* [Homepage](https://rubygems.org/gems/joyce)
|
5
|
+
* [Documentation](http://rubydoc.info/gems/joyce/frames)
|
6
|
+
* [Email](mailto:jweissman1986 at gmail.com)
|
7
|
+
|
8
|
+
[](https://codeclimate.com/github//joyce)
|
9
|
+
|
10
|
+
## Description
|
11
|
+
|
12
|
+
Joyce is a multiplayer game server framework built on top of Metacosm
|
13
|
+
|
14
|
+
## What is the big idea?
|
15
|
+
|
16
|
+
The idea is to build 'isomorphic' Ruby games: to target both a Gosu game client as well as a game server
|
17
|
+
running in the cloud. Both are running the same code, but the responsibilities are split:
|
18
|
+
|
19
|
+
- The game server runs game business logic, and publishes events that the clients consume to hydrate their local views
|
20
|
+
- Clients publish commands the the server validates and handles, updating game state and generating events
|
21
|
+
|
22
|
+
One upshot of this is that all game processing is kept on the server, and clients are doing nothing but
|
23
|
+
rendering views and when necessary figuring out what commands to publish (when the user interacts).
|
24
|
+
|
25
|
+
## Features
|
26
|
+
|
27
|
+
## Examples
|
28
|
+
|
29
|
+
Below is the source for a toy app that displays a list of other connected players and how long they've been connected. The server uses a ping command from clients to determine if players have disconnected.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
require 'joyce'
|
33
|
+
module Example
|
34
|
+
class SampleAppView < Joyce::ApplicationView
|
35
|
+
def render
|
36
|
+
game_view.render(window, font)
|
37
|
+
end
|
38
|
+
|
39
|
+
def game_view
|
40
|
+
GameView.find_by active_player_id: application.player_id
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class SampleServer < Joyce::Server
|
45
|
+
def setup
|
46
|
+
# p [ :sample_server_setup ]
|
47
|
+
Game.create
|
48
|
+
end
|
49
|
+
|
50
|
+
def tick
|
51
|
+
# p [ :sample_server_tick ]
|
52
|
+
Game.all.each(&:iterate!)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Application < Joyce::Application
|
57
|
+
viewed_with Example::SampleAppView
|
58
|
+
|
59
|
+
def setup
|
60
|
+
# p [ :sample_app_setup ]
|
61
|
+
GameView.create(active_player_id: player_id)
|
62
|
+
sim.params[:active_player_id] = player_id
|
63
|
+
end
|
64
|
+
|
65
|
+
def tick
|
66
|
+
@ticks ||= 0
|
67
|
+
@ticks += 1
|
68
|
+
# p [ :sample_app_tick ]
|
69
|
+
if (@ticks % 30 == 0)
|
70
|
+
fire(PingCommand.create(player_id: player_id, player_name: player_name))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def player_id
|
75
|
+
@player_id ||= SecureRandom.uuid
|
76
|
+
end
|
77
|
+
|
78
|
+
def player_name
|
79
|
+
@player_name ||= %w[ Alice Bob Cardiff Danielle Echo Fargo ].sample
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class BulletPointAtom
|
84
|
+
def render(color: 0xf0f0f0f0, location:,size: 10,window:)
|
85
|
+
x,y = location
|
86
|
+
window.draw_quad(x,y,color,
|
87
|
+
x,y+size,color,
|
88
|
+
x+size,y,color,
|
89
|
+
x+size,y+size,color)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class TextBoxAtom
|
94
|
+
def render(message:, location:, font:)
|
95
|
+
x,y = *location
|
96
|
+
font.draw(message, x, y, 1)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class ListMolecule
|
101
|
+
def render(title: '', elements:, location:, font:, window:)
|
102
|
+
x0,y0 = location
|
103
|
+
font.draw(title, x0, y0, 1)
|
104
|
+
elements.each_with_index do |element, index|
|
105
|
+
x, y = x0, y0 + ((index+1)*20)
|
106
|
+
|
107
|
+
bullet_point = BulletPointAtom.new
|
108
|
+
bullet_point.render(location: [x-10,y], window: window)
|
109
|
+
|
110
|
+
text_box = TextBoxAtom.new
|
111
|
+
text_box.render(message: element, location: [x,y], font: font)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class PlayerView < Metacosm::View
|
117
|
+
belongs_to :game_view
|
118
|
+
attr_accessor :name, :joined_at, :player_id
|
119
|
+
end
|
120
|
+
|
121
|
+
class GameView < Metacosm::View
|
122
|
+
has_many :player_views
|
123
|
+
|
124
|
+
attr_accessor :active_player_id, :game_id
|
125
|
+
# attr_accessor :player_names
|
126
|
+
|
127
|
+
def render(window, font)
|
128
|
+
# p [ :render_game_view, pinged_at: pinged_at ]
|
129
|
+
window.draw_quad(10,10, 0xf0f0f0f0,
|
130
|
+
10,20, 0xf0f0f0f0,
|
131
|
+
20,10, 0xf0f0f0f0,
|
132
|
+
20,20, 0xf0f0f0f0)
|
133
|
+
|
134
|
+
if player_names && player_names.any?
|
135
|
+
ListMolecule.new.render(
|
136
|
+
title: "Connected Users:",
|
137
|
+
elements: player_names,
|
138
|
+
location: [40,40],
|
139
|
+
font: font,
|
140
|
+
window: window
|
141
|
+
)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
def player_names
|
147
|
+
self.player_views.map { |p| "#{p.name} (#{time_ago_in_words(p.joined_at)})" } #(&:name)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class PingCommand < Metacosm::Command
|
152
|
+
attr_accessor :player_id, :player_name
|
153
|
+
end
|
154
|
+
|
155
|
+
class PingCommandHandler
|
156
|
+
def handle(player_id:, player_name:)
|
157
|
+
p [ :ping, from_player: player_id ]
|
158
|
+
game = Game.find_by(players: { id: player_id })
|
159
|
+
if game.nil?
|
160
|
+
# we could try to create a new game for the player?
|
161
|
+
# or add them to an existing one?
|
162
|
+
p [ :no_game_yet! ]
|
163
|
+
|
164
|
+
game = Game.first || Game.create
|
165
|
+
game.admit_player(player_id: player_id, player_name: player_name)
|
166
|
+
end
|
167
|
+
|
168
|
+
game.ping(player_id: player_id)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
#
|
173
|
+
# class GameController
|
174
|
+
# def ping(player_id:, player_name:)
|
175
|
+
# game = Game.find_by ...
|
176
|
+
# end
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
|
180
|
+
class ApplicationEventListener < Metacosm::EventListener
|
181
|
+
def game_view
|
182
|
+
GameView.find_by(active_player_id: active_player_id)
|
183
|
+
end
|
184
|
+
|
185
|
+
def active_player_id
|
186
|
+
self.simulation.params[:active_player_id]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class PlayerAdmittedEvent < Metacosm::Event
|
191
|
+
attr_accessor :player_id, :player_name, :connected_player_list
|
192
|
+
end
|
193
|
+
|
194
|
+
class PlayerAdmittedEventListener < ApplicationEventListener
|
195
|
+
def receive(player_id:, player_name:, connected_player_list:)
|
196
|
+
p [ :player_admitted_event_listener ]
|
197
|
+
if game_view
|
198
|
+
# game_view.create_player_view(player_id: player_id, name: player_name, joined_at: Time.now)
|
199
|
+
# go through connected player list and make views...
|
200
|
+
connected_player_list.each do |id:, name:, joined_at:|
|
201
|
+
player_view = game_view.player_views.where(player_id: id).first_or_create
|
202
|
+
player_view.update(name: name, joined_at: joined_at)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
class PlayerDroppedEvent < Metacosm::Event
|
209
|
+
attr_accessor :player_id, :connected_player_list
|
210
|
+
end
|
211
|
+
|
212
|
+
class PlayerDroppedEventListener < ApplicationEventListener
|
213
|
+
def receive(player_id:, connected_player_list:)
|
214
|
+
p [ :player_dropped_event_listener! ]
|
215
|
+
if game_view
|
216
|
+
game_view.player_views.map(&:destroy)
|
217
|
+
connected_player_list.each do |id:, name:, joined_at:|
|
218
|
+
player_view = game_view.player_views.where(player_id: id).first_or_create
|
219
|
+
player_view.update(name: name, joined_at: joined_at)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
```
|
226
|
+
|
227
|
+
## Requirements
|
228
|
+
|
229
|
+
## Install
|
230
|
+
|
231
|
+
$ gem install joyce
|
232
|
+
|
233
|
+
## Synopsis
|
234
|
+
|
235
|
+
$ joyce
|
236
|
+
|
237
|
+
## Copyright
|
238
|
+
|
239
|
+
Copyright (c) 2016 Joseph Weissman
|
240
|
+
|
241
|
+
See {file:LICENSE.txt} for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bundler/setup'
|
7
|
+
rescue LoadError => e
|
8
|
+
abort e.message
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rake'
|
12
|
+
|
13
|
+
|
14
|
+
require 'rubygems/tasks'
|
15
|
+
Gem::Tasks.new
|
16
|
+
|
17
|
+
require 'rspec/core/rake_task'
|
18
|
+
RSpec::Core::RakeTask.new
|
19
|
+
|
20
|
+
task :test => :spec
|
21
|
+
task :default => :spec
|
22
|
+
|
23
|
+
require 'yard'
|
24
|
+
YARD::Rake::YardocTask.new
|
25
|
+
task :doc => :yard
|
26
|
+
|
27
|
+
require 'cucumber/rake/task'
|
28
|
+
|
29
|
+
Cucumber::Rake::Task.new do |t|
|
30
|
+
t.cucumber_opts = %w[--format pretty]
|
31
|
+
end
|
data/bin/joyce
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
root = File.expand_path(File.join(File.dirname(__FILE__),'..'))
|
4
|
+
if File.directory?(File.join(root,'.git'))
|
5
|
+
Dir.chdir(root) do
|
6
|
+
begin
|
7
|
+
require 'bundler/setup'
|
8
|
+
rescue LoadError => e
|
9
|
+
warn e.message
|
10
|
+
warn "Run `gem install bundler` to install Bundler"
|
11
|
+
exit -1
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/dump.rdb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
REDIS0006�ܳC�Z��V
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require_relative 'models/game'
|
2
|
+
require_relative 'models/player'
|
3
|
+
|
4
|
+
module Example
|
5
|
+
class SampleAppView < Joyce::ApplicationView
|
6
|
+
def render
|
7
|
+
game_view.render(window, font)
|
8
|
+
end
|
9
|
+
|
10
|
+
def game_view
|
11
|
+
GameView.find_by active_player_id: application.player_id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class SampleServer < Joyce::Server
|
16
|
+
def setup
|
17
|
+
# p [ :sample_server_setup ]
|
18
|
+
Game.create
|
19
|
+
end
|
20
|
+
|
21
|
+
def tick
|
22
|
+
# p [ :sample_server_tick ]
|
23
|
+
Game.all.each(&:iterate!)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Application < Joyce::Application
|
28
|
+
viewed_with Example::SampleAppView
|
29
|
+
|
30
|
+
def setup
|
31
|
+
# p [ :sample_app_setup ]
|
32
|
+
GameView.create(active_player_id: player_id)
|
33
|
+
sim.params[:active_player_id] = player_id
|
34
|
+
end
|
35
|
+
|
36
|
+
def tick
|
37
|
+
@ticks ||= 0
|
38
|
+
@ticks += 1
|
39
|
+
# p [ :sample_app_tick ]
|
40
|
+
if (@ticks % 30 == 0)
|
41
|
+
fire(PingCommand.create(player_id: player_id, player_name: player_name))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def player_id
|
46
|
+
@player_id ||= SecureRandom.uuid
|
47
|
+
end
|
48
|
+
|
49
|
+
def player_name
|
50
|
+
@player_name ||= %w[ Alice Bob Cardiff Danielle Echo Fargo ].sample
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class BulletPointAtom
|
55
|
+
def render(color: 0xf0f0f0f0, location:,size: 10,window:)
|
56
|
+
x,y = location
|
57
|
+
window.draw_quad(x,y,color,
|
58
|
+
x,y+size,color,
|
59
|
+
x+size,y,color,
|
60
|
+
x+size,y+size,color)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class TextBoxAtom
|
65
|
+
def render(message:, location:, font:)
|
66
|
+
x,y = *location
|
67
|
+
font.draw(message, x, y, 1)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class ListMolecule
|
72
|
+
def render(title: '', elements:, location:, font:, window:)
|
73
|
+
x0,y0 = location
|
74
|
+
font.draw(title, x0, y0, 1)
|
75
|
+
elements.each_with_index do |element, index|
|
76
|
+
x, y = x0, y0 + ((index+1)*20)
|
77
|
+
|
78
|
+
bullet_point = BulletPointAtom.new
|
79
|
+
bullet_point.render(location: [x-10,y], window: window)
|
80
|
+
|
81
|
+
text_box = TextBoxAtom.new
|
82
|
+
text_box.render(message: element, location: [x,y], font: font)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class PlayerView < Metacosm::View
|
88
|
+
belongs_to :game_view
|
89
|
+
attr_accessor :name, :joined_at, :player_id
|
90
|
+
end
|
91
|
+
|
92
|
+
class GameView < Metacosm::View
|
93
|
+
has_many :player_views
|
94
|
+
|
95
|
+
attr_accessor :active_player_id, :game_id
|
96
|
+
# attr_accessor :player_names
|
97
|
+
|
98
|
+
def render(window, font)
|
99
|
+
# p [ :render_game_view, pinged_at: pinged_at ]
|
100
|
+
window.draw_quad(10,10, 0xf0f0f0f0,
|
101
|
+
10,20, 0xf0f0f0f0,
|
102
|
+
20,10, 0xf0f0f0f0,
|
103
|
+
20,20, 0xf0f0f0f0)
|
104
|
+
|
105
|
+
if player_names && player_names.any?
|
106
|
+
ListMolecule.new.render(
|
107
|
+
title: "Connected Users:",
|
108
|
+
elements: player_names,
|
109
|
+
location: [40,40],
|
110
|
+
font: font,
|
111
|
+
window: window
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
def player_names
|
118
|
+
self.player_views.map { |p| "#{p.name} (#{time_ago_in_words(p.joined_at)})" } #(&:name)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class PingCommand < Metacosm::Command
|
123
|
+
attr_accessor :player_id, :player_name
|
124
|
+
end
|
125
|
+
|
126
|
+
class PingCommandHandler
|
127
|
+
def handle(player_id:, player_name:)
|
128
|
+
p [ :ping, from_player: player_id ]
|
129
|
+
game = Game.find_by(players: { id: player_id })
|
130
|
+
if game.nil?
|
131
|
+
# we could try to create a new game for the player?
|
132
|
+
# or add them to an existing one?
|
133
|
+
p [ :no_game_yet! ]
|
134
|
+
|
135
|
+
game = Game.first || Game.create
|
136
|
+
game.admit_player(player_id: player_id, player_name: player_name)
|
137
|
+
end
|
138
|
+
|
139
|
+
game.ping(player_id: player_id)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
#
|
144
|
+
# class GameController
|
145
|
+
# def ping(player_id:, player_name:)
|
146
|
+
# game = Game.find_by ...
|
147
|
+
# end
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
|
151
|
+
class ApplicationEventListener < Metacosm::EventListener
|
152
|
+
def game_view
|
153
|
+
GameView.find_by(active_player_id: active_player_id)
|
154
|
+
end
|
155
|
+
|
156
|
+
def active_player_id
|
157
|
+
self.simulation.params[:active_player_id]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class PlayerAdmittedEvent < Metacosm::Event
|
162
|
+
attr_accessor :player_id, :player_name, :connected_player_list
|
163
|
+
end
|
164
|
+
|
165
|
+
class PlayerAdmittedEventListener < ApplicationEventListener
|
166
|
+
def receive(player_id:, player_name:, connected_player_list:)
|
167
|
+
p [ :player_admitted_event_listener ]
|
168
|
+
if game_view
|
169
|
+
# game_view.create_player_view(player_id: player_id, name: player_name, joined_at: Time.now)
|
170
|
+
# go through connected player list and make views...
|
171
|
+
connected_player_list.each do |id:, name:, joined_at:|
|
172
|
+
player_view = game_view.player_views.where(player_id: id).first_or_create
|
173
|
+
player_view.update(name: name, joined_at: joined_at)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class PlayerDroppedEvent < Metacosm::Event
|
180
|
+
attr_accessor :player_id, :connected_player_list
|
181
|
+
end
|
182
|
+
|
183
|
+
class PlayerDroppedEventListener < ApplicationEventListener
|
184
|
+
def receive(player_id:, connected_player_list:)
|
185
|
+
p [ :player_dropped_event_listener! ]
|
186
|
+
if game_view
|
187
|
+
game_view.player_views.map(&:destroy)
|
188
|
+
connected_player_list.each do |id:, name:, joined_at:|
|
189
|
+
player_view = game_view.player_views.where(player_id: id).first_or_create
|
190
|
+
player_view.update(name: name, joined_at: joined_at)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# TODO sagas in metacosm (also controllers for commands...)
|
197
|
+
# class PlayerSaga < Metacosm::Saga
|
198
|
+
# def player_dropped(player_id:, connected_player_list:)
|
199
|
+
# end
|
200
|
+
# def player_admitted(player_id:, player_name:, connected_player_list:)
|
201
|
+
# end
|
202
|
+
# end
|
203
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Example
|
2
|
+
class Game < Metacosm::Model
|
3
|
+
has_many :players
|
4
|
+
|
5
|
+
def iterate!
|
6
|
+
# check for dropped players
|
7
|
+
players_to_drop = self.players.all.select do |player|
|
8
|
+
player.pinged_at < 3.seconds.ago
|
9
|
+
end
|
10
|
+
|
11
|
+
players_to_drop.each do |player|
|
12
|
+
drop_player(player)
|
13
|
+
end
|
14
|
+
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def ping(player_id:)
|
19
|
+
player = self.players.where(id: player_id).first
|
20
|
+
player.update(pinged_at: Time.now)
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def admit_player(player_name:, player_id:)
|
25
|
+
self.players.create(
|
26
|
+
name: player_name,
|
27
|
+
id: player_id,
|
28
|
+
joined_at: Time.now,
|
29
|
+
pinged_at: Time.now
|
30
|
+
)
|
31
|
+
|
32
|
+
emit(
|
33
|
+
PlayerAdmittedEvent.create(
|
34
|
+
player_id: player_id,
|
35
|
+
player_name: player_name,
|
36
|
+
connected_player_list: connected_player_list,
|
37
|
+
)
|
38
|
+
)
|
39
|
+
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def drop_player(player)
|
45
|
+
lost_player_id = player.id
|
46
|
+
player.destroy
|
47
|
+
|
48
|
+
emit(
|
49
|
+
PlayerDroppedEvent.create(
|
50
|
+
player_id: lost_player_id,
|
51
|
+
connected_player_list: connected_player_list
|
52
|
+
)
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def connected_player_list
|
57
|
+
self.players.map do |player|
|
58
|
+
{
|
59
|
+
id: player.id,
|
60
|
+
name: player.name,
|
61
|
+
joined_at: player.joined_at
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/features/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
Feature: Blah blah blah
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
@wip
|
data/gemspec.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
name: joyce
|
2
|
+
summary: "multiplayer game server framework"
|
3
|
+
description: "isomorphic ruby games"
|
4
|
+
license: MIT
|
5
|
+
authors: Joseph Weissman
|
6
|
+
email: jweissman1986@gmail.com
|
7
|
+
homepage: https://rubygems.org/gems/joyce
|
8
|
+
|
9
|
+
dependencies:
|
10
|
+
gosu: ~> 0.10
|
11
|
+
metacosm: ~> 0.3
|
12
|
+
actionpack: ~> 4.2
|
13
|
+
|
14
|
+
development_dependencies:
|
15
|
+
bundler: ~> 1.10
|
16
|
+
codeclimate-test-reporter: ~> 0.1
|
17
|
+
cucumber: ~> 0.10.2
|
18
|
+
rake: ~> 10.0
|
19
|
+
rspec: ~> 3.0
|
20
|
+
rubygems-tasks: ~> 0.2
|
21
|
+
yard: ~> 0.8
|
data/joyce.gemspec
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gemspec = YAML.load_file('gemspec.yml')
|
7
|
+
|
8
|
+
gem.name = gemspec.fetch('name')
|
9
|
+
gem.version = gemspec.fetch('version') do
|
10
|
+
lib_dir = File.join(File.dirname(__FILE__),'lib')
|
11
|
+
$LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
|
12
|
+
|
13
|
+
require 'joyce/version'
|
14
|
+
Joyce::VERSION
|
15
|
+
end
|
16
|
+
|
17
|
+
gem.summary = gemspec['summary']
|
18
|
+
gem.description = gemspec['description']
|
19
|
+
gem.licenses = Array(gemspec['license'])
|
20
|
+
gem.authors = Array(gemspec['authors'])
|
21
|
+
gem.email = gemspec['email']
|
22
|
+
gem.homepage = gemspec['homepage']
|
23
|
+
|
24
|
+
glob = lambda { |patterns| gem.files & Dir[*patterns] }
|
25
|
+
|
26
|
+
gem.files = `git ls-files`.split($/)
|
27
|
+
gem.files = glob[gemspec['files']] if gemspec['files']
|
28
|
+
|
29
|
+
gem.executables = gemspec.fetch('executables') do
|
30
|
+
glob['bin/*'].map { |path| File.basename(path) }
|
31
|
+
end
|
32
|
+
gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
|
33
|
+
|
34
|
+
gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
|
35
|
+
gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb']
|
36
|
+
gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
|
37
|
+
|
38
|
+
gem.require_paths = Array(gemspec.fetch('require_paths') {
|
39
|
+
%w[ext lib].select { |dir| File.directory?(dir) }
|
40
|
+
})
|
41
|
+
|
42
|
+
gem.requirements = Array(gemspec['requirements'])
|
43
|
+
gem.required_ruby_version = gemspec['required_ruby_version']
|
44
|
+
gem.required_rubygems_version = gemspec['required_rubygems_version']
|
45
|
+
gem.post_install_message = gemspec['post_install_message']
|
46
|
+
|
47
|
+
split = lambda { |string| string.split(/,\s*/) }
|
48
|
+
|
49
|
+
if gemspec['dependencies']
|
50
|
+
gemspec['dependencies'].each do |name,versions|
|
51
|
+
gem.add_dependency(name,split[versions])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
if gemspec['development_dependencies']
|
56
|
+
gemspec['development_dependencies'].each do |name,versions|
|
57
|
+
gem.add_development_dependency(name,split[versions])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/joyce.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'gosu'
|
2
|
+
require 'redis' # TODO move require to mc/remote_sim?
|
3
|
+
require 'metacosm'
|
4
|
+
require 'metacosm/remote_simulation'
|
5
|
+
|
6
|
+
require 'action_view' # just for distance of time in words..
|
7
|
+
include ActionView::Helpers::DateHelper
|
8
|
+
|
9
|
+
require 'joyce/version'
|
10
|
+
require 'joyce/application'
|
11
|
+
|
12
|
+
module Joyce
|
13
|
+
EVENT_STREAM = :joyce_event_stream
|
14
|
+
COMMAND_QUEUE = :joyce_command_queue
|
15
|
+
|
16
|
+
class ApplicationView
|
17
|
+
attr_reader :application
|
18
|
+
|
19
|
+
def initialize(application)
|
20
|
+
@application ||= application
|
21
|
+
end
|
22
|
+
|
23
|
+
def render
|
24
|
+
# ...
|
25
|
+
end
|
26
|
+
|
27
|
+
def window
|
28
|
+
@application.window
|
29
|
+
end
|
30
|
+
|
31
|
+
def font
|
32
|
+
@font ||= Gosu::Font.new(18)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class ApplicationWindow < Gosu::Window
|
37
|
+
attr_accessor :width, :height
|
38
|
+
attr_reader :app
|
39
|
+
|
40
|
+
def initialize(app, width: 800, height: 600)
|
41
|
+
@app = app
|
42
|
+
self.width = width
|
43
|
+
self.height = height
|
44
|
+
|
45
|
+
super(self.width, self.height)
|
46
|
+
end
|
47
|
+
|
48
|
+
def draw
|
49
|
+
app.view.render
|
50
|
+
end
|
51
|
+
|
52
|
+
def update
|
53
|
+
app.tick
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class NullWindow
|
58
|
+
attr_reader :app
|
59
|
+
def initialize(app, width: 800, height: 600)
|
60
|
+
# p [ :created_null_window, provided_dimensions: [ width, height ] ]
|
61
|
+
@app = app
|
62
|
+
end
|
63
|
+
|
64
|
+
def show
|
65
|
+
app.tick
|
66
|
+
false
|
67
|
+
end
|
68
|
+
|
69
|
+
def draw_quad(*)
|
70
|
+
# p [ :null_window, :draw_quad ]
|
71
|
+
self
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Server
|
76
|
+
def setup
|
77
|
+
# p [ :server_setup ]
|
78
|
+
end
|
79
|
+
|
80
|
+
def boot
|
81
|
+
sim.on_event(publish_to: EVENT_STREAM)
|
82
|
+
@cmd_thread = sim.subscribe_for_commands(channel: COMMAND_QUEUE)
|
83
|
+
setup
|
84
|
+
sim.conduct!
|
85
|
+
drive!
|
86
|
+
# cmd_thread.join
|
87
|
+
end
|
88
|
+
|
89
|
+
def join
|
90
|
+
@cmd_thread.join
|
91
|
+
end
|
92
|
+
|
93
|
+
def tick
|
94
|
+
# p [ :server_tick ]
|
95
|
+
end
|
96
|
+
|
97
|
+
def drive!
|
98
|
+
@driving = true
|
99
|
+
Thread.new do
|
100
|
+
while @driving
|
101
|
+
tick
|
102
|
+
sleep 0.05
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def halt!
|
108
|
+
@driving = false
|
109
|
+
end
|
110
|
+
|
111
|
+
def sim
|
112
|
+
@simulation ||= Metacosm::Simulation.current
|
113
|
+
end
|
114
|
+
|
115
|
+
def received_commands
|
116
|
+
sim.received_commands
|
117
|
+
end
|
118
|
+
|
119
|
+
class << self
|
120
|
+
def kickstart!
|
121
|
+
server = new
|
122
|
+
server.boot
|
123
|
+
server
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class RemoteSim < Metacosm::RemoteSimulation
|
129
|
+
def initialize
|
130
|
+
super(COMMAND_QUEUE, EVENT_STREAM)
|
131
|
+
end
|
132
|
+
|
133
|
+
def redis_connection
|
134
|
+
::Redis.new
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Joyce
|
2
|
+
class Application
|
3
|
+
def initialize(headless: false)
|
4
|
+
@headless = headless
|
5
|
+
end
|
6
|
+
|
7
|
+
def setup
|
8
|
+
# ...
|
9
|
+
end
|
10
|
+
|
11
|
+
def tick
|
12
|
+
# ...
|
13
|
+
end
|
14
|
+
|
15
|
+
def launch
|
16
|
+
sim.conduct!
|
17
|
+
setup
|
18
|
+
window.show
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def fire(cmd)
|
23
|
+
sim.fire(cmd)
|
24
|
+
end
|
25
|
+
|
26
|
+
def received_events
|
27
|
+
sim.received_events
|
28
|
+
end
|
29
|
+
|
30
|
+
def sim
|
31
|
+
@simulation ||= RemoteSim.current
|
32
|
+
end
|
33
|
+
|
34
|
+
def view
|
35
|
+
@view ||= construct_view
|
36
|
+
end
|
37
|
+
|
38
|
+
def window
|
39
|
+
@window ||= @headless ? NullWindow.new(self) : ApplicationWindow.new(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def construct_view
|
44
|
+
self.class.view_class.new(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
attr_reader :view_class
|
49
|
+
def viewed_with(view_class)
|
50
|
+
@view_class ||= view_class
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def kickstart!(headless: false)
|
55
|
+
app = new(headless: headless)
|
56
|
+
app.launch
|
57
|
+
app
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/spec/joyce_spec.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Joyce do
|
4
|
+
it "should have a VERSION constant" do
|
5
|
+
expect(subject.const_get('VERSION')).to_not be_empty
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Joyce::Application do
|
10
|
+
describe '.kickstart' do
|
11
|
+
subject(:application) { instance_double(Joyce::Application) }
|
12
|
+
it 'should launch a new app instance' do
|
13
|
+
expect(Application).to receive(:new).and_return(application)
|
14
|
+
expect(application).to receive(:launch)
|
15
|
+
Application.kickstart!(headless: true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#launch" do
|
20
|
+
subject(:application) { Joyce::Application.new(headless: true) }
|
21
|
+
it 'should call #show on app window' do
|
22
|
+
expect(application.window).to receive(:show)
|
23
|
+
application.launch
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe Joyce::Server do
|
29
|
+
describe ".kickstart" do
|
30
|
+
subject(:server) { instance_double(Joyce::Server) }
|
31
|
+
it 'should call boot on a new server instance' do
|
32
|
+
expect(Server).to receive(:new).and_return(server)
|
33
|
+
expect(server).to receive(:boot)
|
34
|
+
Server.kickstart!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe Example::Application do
|
40
|
+
subject(:app) { Example::Application.new(headless: true) }
|
41
|
+
|
42
|
+
it 'should have a view with the indicated class' do
|
43
|
+
expect(app.view.class).to eq(Example::SampleAppView)
|
44
|
+
end
|
45
|
+
|
46
|
+
context "#launch" do
|
47
|
+
it 'should call setup' do
|
48
|
+
expect(app).to receive(:setup)
|
49
|
+
app.launch
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe Example::SampleAppView do
|
55
|
+
let(:sample_app) { Example::Application.new(headless: true) }
|
56
|
+
let(:window) { instance_double(ApplicationWindow) }
|
57
|
+
|
58
|
+
subject(:sample_app_view) { sample_app.view }
|
59
|
+
before { sample_app.setup }
|
60
|
+
|
61
|
+
it 'should render to the app window' do
|
62
|
+
expect(sample_app).to receive(:window).and_return(window)
|
63
|
+
expect(window).to receive(:draw_quad)
|
64
|
+
sample_app_view.render
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe Example::SampleServer, redis: true, flaky: true do
|
69
|
+
subject(:server) { Example::SampleServer.new }
|
70
|
+
let(:app) { Example::Application.new(headless: true) }
|
71
|
+
let(:command) { Example::PingCommand.create(player_id: 'the_player_id', player_name: 'Alice') }
|
72
|
+
let(:event) { Example::PlayerAdmittedEvent.create( player_name: "Alice", player_id: 'the_player_id', connected_player_list: connected_player_list) }
|
73
|
+
let(:connected_player_list) do
|
74
|
+
[
|
75
|
+
{ id: 'the_player_id', name: 'Alice', joined_at: 1.day.ago }
|
76
|
+
]
|
77
|
+
end
|
78
|
+
|
79
|
+
before do
|
80
|
+
app.launch
|
81
|
+
server.boot
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should receive commands from app' do
|
85
|
+
expect { app.fire(command); sleep 3 }.to change{server.received_commands.count}.by(1)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should propagate events to app' do
|
89
|
+
expect { server.sim.receive(event); sleep 3 }.to change{app.received_events.count}.by(1)
|
90
|
+
end
|
91
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: joyce
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joseph Weissman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: gosu
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: metacosm
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: actionpack
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: codeclimate-test-reporter
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.1'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: cucumber
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.10.2
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.10.2
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '10.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '10.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubygems-tasks
|
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
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: yard
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.8'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.8'
|
153
|
+
description: isomorphic ruby games
|
154
|
+
email: jweissman1986@gmail.com
|
155
|
+
executables:
|
156
|
+
- joyce
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files:
|
159
|
+
- ChangeLog.md
|
160
|
+
- LICENSE.txt
|
161
|
+
- README.md
|
162
|
+
files:
|
163
|
+
- ".document"
|
164
|
+
- ".gitignore"
|
165
|
+
- ".rspec"
|
166
|
+
- ".yardopts"
|
167
|
+
- ChangeLog.md
|
168
|
+
- Gemfile
|
169
|
+
- LICENSE.txt
|
170
|
+
- README.md
|
171
|
+
- Rakefile
|
172
|
+
- bin/joyce
|
173
|
+
- dump.rdb
|
174
|
+
- example/application.rb
|
175
|
+
- example/models/game.rb
|
176
|
+
- example/models/player.rb
|
177
|
+
- features/.gitkeep
|
178
|
+
- features/joyce.feature
|
179
|
+
- features/step_definitions/.gitkeep
|
180
|
+
- features/step_definitions/joyce_steps.rb
|
181
|
+
- gemspec.yml
|
182
|
+
- joyce.gemspec
|
183
|
+
- lib/joyce.rb
|
184
|
+
- lib/joyce/application.rb
|
185
|
+
- lib/joyce/version.rb
|
186
|
+
- spec/joyce_spec.rb
|
187
|
+
- spec/spec_helper.rb
|
188
|
+
homepage: https://rubygems.org/gems/joyce
|
189
|
+
licenses:
|
190
|
+
- MIT
|
191
|
+
metadata: {}
|
192
|
+
post_install_message:
|
193
|
+
rdoc_options: []
|
194
|
+
require_paths:
|
195
|
+
- lib
|
196
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
202
|
+
requirements:
|
203
|
+
- - ">="
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '0'
|
206
|
+
requirements: []
|
207
|
+
rubyforge_project:
|
208
|
+
rubygems_version: 2.4.5.1
|
209
|
+
signing_key:
|
210
|
+
specification_version: 4
|
211
|
+
summary: multiplayer game server framework
|
212
|
+
test_files: []
|