demiurge 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,583 @@
1
+ require_relative "../demiurge"
2
+
3
+ class Demiurge::Engine
4
+ # This method loads new World File code into an existing engine. It
5
+ # should be passed a list of filenames, normally roughly the same
6
+ # list that was passed to Demiurge::DSL.engine_from_dsl_files to
7
+ # create the engine initially.
8
+ #
9
+ # See {#reload_from_dsl_text} for more details and allowed
10
+ # options. It is identical in operation except for taking code as
11
+ # parameters instead of filenames.
12
+ #
13
+ # @see file:RELOADING.md
14
+ # @see Demiurge::DSL.engine_from_dsl_files
15
+ # @see #reload_from_dsl_text
16
+ # @see #load_state_from_dump
17
+ # @param filenames [Array<String>] An array of filenames, suitable for calling File.read on
18
+ # @param options [Hash] An optional hash of options for modifying the behavior of the reload
19
+ # @option options [Boolean] verify_only Only see if the new engine code would load, don't replace any code.
20
+ # @option options [Boolean] guessing_okay Attempt to guess about renamed objects
21
+ # @option options [Boolean] no_addition Don't allow adding new StateItems, raise an error instead
22
+ # @option options [Boolean] no_addition Don't allow removing StateItems, raise an error instead
23
+ # @return [void] A configured Engine
24
+ # @since 0.0.1
25
+ def reload_from_dsl_files(*filenames, options: {})
26
+ filename_string_pairs = filenames.map { |fn| [fn, File.read(fn)] }
27
+ engine_from_dsl_text(*filename_string_pairs)
28
+ end
29
+
30
+ # This method loads new World File code into an existing engine. It
31
+ # should be passed a list of "specs", normally roughly the same list
32
+ # that was passed to Demiurge::DSL.engine_from_dsl_files to create
33
+ # the engine initially. A "spec" is either a single string of World
34
+ # File DSL code, or a two-element array of the form: ["label",
35
+ # "code"]. Each is a string. "Code" is World File DSL Ruby code,
36
+ # while "label" is the name that will be used in stack traces.
37
+ #
38
+ # Options:
39
+ #
40
+ # * verify_only - only check for errors, don't load the new code into the engine, aka "dry run mode"
41
+ # * guessing_okay - attempt to notice renames and modify old state to new state accordingly
42
+ # * no_addition - if any new StateItem would be created, raise a NonMatchingStateError
43
+ # * no_removal - if any StateItem would be removed, raise a NonMatchingStateError
44
+ #
45
+ # Note that if "guessing_okay" is turned on, certain cases where
46
+ # same-type or (in the future) similar-type items are added and
47
+ # removed may be "guessed" as renames rather than treated as
48
+ # addition or removal.
49
+ #
50
+ # Where the files and world objects are
51
+ # essentially the same, this should reload any code changes into the
52
+ # engine. Where they are different, the method will try to determine
53
+ # the best match between the new and old state, but may fail at
54
+ # doing so.
55
+ #
56
+ # Ordinarily, this method should be called on a fully-configured,
57
+ # fully-initialized engine with its full state loaded, in between
58
+ # ticks.
59
+ #
60
+ # When in doubt, it's better to save state and reload the engine
61
+ # from nothing. This gives far better opportunities for a human to
62
+ # determine what changes have occurred and manually fix any errors.
63
+ # A game's current state is a complex thing and a computer simply
64
+ # cannot correctly determine all possible changes to it in a useful
65
+ # way.
66
+ #
67
+ # @see file:RELOADING.md
68
+ # @see Demiurge::DSL.engine_from_dsl_files
69
+ # @see Demiurge::DSL.engine_from_dsl_text
70
+ # @see #load_state_from_dump
71
+ # @param specs [Array<String>,Array<Array<String>>] An array of specs, see above
72
+ # @param options [Hash] An optional hash of options for modifying the behavior of the reload
73
+ # @option options [Boolean] verify_only Only see if the new engine code would load, don't replace any code.
74
+ # @option options [Boolean] guessing_okay Attempt to guess about renamed objects
75
+ # @option options [Boolean] no_addition Don't allow adding new StateItems, raise an error instead
76
+ # @option options [Boolean] no_addition Don't allow removing StateItems, raise an error instead
77
+ # @return [void] A configured Engine
78
+ # @since 0.0.1
79
+ def reload_from_dsl_text(*specs, options: { "verify_only" => false, "guessing_okay" => false, "no_addition" => false, "no_removal" => false })
80
+ old_engine = self
81
+ new_engine = nil
82
+
83
+ send_notification type: Demiurge::Notifications::LoadWorldVerify, zone: "admin", location: nil, actor: nil, include_context: true
84
+
85
+ begin
86
+ new_engine = Demiurge::DSL.engine_from_dsl_text(*specs)
87
+ rescue
88
+ # Didn't work? Leave the existing engine intact, but raise.
89
+ raise Demiurge::Errors::CannotLoadWorldFiles.new("Error reloading World File text")
90
+ end
91
+
92
+ # Match up old and new state items, but the "admin" InertStateItem doesn't get messed with
93
+ old_item_names = old_engine.all_item_names - ["admin"]
94
+ new_item_names = new_engine.all_item_names - ["admin"]
95
+
96
+ shared_item_names = old_item_names & new_item_names
97
+ added_item_names = new_item_names - shared_item_names
98
+ removed_item_names = old_item_names - shared_item_names
99
+ renamed_pairs = {}
100
+
101
+ if options["guessing_okay"]
102
+ # For right this second, don't guess. When guessing happens,
103
+ # this will populate the renamed_pairs hash.
104
+ end
105
+
106
+ if options["no_addition"] && !added_item_names.empty?
107
+ raise NonMatchingStateError.new "StateItems added when they weren't allowed: #{added_item_names.inspect}!"
108
+ end
109
+
110
+ if options["no_removal"] && !removed_item_names.empty?
111
+ raise NonMatchingStateError.new "StateItems removed when they weren't allowed: #{removed_item_names.inspect}!"
112
+ end
113
+
114
+ # Okay, finished with error checking - the dry-run is over
115
+ return if options["verify_only"]
116
+
117
+ # Now, replace the engine code
118
+
119
+ send_notification type: Demiurge::Notifications::LoadWorldStart, zone: "admin", location: nil, actor: nil, include_context: true
120
+
121
+ # Replace all actions performed by ActionItems
122
+ old_engine.replace_all_actions_for_all_items(new_engine.all_actions_for_all_items)
123
+
124
+ # For removed items, delete the StateItem from the old engine
125
+ removed_item_names.each do |removed_item_name|
126
+ old_engine.unregister_state_item(old_engine.item_by_name removed_item_name)
127
+ end
128
+
129
+ # For added items, use the state data from the new engine and add the item to the old engine
130
+ added_item_names.each do |added_item_name|
131
+ new_item = new_engine.item_by_name(added_item_name)
132
+ ss = new_item.structured_state
133
+ old_engine.register_state_item(StateItem.from_name_type(old_engine, *ss))
134
+ end
135
+
136
+ # A rename is basically an add and a remove... But not necessarily
137
+ # with the same name. And the newly-created item uses the state
138
+ # from the older item. A rename is also permitted to choose a new
139
+ # type, so create the new item in the old engine with the new name
140
+ # and type, but the old state.
141
+ renamed_pairs.each do |old_name, new_name|
142
+ old_item = old_engine.item_by_name(old_name)
143
+ new_item = new_engine.item_by_name(new_name)
144
+
145
+ old_type, _, old_state = *old_item.structured_state
146
+ new_type, _, new_state = *new_item.structured_state
147
+
148
+ old_engine.unregister_state_item(old_item)
149
+ old_engine.register_state_item(StateItem.from_name_type(old_engine, new_type, new_name, old_state))
150
+ end
151
+
152
+ send_notification type: Demiurge::Notifications::LoadWorldEnd, zone: "admin", location: nil, actor: nil, include_context: true
153
+ nil
154
+ end
155
+ end
156
+
157
+ # This module contains the Builder classes that parse the World File DSL.
158
+ #
159
+ # @since 0.0.1
160
+ module Demiurge::DSL
161
+ # This is a primary method for creating a new Demiurge Engine. It
162
+ # should be passed a list of filenames to load World File DSL
163
+ # from. It will return a fully-configured Engine which has called
164
+ # finished_init. If the Engine should load from an existing
165
+ # state-dump, that can be accomplished via load_state_from_dump.
166
+ #
167
+ # @see file:RELOADING.md
168
+ # @see Demiurge::Engine#load_state_from_dump
169
+ # @see Demiurge::Engine#reload_from_dsl_files
170
+ # @param filenames [Array<String>] An array of filenames, suitable for calling File.read on
171
+ # @return [Demiurge::Engine] A configured Engine
172
+ # @since 0.0.1
173
+ def self.engine_from_dsl_files(*filenames)
174
+ filename_string_pairs = filenames.map { |fn| [fn, File.read(fn)] }
175
+ engine_from_dsl_text(*filename_string_pairs)
176
+ end
177
+
178
+ # This method takes either strings containing World File DSL text,
179
+ # or name/string pairs. If a pair is supplied, the name gives the
180
+ # origin of the text for error messages.
181
+ #
182
+ # @see file:RELOADING.md
183
+ # @see Demiurge::Engine#load_state_from_dump
184
+ # @see Demiurge::Engine#reload_from_dsl_text
185
+ # @param specs [Array<String>, Array<Array<String>>] Either an array of chunks of DSL text, or an Array of two-element Arrays. Each two-element Array is a String name followed by a String of DSL text
186
+ # @return [Demiurge::Engine] A configured Engine
187
+ # @since 0.0.1
188
+ def self.engine_from_dsl_text(*specs)
189
+ builder = Demiurge::DSL::TopLevelBuilder.new
190
+
191
+ specs.each do |spec|
192
+ if spec.is_a?(String)
193
+ builder.instance_eval spec
194
+ elsif spec.is_a?(Array)
195
+ if spec.size != 2
196
+ raise "Not sure what to do with a #{spec.size}-elt array, normally this is a filename/string pair!"
197
+ end
198
+ builder.instance_eval spec[1], spec[0]
199
+ else
200
+ raise "Not sure what to do in engine_from_dsl_text with a #{spec.class}!"
201
+ end
202
+ end
203
+
204
+ builder.built_engine
205
+ end
206
+
207
+ # ActionItemBuilder is the parent class of all Builder classes
208
+ # except the {Demiurge::DSL::TopLevelBuilder}. It's used for a block
209
+ # of the World File DSL.
210
+ #
211
+ # @since 0.0.1
212
+ class ActionItemBuilder
213
+ # @return [StateItem] The item built by this builder
214
+ attr_reader :built_item
215
+
216
+ private
217
+ def check_options(hash, legal_options)
218
+ illegal_options = hash.keys - legal_options
219
+ raise "Illegal options #{illegal_options.inspect} passed to #{caller(1, 3).inspect}!" unless illegal_options.empty?
220
+ end
221
+ public
222
+
223
+ # @return [Array<String>] Legal options to pass to {ActionItemBuilder#initialize}
224
+ LEGAL_OPTIONS = [ "state", "type", "no_build" ]
225
+
226
+ # Initialize a DSL Builder block to configure some sort of ActionItem.
227
+ #
228
+ # @param name [String] The name to be registered with the Engine
229
+ # @param engine [Demiurge::Engine] The engine that will include this item
230
+ # @param options [Hash] Options for how the DSL block acts
231
+ # @option options [Hash] state The initial state Hash to create the item with
232
+ # @option options [String] type The item type to create
233
+ # @option options [Boolean] no_build If true, don't create and register a new StateItem with the Engine
234
+ # @return [void]
235
+ # @since 0.0.1
236
+ def initialize(name, engine, options = {})
237
+ check_options(options, LEGAL_OPTIONS)
238
+ @name = name
239
+ @engine = engine
240
+ @state = options["state"] || {}
241
+ @position = nil
242
+ @display = nil # This is display-specific information that gets passed to the display library
243
+
244
+ unless options["type"]
245
+ raise "You must pass a type when initializing a builder!"
246
+ end
247
+ unless options["no_build"]
248
+ @built_item = ::Demiurge::StateItem.from_name_type(@engine, options["type"], @name, @state)
249
+ @engine.register_state_item(@built_item)
250
+ end
251
+ nil
252
+ end
253
+
254
+ # If the DSL block sets state on the Builder object, this allows
255
+ # it to get to the internal Hash object rather than a
256
+ # wrapper. This should only be used internally to the DSL, not by
257
+ # others.
258
+ #
259
+ # @see #state
260
+ # @return [Hash] The state Hash
261
+ # @api private
262
+ # @since 0.0.1
263
+ def __state_internal
264
+ @built_item.state
265
+ end
266
+
267
+ # Get the state, or at least a wrapper object to it, for DSL usage.
268
+ #
269
+ # @return [Demiurge::ActionItemInternal::ActionItemStateWrapper] The state Hash wrapper
270
+ # @since 0.0.1
271
+ def state
272
+ @wrapper ||= ::Demiurge::ActionItemInternal::ActionItemStateWrapper.new(self)
273
+ end
274
+
275
+ # Register an action with the Engine. Since StateItems are
276
+ # effectively disposable, we need somewhere outside of the
277
+ # StateItem itself to store its actions as Ruby code. The Engine
278
+ # is the place they are stored.
279
+ #
280
+ # @param action [Hash] The action hash to use as the internal action structure
281
+ # @api private
282
+ # @since 0.0.1
283
+ private
284
+ def register_built_action(action)
285
+ raise("Must specify a string 'name' to register_build_action! Only gave #{action.inspect}!") unless action["name"]
286
+ check_options(action, ::Demiurge::ActionItem::ACTION_LEGAL_KEYS)
287
+ @built_item.register_actions(action["name"] => action)
288
+ end
289
+ public
290
+
291
+ # Perform the given action every so many ticks. This will set up
292
+ # the necessary state entries to cause the action to occur each
293
+ # time that many ticks pass. The given action name is attached to
294
+ # the given block (if any.) The named action can be modified using
295
+ # define_action if you want to set extra settings like engine_code
296
+ # or busy for the action in question. If no block is given, you
297
+ # should use define_action to create the action in question, or it
298
+ # will have no definition and cause errors.
299
+ #
300
+ # @param action_name [String] The action name for this item to use repeatedly
301
+ # @param t [Integer] The number of ticks that pass between actions
302
+ # @yield [...] Called when the action is performed with any arguments supplied by the caller
303
+ # @yieldreturn [void]
304
+ # @return [void]
305
+ # @since 0.0.1
306
+ def every_X_ticks(action_name, t, &block)
307
+ raise("Must provide a positive number for how many ticks, not #{t.inspect}!") unless t.is_a?(Numeric) && t >= 0.0
308
+ @built_item.state["everies"] ||= []
309
+ @built_item.state["everies"] << { "action" => action_name, "every" => t, "counter" => 0 }
310
+ @built_item.register_actions(action_name => { "name" => action_name, "block" => block })
311
+ nil
312
+ end
313
+
314
+ # Set the position of the built object.
315
+ #
316
+ # @param pos [String] The new position string for this built object.
317
+ # @return [void]
318
+ # @since 0.0.1
319
+ def position(pos)
320
+ @built_item.state["position"] = pos
321
+ nil
322
+ end
323
+
324
+ # Pass a block through that is intended for the display library to
325
+ # use later. If no display library is used, this is a no-op.
326
+ #
327
+ # @yield [] The block will be called by the display library, in a display-library-specific context, or not at all
328
+ # @return [void]
329
+ # @since 0.0.1
330
+ def display(&block)
331
+ # Need to figure out how to pass this through to the Display
332
+ # library. By design, the simulation/state part of Demiurge
333
+ # ignores this completely.
334
+ @built_item.register_actions("$display" => { "name" => "$display", "block" => block })
335
+ nil
336
+ end
337
+
338
+ # The specified action will be called for notifications of the
339
+ # appropriate type.
340
+ # @todo Figure out timing of the subscription - right now it will use the item's location midway through parsing the DSL!
341
+ #
342
+ # @param event [String] The name of the notification to subscribe to
343
+ # @param action_name [String] The action name of the new action
344
+ # @param options [Hash] Additional specifications about what/how to subscribe
345
+ # @option options [String,:all] location The location name to subscribe for - defaults to this item's location
346
+ # @option options [String,:all] zone The zone name to subscribe for - defaults to this item's zone
347
+ # @option options [String,:all] actor The acting item name to subscribe for - defaults to any item
348
+ # @yield [Hash] Receives notification hashes when these notifications occur
349
+ # @return [void]
350
+ # @since 0.0.1
351
+ def on_notification(event, action_name, options = {}, &block)
352
+ @built_item.state["on_handlers"] ||= {}
353
+ @built_item.state["on_handlers"][event] = action_name
354
+ register_built_action("name" => action_name, "block" => block)
355
+
356
+ location = options[:location] || options["location"] || @built_item.location
357
+ zone = options[:zone] || options["zone"] || location.zone_name || @built_item.zone_name
358
+ item = options[:actor] || options["actor"] || :all
359
+
360
+ @engine.subscribe_to_notifications type: event, zone: zone, location: location, actor: item do |notification|
361
+ # To keep this statedump-safe, need to look up the item again
362
+ # every time. @built_item isn't guaranteed to last.
363
+ @engine.item_by_name(@name).run_action(action_name, notification)
364
+ end
365
+ nil
366
+ end
367
+
368
+ # "on" is an older name for "on_notification" and is deprecated.
369
+ # @deprecated
370
+ alias_method :on, :on_notification
371
+
372
+ # The specified action will be called for Intentions using the
373
+ # appropriate action. This is used to modify or cancel an
374
+ # Intention before it runs.
375
+ #
376
+ # @param caught_action [String] The action type of the Intention being caught, or "all" for all intentions
377
+ # @param action_to_run [String] The action name of the new intercepting action
378
+ # @yield [Intention] Receives the Intention when these Intentions occur
379
+ # @return [void]
380
+ # @since 0.0.1
381
+ def on_intention(caught_action, action_to_run, &block)
382
+ @built_item.state["on_action_handlers"] ||= {}
383
+ raise "Already have an on_action (offer) handler for action #{caught_action}! Failing!" if @built_item.state["on_action_handlers"][caught_action]
384
+ @built_item.state["on_action_handlers"][caught_action] = action_to_run
385
+ register_built_action("name" => action_to_run, "block" => block)
386
+ nil
387
+ end
388
+
389
+ # "on_action" is an older name for "on_intention" and is deprecated.
390
+ # @deprecated
391
+ alias_method :on_action, :on_intention
392
+
393
+ # If you want to define an action for later calling, or to set
394
+ # options on an action that was defined as part of another
395
+ # handler, you can call define_action to make that happen.
396
+ #
397
+ # @example Make an every_X_ticks action also keep the agent busy and run as Engine code
398
+ # ```
399
+ # every_X_ticks("burp", 15) { engine.item_by_name("sooper sekrit").ruby_only_burp_action }
400
+ # define_action("burp", "engine_code" => true, "busy" => 7)
401
+ # ```
402
+ #
403
+ # @param action_name [String] The action name to declare or modify
404
+ # @param options [Hash] Options for this action
405
+ # @option options [Integer] busy How many ticks an agent should remain busy for after taking this action
406
+ # @option options [Boolean] engine_code If true, use the EngineBlockRunner instead of a normal runner for this code; usually a bad idea
407
+ # @option options [Array<String>] tags Tags that the action can be queried by later - useful for tagging player or agent actions, or admin-only actions
408
+ # @yield [...] Actions receive whatever arguments their later caller supplies
409
+ # @yieldreturn [void]
410
+ # @return [void]
411
+ # @since 0.0.1
412
+ def define_action(action_name, options = {}, &block)
413
+ legal_options = [ "busy", "engine_code", "tags" ]
414
+ illegal_keys = options.keys - legal_options
415
+ raise("Illegal keys #{illegal_keys.inspect} passed to define_action of #{action_name.inspect}!") unless illegal_keys.empty?;
416
+ register_built_action({ "name" => action_name, "block" => block }.merge(options))
417
+ nil
418
+ end
419
+ end
420
+
421
+ # This is the top-level DSL Builder class, for parsing the top syntactic level of the World Files.
422
+ #
423
+ # @since 0.0.1
424
+ class TopLevelBuilder
425
+ # This is the private structure of type names that are registered with the Demiurge World File DSL
426
+ @@types = {}
427
+
428
+ # Constructor for a new set of World Files and their top-level state.
429
+ def initialize(options = {})
430
+ @zones = []
431
+ @engine = options["engine"] || ::Demiurge::Engine.new(types: @@types, state: [])
432
+ end
433
+
434
+ # For now, this just declares an InertStateItem for a given name.
435
+ # It doesn't change the behavior at all. It just keeps that item
436
+ # name from being "orphaned" state that doesn't correspond to any
437
+ # state item.
438
+ #
439
+ # Later, this may be a way to describe how important or transitory
440
+ # state is - is it reset like a zone? Completely transient?
441
+ # Cleared per reboot?
442
+ #
443
+ # @param item_name [String] The item name for scoping the state in the Engine
444
+ # @param options [Hash] Options about the InertStateItem
445
+ # @option options [String] zone The zone this InertStateItem considers itself to be in, defaults to "admin"
446
+ # @option options [Hash] state The initial state Hash
447
+ # @option options [String] type The object type to instantiate, if not InertStateItem
448
+ # @return [void]
449
+ # @since 0.0.1
450
+ def inert(item_name, options = {})
451
+ zone_name = options["zone"] || "admin"
452
+ state = options["state"] || {}
453
+ state.merge! "zone" => zone_name, "home_zone" => zone_name
454
+ inert_item = ::Demiurge::StateItem.from_name_type(@engine, options["type"] || "InertStateItem", item_name, state)
455
+ @engine.register_state_item(inert_item)
456
+ nil
457
+ end
458
+
459
+ # Start a new Zone block, using a ZoneBuilder.
460
+ def zone(name, options = {}, &block)
461
+ if @zones.any? { |z| z.name == name }
462
+ # Reopening an existing zone
463
+ builder = ZoneBuilder.new(name, @engine, options.merge("existing" => @zones.detect { |z| z.name == name }))
464
+ else
465
+ builder = ZoneBuilder.new(name, @engine, options)
466
+ end
467
+
468
+ builder.instance_eval(&block)
469
+ new_zone = builder.built_item
470
+
471
+ @zones |= [ new_zone ] if new_zone # Add if not already present
472
+ nil
473
+ end
474
+
475
+ # It's hard to figure out where and how to register types and
476
+ # plugins for the World File format. By their nature, they need to
477
+ # be in place before an Engine exists, so that's not the right
478
+ # place. If they didn't exist before engines, we'd somehow need to
479
+ # register them with each engine as it was created. Since Engines
480
+ # keep track of that, that's exactly the same problem we're trying
481
+ # to solve, just for the Engine builder. And it seems like
482
+ # "register this plugin with Demiurge World Files" is more of a
483
+ # process-global operation than a per-Engine operation. So these
484
+ # wind up in awkward spots.
485
+ def self.register_type(name, klass)
486
+ if @@types[name.to_s]
487
+ raise("Attempting to re-register type #{name.inspect} with a different class!") unless @@types[name.to_s] == klass
488
+ else
489
+ @@types[name.to_s] = klass
490
+ end
491
+ end
492
+
493
+ # Return the built Engine, but first call the .finished_init
494
+ # callback. This will make sure that cached and duplicated data
495
+ # structures are properly filled in.
496
+ def built_engine
497
+ @engine.finished_init
498
+ @engine
499
+ end
500
+ end
501
+
502
+ # Declare an "agent" block in the World File DSL.
503
+ class AgentBuilder < ActionItemBuilder
504
+ def initialize(name, engine, options = {})
505
+ options = { "type" => "Agent" }.merge(options)
506
+ super(name, engine, options)
507
+ end
508
+ end
509
+
510
+ # Declare a "zone" block in the World File DSL.
511
+ class ZoneBuilder < ActionItemBuilder
512
+ # Constructor. See if this zone name already exists, and either
513
+ # create a new zone or append to the old one.
514
+ def initialize(name, engine, options = {})
515
+ @existing = options.delete("existing")
516
+ if @existing
517
+ old_type = @existing.state_type
518
+ new_type = options["type"]
519
+ if new_type && old_type != new_type
520
+ raise("Can't reopen zone with type #{(options["type"] || "Unspecified").inspect} after creating with type #{old_type.inspect}!")
521
+ end
522
+ options["no_build"] = true
523
+ @built_item = @existing
524
+ end
525
+ super(name, engine, options.merge("type" => options["type"] || "Zone"))
526
+ @locations = []
527
+ @agents = []
528
+ end
529
+
530
+ # Declare a location in this zone.
531
+ def location(name, options = {}, &block)
532
+ state = { "zone" => @name, "home_zone" => @name }.merge(options)
533
+ builder = LocationBuilder.new(name, @engine, "type" => options["type"] || "Location", "state" => state)
534
+ builder.instance_eval(&block)
535
+ @built_item.state["contents"] << name
536
+ nil
537
+ end
538
+
539
+ # Declare an agent in this zone. If the agent doesn't get a
540
+ # location declaration, by default the agent will usually be
541
+ # invisible (not an interactable location) but will be
542
+ # instantiable as a parent.
543
+ def agent(name, options = {}, &block)
544
+ state = { "zone" => @name, "home_zone" => @name }.merge(options)
545
+ builder = AgentBuilder.new(name, @engine, "type" => options["type"] || "Agent", "state" => state)
546
+ builder.instance_eval(&block)
547
+ @built_item.state["contents"] << name
548
+ nil
549
+ end
550
+ end
551
+
552
+ # Declare a "location" block in a World File.
553
+ class LocationBuilder < ActionItemBuilder
554
+ # Constructor for a "location" DSL block
555
+ def initialize(name, engine, options = {})
556
+ options["type"] ||= "Location"
557
+ super
558
+ @agents = []
559
+ end
560
+
561
+ # Declare a description for this location.
562
+ def description(d)
563
+ @state["description"] = d
564
+ end
565
+
566
+ # Declare an agent in this location.
567
+ def agent(name, options = {}, &block)
568
+ state = { "position" => @name, "zone" => @state["zone"], "home_zone" => @state["zone"] }
569
+ builder = AgentBuilder.new(name, @engine, options.merge("state" => state) )
570
+ builder.instance_eval(&block)
571
+ @built_item.state["contents"] << name
572
+ nil
573
+ end
574
+ end
575
+
576
+ end
577
+
578
+ Demiurge::DSL::TopLevelBuilder.register_type "ActionItem", Demiurge::ActionItem
579
+ Demiurge::DSL::TopLevelBuilder.register_type "InertStateItem", Demiurge::InertStateItem
580
+ Demiurge::DSL::TopLevelBuilder.register_type "Zone", Demiurge::Zone
581
+ Demiurge::DSL::TopLevelBuilder.register_type "Location", Demiurge::Location
582
+ Demiurge::DSL::TopLevelBuilder.register_type "Agent", Demiurge::Agent
583
+ Demiurge::DSL::TopLevelBuilder.register_type "WanderingAgent", Demiurge::WanderingAgent