chione 0.0.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
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.gemtest +0 -0
- data/.rdoc_options +22 -0
- data/.simplecov +14 -0
- data/ChangeLog +114 -0
- data/History.rdoc +9 -0
- data/Manifest.txt +27 -0
- data/README.rdoc +96 -0
- data/Rakefile +92 -0
- data/lib/chione.rb +37 -0
- data/lib/chione/aspect.rb +163 -0
- data/lib/chione/assemblage.rb +64 -0
- data/lib/chione/behaviors.rb +31 -0
- data/lib/chione/component.rb +43 -0
- data/lib/chione/entity.rb +80 -0
- data/lib/chione/manager.rb +44 -0
- data/lib/chione/mixins.rb +91 -0
- data/lib/chione/system.rb +69 -0
- data/lib/chione/world.rb +358 -0
- data/spec/chione/aspect_spec.rb +143 -0
- data/spec/chione/assemblage_spec.rb +60 -0
- data/spec/chione/component_spec.rb +54 -0
- data/spec/chione/entity_spec.rb +109 -0
- data/spec/chione/manager_spec.rb +39 -0
- data/spec/chione/mixins_spec.rb +94 -0
- data/spec/chione/system_spec.rb +72 -0
- data/spec/chione/world_spec.rb +451 -0
- data/spec/chione_spec.rb +11 -0
- data/spec/spec_helper.rb +34 -0
- metadata +250 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
#!/usr/bin/env rspec -cfd
|
2
|
+
|
3
|
+
require_relative '../spec_helper'
|
4
|
+
|
5
|
+
require 'chione/system'
|
6
|
+
|
7
|
+
|
8
|
+
describe Chione::System do
|
9
|
+
|
10
|
+
let( :location_component ) do
|
11
|
+
Class.new( Chione::Component ) do
|
12
|
+
field :x, default: 0
|
13
|
+
field :y, default: 0
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
let( :tags_component ) do
|
18
|
+
Class.new( Chione::Component ) do
|
19
|
+
field :tags, default: []
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
describe "a subclass" do
|
25
|
+
|
26
|
+
let( :subclass ) do
|
27
|
+
Class.new(described_class) do
|
28
|
+
def initialize( * )
|
29
|
+
super
|
30
|
+
@processed = false
|
31
|
+
end
|
32
|
+
attr_reader :processed
|
33
|
+
|
34
|
+
def process_loop
|
35
|
+
@processed = true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
it "has a default Aspect which matches all entities" do
|
42
|
+
expect( subclass.aspect ).to be_empty
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
it "can declare components for its aspect" do
|
47
|
+
subclass.aspect one_of: tags_component
|
48
|
+
|
49
|
+
expect( subclass.aspect ).to_not be_empty
|
50
|
+
expect( subclass.aspect.one_of ).to include( tags_component )
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
it "is required to implement #process_loop" do
|
55
|
+
expect {
|
56
|
+
Class.new( described_class ).new( :world ).process_loop
|
57
|
+
}.to raise_error( NotImplementedError, /implement required method/i )
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
it "runs a Thread in its #process_loop when started" do
|
62
|
+
system = subclass.new( :world )
|
63
|
+
system_thread = system.start
|
64
|
+
expect( system_thread ).to be_a( Thread )
|
65
|
+
system_thread.join( 2 )
|
66
|
+
expect( system.processed ).to be_truthy
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,451 @@
|
|
1
|
+
#!/usr/bin/env rspec -cfd
|
2
|
+
|
3
|
+
require_relative '../spec_helper'
|
4
|
+
|
5
|
+
require 'chione/world'
|
6
|
+
|
7
|
+
require 'chione/aspect'
|
8
|
+
require 'chione/assemblage'
|
9
|
+
require 'chione/component'
|
10
|
+
require 'chione/entity'
|
11
|
+
require 'chione/manager'
|
12
|
+
require 'chione/system'
|
13
|
+
|
14
|
+
|
15
|
+
describe Chione::World do
|
16
|
+
|
17
|
+
let( :world ) { described_class.new }
|
18
|
+
|
19
|
+
let( :test_system ) do
|
20
|
+
Class.new( Chione::System ) do
|
21
|
+
def self::name; "TestSystem"; end
|
22
|
+
def initialize( world, *args )
|
23
|
+
super
|
24
|
+
@args = args
|
25
|
+
@started = false
|
26
|
+
@stopped = false
|
27
|
+
@thread = nil
|
28
|
+
end
|
29
|
+
attr_reader :args, :started, :stopped, :thread
|
30
|
+
def start
|
31
|
+
@started = true
|
32
|
+
super
|
33
|
+
end
|
34
|
+
def stop
|
35
|
+
super
|
36
|
+
@stopped = true
|
37
|
+
end
|
38
|
+
def process_loop
|
39
|
+
loop do
|
40
|
+
sleep 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
let( :test_manager ) do
|
46
|
+
Class.new( Chione::Manager ) do
|
47
|
+
def self::name; "TestManager"; end
|
48
|
+
def initialize( world, *args )
|
49
|
+
super
|
50
|
+
@args = args
|
51
|
+
end
|
52
|
+
attr_reader :args, :started, :stopped
|
53
|
+
def start
|
54
|
+
@started = true
|
55
|
+
end
|
56
|
+
def stop
|
57
|
+
@stopped = true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
let( :location_component ) do
|
63
|
+
Class.new( Chione::Component ) do
|
64
|
+
def self::name
|
65
|
+
"Location"
|
66
|
+
end
|
67
|
+
field :x, default: 0
|
68
|
+
field :y, default: 0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
let( :tags_component ) do
|
73
|
+
Class.new( Chione::Component ) do
|
74
|
+
def self::name
|
75
|
+
"Tags"
|
76
|
+
end
|
77
|
+
field :tags, default: []
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
let( :color_component ) do
|
82
|
+
Class.new( Chione::Component ) do
|
83
|
+
def self::name
|
84
|
+
"Color"
|
85
|
+
end
|
86
|
+
field :hue, default: 0
|
87
|
+
field :shade, default: 0
|
88
|
+
field :value, default: 0
|
89
|
+
field :opacity, default: 0
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
let( :assemblage ) do
|
94
|
+
mod = Module.new
|
95
|
+
mod.extend( Chione::Assemblage )
|
96
|
+
mod.add( location_component, x: 10, y: 8 )
|
97
|
+
mod.add( tags_component, tags: [:foo, :bar] )
|
98
|
+
mod
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
it "joins on any Threads started by subsystems when stopping" do
|
103
|
+
system = world.add_system( test_system )
|
104
|
+
world.start
|
105
|
+
sleep 0.1 until world.running?
|
106
|
+
world.stop
|
107
|
+
expect( system.thread ).to_not be_alive
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
describe "configuration" do
|
112
|
+
|
113
|
+
it "is done via the Configurability API" do
|
114
|
+
new_stop_wait = Chione::World::CONFIG_DEFAULTS[:max_stop_wait] + 10
|
115
|
+
config = Configurability.default_config
|
116
|
+
config.gameworld = {
|
117
|
+
max_stop_wait: new_stop_wait
|
118
|
+
}
|
119
|
+
|
120
|
+
config.install
|
121
|
+
|
122
|
+
expect( described_class.max_stop_wait ).to eq( new_stop_wait )
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
describe "publish/subscribe" do
|
129
|
+
|
130
|
+
it "allows subscription to events" do
|
131
|
+
received = []
|
132
|
+
world.subscribe( 'test/subscription' ) {|*args| received << args }
|
133
|
+
expect {
|
134
|
+
world.publish( 'test/subscription' )
|
135
|
+
}.to change { received.length }.by( 1 )
|
136
|
+
|
137
|
+
expect( received ).to eq([ ['test/subscription', []] ])
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
it "allows more than one subscription to the same event pattern" do
|
142
|
+
received = []
|
143
|
+
world.subscribe( 'test/subscription' ) {|*args| received << 1 }
|
144
|
+
world.subscribe( 'test/subscription' ) {|*args| received << 2 }
|
145
|
+
expect {
|
146
|
+
world.publish( 'test/subscription' )
|
147
|
+
}.to change { received.length }.by( 2 )
|
148
|
+
|
149
|
+
expect( received ).to eq([ 1, 2 ])
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
it "removes subscriptions that raise errors" do
|
154
|
+
callback = world.subscribe( 'test/subscription' ) {|*args| raise "oops" }
|
155
|
+
expect {
|
156
|
+
Loggability.with_level( :fatal ) do
|
157
|
+
world.publish( 'test/subscription' )
|
158
|
+
end
|
159
|
+
}.to change { world.subscriptions['test/subscription'].length }.by( -1 )
|
160
|
+
expect( world.subscriptions['test/subscriptions'] ).to_not include( callback )
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
it "allows unsubscription from events via the returned callback" do
|
165
|
+
received = []
|
166
|
+
callback = world.subscribe( 'test/subscription' ) {|*args| received << args }
|
167
|
+
world.unsubscribe( callback )
|
168
|
+
|
169
|
+
expect {
|
170
|
+
world.publish( 'test/subscription' )
|
171
|
+
}.to_not change { received.length }
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
describe "with glob-style wildcard patterns" do
|
176
|
+
|
177
|
+
it "matches any one event segment with an asterisk" do
|
178
|
+
received = []
|
179
|
+
|
180
|
+
world.subscribe( 'test/*' ) {|*args| received << args }
|
181
|
+
|
182
|
+
expect {
|
183
|
+
world.publish( 'test/subscription', :stuff )
|
184
|
+
world.publish( 'test/other', 18, 2 )
|
185
|
+
world.publish( 'test/with/more', :chinchillas )
|
186
|
+
}.to change { received.length }.by( 2 )
|
187
|
+
|
188
|
+
expect( received ).to eq([
|
189
|
+
['test/subscription', [:stuff]],
|
190
|
+
['test/other', [18, 2]],
|
191
|
+
])
|
192
|
+
end
|
193
|
+
|
194
|
+
it "matches any number of event segments with a double-asterisk" do
|
195
|
+
received = []
|
196
|
+
|
197
|
+
world.subscribe( 'test/**/*' ) {|*args| received << args }
|
198
|
+
|
199
|
+
expect {
|
200
|
+
world.publish( 'test/subscription', :stuff )
|
201
|
+
world.publish( 'test/other', 22, 8 )
|
202
|
+
world.publish( 'test/with/more' )
|
203
|
+
world.publish( 'entity/something', 'leopards' )
|
204
|
+
}.to change { received.length }.by( 3 )
|
205
|
+
end
|
206
|
+
|
207
|
+
it "matches alternatives with a curly-braced list" do
|
208
|
+
received = []
|
209
|
+
|
210
|
+
world.subscribe( 'test/{foo,bar}' ) {|*args| received << args }
|
211
|
+
|
212
|
+
expect {
|
213
|
+
world.publish( 'test/foo', :calliope )
|
214
|
+
world.publish( 'test/bar', size: 8, length: 24.4 )
|
215
|
+
world.publish( 'test/bar/more', {} )
|
216
|
+
world.publish( 'test/more' )
|
217
|
+
}.to change { received.length }.by( 2 )
|
218
|
+
|
219
|
+
expect( received ).to eq([
|
220
|
+
[ 'test/foo', [:calliope] ],
|
221
|
+
[ 'test/bar', [{size: 8, length: 24.4}] ],
|
222
|
+
])
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
describe "entities" do
|
231
|
+
|
232
|
+
it "can create entities" do
|
233
|
+
expect( world.create_entity ).to be_a( Chione::Entity )
|
234
|
+
end
|
235
|
+
|
236
|
+
|
237
|
+
it "knows whether or not it has a particular entity" do
|
238
|
+
entity = world.create_entity
|
239
|
+
expect( world ).to have_entity( entity )
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
it "knows whether or not it has an entity with a given ID" do
|
244
|
+
entity = world.create_entity
|
245
|
+
id = entity.id
|
246
|
+
|
247
|
+
expect( world ).to have_entity( id )
|
248
|
+
end
|
249
|
+
|
250
|
+
|
251
|
+
it "can create entities using an Assemblage" do
|
252
|
+
entity = world.create_entity( assemblage )
|
253
|
+
|
254
|
+
expect( entity ).to be_a( Chione::Entity )
|
255
|
+
expect( entity.components.keys ).to include( *assemblage.components.keys )
|
256
|
+
end
|
257
|
+
|
258
|
+
|
259
|
+
it "publishes an `entity/created` event when it creates an Entity" do
|
260
|
+
event_payload = nil
|
261
|
+
|
262
|
+
world.subscribe( 'entity/created' ) {|*payload| event_payload = payload }
|
263
|
+
entity = world.create_entity
|
264
|
+
|
265
|
+
expect( event_payload ).to eq([ 'entity/created', [entity] ])
|
266
|
+
end
|
267
|
+
|
268
|
+
|
269
|
+
it "can destroy entities" do
|
270
|
+
entity = world.create_entity
|
271
|
+
world.destroy_entity( entity )
|
272
|
+
|
273
|
+
expect( world ).to_not have_entity( entity )
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
it "errors when trying to destroy an entity that was already destroyed" do
|
278
|
+
entity = world.create_entity
|
279
|
+
world.destroy_entity( entity )
|
280
|
+
expect {
|
281
|
+
world.destroy_entity( entity )
|
282
|
+
}.to raise_error( /does not contain entity \S+/i )
|
283
|
+
end
|
284
|
+
|
285
|
+
|
286
|
+
it "publishes an `entity/destroyed` event when it destroys an Entity" do
|
287
|
+
event_payload = nil
|
288
|
+
|
289
|
+
world.subscribe( 'entity/destroyed' ) {|*payload| event_payload = payload }
|
290
|
+
entity = world.create_entity
|
291
|
+
world.destroy_entity( entity )
|
292
|
+
|
293
|
+
expect( event_payload ).to eq([ 'entity/destroyed', [entity] ])
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
describe "components" do
|
300
|
+
|
301
|
+
let!( :entity1 ) do
|
302
|
+
obj = world.create_entity
|
303
|
+
obj.add_component( location_component.new )
|
304
|
+
obj
|
305
|
+
end
|
306
|
+
|
307
|
+
let!( :entity2 ) do
|
308
|
+
obj = world.create_entity
|
309
|
+
obj.add_component( location_component.new )
|
310
|
+
obj.add_component( tags_component.new )
|
311
|
+
obj
|
312
|
+
end
|
313
|
+
|
314
|
+
let!( :entity3 ) do
|
315
|
+
obj = world.create_entity
|
316
|
+
obj.add_component( tags_component.new )
|
317
|
+
obj
|
318
|
+
end
|
319
|
+
|
320
|
+
let( :location_aspect ) { Chione::Aspect.with_one_of(location_component) }
|
321
|
+
let( :tags_aspect ) { Chione::Aspect.with_one_of(tags_component) }
|
322
|
+
let( :colored_aspect ) { Chione::Aspect.with_one_of(color_component) }
|
323
|
+
let( :tagged_located_aspect ) do
|
324
|
+
Chione::Aspect.with_all_of( location_component, tags_component )
|
325
|
+
end
|
326
|
+
|
327
|
+
|
328
|
+
it "can look up sets of entities which match Aspects" do
|
329
|
+
entities_with_location = world.entities_with( location_aspect )
|
330
|
+
expect( entities_with_location ).to include( entity1, entity2 )
|
331
|
+
expect( entities_with_location ).to_not include( entity3 )
|
332
|
+
|
333
|
+
entities_with_tags = world.entities_with( tags_aspect )
|
334
|
+
expect( entities_with_tags ).to include( entity2, entity3 )
|
335
|
+
expect( entities_with_tags ).to_not include( entity1 )
|
336
|
+
|
337
|
+
entities_with_both = world.entities_with( tagged_located_aspect )
|
338
|
+
expect( entities_with_both ).to include( entity2 )
|
339
|
+
expect( entities_with_both ).to_not include( entity1, entity3 )
|
340
|
+
|
341
|
+
entities_with_color = world.entities_with( colored_aspect )
|
342
|
+
expect( entities_with_color ).to be_empty
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
it "can look up sets of entities which match a System's aspect" do
|
347
|
+
system = Class.new( Chione::System )
|
348
|
+
system.for_entities_that_have \
|
349
|
+
all_of: [ location_component ],
|
350
|
+
one_of: [ tags_component, color_component ]
|
351
|
+
|
352
|
+
entities = world.entities_for( system )
|
353
|
+
|
354
|
+
expect( entities ).to include( entity2 )
|
355
|
+
expect( entities ).to_not include( entity1, entity3 )
|
356
|
+
end
|
357
|
+
|
358
|
+
end
|
359
|
+
|
360
|
+
|
361
|
+
describe "systems" do
|
362
|
+
|
363
|
+
it "can have Systems added to it" do
|
364
|
+
system = world.add_system( test_system )
|
365
|
+
expect( world.systems ).to include( test_system )
|
366
|
+
expect( world.systems[test_system] ).to be( system )
|
367
|
+
end
|
368
|
+
|
369
|
+
|
370
|
+
it "can register Systems constructed with custom arguments" do
|
371
|
+
system = world.add_system( test_system, 1, 2 )
|
372
|
+
expect( system.args ).to eq([ 1, 2 ])
|
373
|
+
end
|
374
|
+
|
375
|
+
|
376
|
+
it "broadcasts a `system/added` event when a System is added" do
|
377
|
+
event_payload = nil
|
378
|
+
world.subscribe( 'system/added' ) {|*payload| event_payload = payload }
|
379
|
+
|
380
|
+
sys = world.add_system( test_system )
|
381
|
+
|
382
|
+
expect( event_payload ).to eq([ 'system/added', [test_system] ])
|
383
|
+
end
|
384
|
+
|
385
|
+
|
386
|
+
it "starts its systems when it starts up" do
|
387
|
+
system = world.add_system( test_system )
|
388
|
+
world.start
|
389
|
+
sleep 0.1 until world.running?
|
390
|
+
world.stop
|
391
|
+
expect( system.started ).to be_truthy
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
it "starts a system as soon as it's added if it's already started" do
|
396
|
+
world.start
|
397
|
+
sleep 0.1 until world.running?
|
398
|
+
system = world.add_system( test_system )
|
399
|
+
world.stop
|
400
|
+
expect( system.started ).to be_truthy
|
401
|
+
end
|
402
|
+
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
describe "managers" do
|
407
|
+
|
408
|
+
it "can register Managers" do
|
409
|
+
manager = world.add_manager( test_manager )
|
410
|
+
expect( world.managers ).to include( test_manager )
|
411
|
+
expect( world.managers[test_manager] ).to be( manager )
|
412
|
+
end
|
413
|
+
|
414
|
+
|
415
|
+
it "can register Managers constructed with custom arguments" do
|
416
|
+
manager = world.add_manager( test_manager, 1, 2 )
|
417
|
+
expect( manager.args ).to eq([ 1, 2 ])
|
418
|
+
end
|
419
|
+
|
420
|
+
|
421
|
+
it "broadcasts a `manager/added` event when a Manager is added" do
|
422
|
+
event_payload = nil
|
423
|
+
world.subscribe( 'manager/added' ) {|*payload| event_payload = payload }
|
424
|
+
|
425
|
+
manager = world.add_manager( test_manager )
|
426
|
+
|
427
|
+
expect( event_payload ).to eq([ 'manager/added', [test_manager] ])
|
428
|
+
end
|
429
|
+
|
430
|
+
|
431
|
+
it "starts its managers when it starts up" do
|
432
|
+
manager = world.add_manager( test_manager )
|
433
|
+
world.start
|
434
|
+
sleep 0.1 until world.running?
|
435
|
+
world.stop
|
436
|
+
expect( manager.started ).to be_truthy
|
437
|
+
end
|
438
|
+
|
439
|
+
|
440
|
+
it "starts a manager as soon as it's added if it's already been started" do
|
441
|
+
world.start
|
442
|
+
sleep 0.1 until world.running?
|
443
|
+
manager = world.add_manager( test_manager )
|
444
|
+
world.stop
|
445
|
+
expect( manager.started ).to be_truthy
|
446
|
+
end
|
447
|
+
|
448
|
+
end
|
449
|
+
|
450
|
+
end
|
451
|
+
|