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.
@@ -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
+