chione 0.0.2

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