gamefic 2.4.0 → 3.0.0

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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +41 -40
  3. data/.rspec-opal +2 -0
  4. data/.solargraph.yml +20 -3
  5. data/CHANGELOG.md +9 -0
  6. data/Rakefile +11 -1
  7. data/bin/console +14 -0
  8. data/bin/setup +8 -0
  9. data/gamefic.gemspec +5 -2
  10. data/lib/gamefic/action.rb +52 -183
  11. data/lib/gamefic/active/cue.rb +25 -0
  12. data/lib/gamefic/active/epic.rb +68 -0
  13. data/lib/gamefic/active/messaging.rb +43 -0
  14. data/lib/gamefic/active/take.rb +69 -0
  15. data/lib/gamefic/active.rb +95 -192
  16. data/lib/gamefic/actor.rb +2 -0
  17. data/lib/gamefic/block.rb +28 -0
  18. data/lib/gamefic/command.rb +16 -6
  19. data/lib/gamefic/core_ext/array.rb +4 -4
  20. data/lib/gamefic/core_ext/string.rb +10 -5
  21. data/lib/gamefic/describable.rb +39 -65
  22. data/lib/gamefic/dispatcher.rb +63 -32
  23. data/lib/gamefic/entity.rb +44 -19
  24. data/lib/gamefic/logging.rb +32 -0
  25. data/lib/gamefic/messenger.rb +66 -0
  26. data/lib/gamefic/narrative.rb +104 -0
  27. data/lib/gamefic/node.rb +44 -53
  28. data/lib/gamefic/plot.rb +60 -93
  29. data/lib/gamefic/props/default.rb +41 -0
  30. data/lib/gamefic/props/multiple_choice.rb +65 -0
  31. data/lib/gamefic/props/pause.rb +11 -0
  32. data/lib/gamefic/props/yes_or_no.rb +21 -0
  33. data/lib/gamefic/props.rb +10 -0
  34. data/lib/gamefic/query/base.rb +45 -126
  35. data/lib/gamefic/query/general.rb +46 -0
  36. data/lib/gamefic/query/result.rb +20 -0
  37. data/lib/gamefic/query/scoped.rb +41 -0
  38. data/lib/gamefic/query/text.rb +30 -31
  39. data/lib/gamefic/query.rb +7 -15
  40. data/lib/gamefic/response.rb +118 -0
  41. data/lib/gamefic/rulebook/calls.rb +90 -0
  42. data/lib/gamefic/rulebook/events.rb +79 -0
  43. data/lib/gamefic/rulebook/hooks.rb +57 -0
  44. data/lib/gamefic/rulebook/scenes.rb +68 -0
  45. data/lib/gamefic/rulebook.rb +139 -0
  46. data/lib/gamefic/scanner.rb +103 -0
  47. data/lib/gamefic/scene/activity.rb +9 -17
  48. data/lib/gamefic/scene/conclusion.rb +6 -5
  49. data/lib/gamefic/scene/default.rb +88 -0
  50. data/lib/gamefic/scene/multiple_choice.rb +14 -69
  51. data/lib/gamefic/scene/pause.rb +9 -13
  52. data/lib/gamefic/scene/yes_or_no.rb +6 -46
  53. data/lib/gamefic/scene.rb +11 -7
  54. data/lib/gamefic/scope/base.rb +44 -0
  55. data/lib/gamefic/scope/children.rb +16 -0
  56. data/lib/gamefic/scope/family.rb +20 -0
  57. data/lib/gamefic/scope/myself.rb +13 -0
  58. data/lib/gamefic/scope/parent.rb +13 -0
  59. data/lib/gamefic/scope/siblings.rb +14 -0
  60. data/lib/gamefic/scope.rb +8 -0
  61. data/lib/gamefic/scriptable/actions.rb +156 -0
  62. data/lib/gamefic/scriptable/entities.rb +76 -0
  63. data/lib/gamefic/scriptable/events.rb +65 -0
  64. data/lib/gamefic/scriptable/proxy.rb +55 -0
  65. data/lib/gamefic/scriptable/queries.rb +73 -0
  66. data/lib/gamefic/scriptable/scenes.rb +162 -0
  67. data/lib/gamefic/scriptable.rb +167 -73
  68. data/lib/gamefic/snapshot.rb +36 -0
  69. data/lib/gamefic/stage.rb +51 -0
  70. data/lib/gamefic/subplot.rb +51 -79
  71. data/lib/gamefic/syntax/template.rb +67 -0
  72. data/lib/gamefic/syntax.rb +102 -83
  73. data/lib/gamefic/vault.rb +50 -0
  74. data/lib/gamefic/version.rb +1 -1
  75. data/lib/gamefic.rb +26 -15
  76. data/spec-opal/spec_helper.rb +24 -0
  77. metadata +91 -29
  78. data/lib/gamefic/element.rb +0 -46
  79. data/lib/gamefic/keywords.rb +0 -52
  80. data/lib/gamefic/messaging.rb +0 -43
  81. data/lib/gamefic/plot/darkroom.rb +0 -120
  82. data/lib/gamefic/plot/host.rb +0 -42
  83. data/lib/gamefic/plot/snapshot.rb +0 -27
  84. data/lib/gamefic/query/children.rb +0 -9
  85. data/lib/gamefic/query/descendants.rb +0 -15
  86. data/lib/gamefic/query/external.rb +0 -39
  87. data/lib/gamefic/query/family.rb +0 -18
  88. data/lib/gamefic/query/itself.rb +0 -13
  89. data/lib/gamefic/query/matches.rb +0 -75
  90. data/lib/gamefic/query/parent.rb +0 -9
  91. data/lib/gamefic/query/siblings.rb +0 -13
  92. data/lib/gamefic/query/tree.rb +0 -17
  93. data/lib/gamefic/scene/base.rb +0 -142
  94. data/lib/gamefic/scene/multiple_scene.rb +0 -29
  95. data/lib/gamefic/serialize.rb +0 -196
  96. data/lib/gamefic/world/callbacks.rb +0 -135
  97. data/lib/gamefic/world/commands.rb +0 -181
  98. data/lib/gamefic/world/entities.rb +0 -98
  99. data/lib/gamefic/world/playbook.rb +0 -233
  100. data/lib/gamefic/world/players.rb +0 -37
  101. data/lib/gamefic/world/scenes.rb +0 -228
  102. data/lib/gamefic/world.rb +0 -18
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Query
5
+ # A General query accepts an array of entities to filter. Unlike Scoped
6
+ # queries, the resulting entities will not necessarily be in the actor's
7
+ # immediate vicinity.
8
+ #
9
+ # General queries can also be passed a Proc that returns an array of
10
+ # entities. If the Proc accepts an argument, it will be given the subject
11
+ # of the query.
12
+ #
13
+ class General < Base
14
+ # @param entities [Array, Proc]
15
+ # @param arguments [Array<Object>]
16
+ # @param ambiguous [Boolean]
17
+ def initialize entities, *arguments, ambiguous: false
18
+ super(*arguments, ambiguous: ambiguous)
19
+ @entities = entities
20
+ end
21
+
22
+ def query subject, token
23
+ filtered = available_entities(subject).that_are(*@arguments)
24
+ return Result.new(token, nil) if filtered.include?(token)
25
+
26
+ scan = Scanner.scan(filtered, token)
27
+
28
+ ambiguous? ? ambiguous_result(scan) : unambiguous_result(scan)
29
+ end
30
+
31
+ private
32
+
33
+ def available_entities(subject)
34
+ if @entities.is_a?(Proc)
35
+ if @entities.arity.zero?
36
+ @entities.call
37
+ else
38
+ @entities.call(subject)
39
+ end
40
+ else
41
+ @entities
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Query
5
+ # The result of a query.
6
+ #
7
+ class Result
8
+ # @return [Entity, Array<Entity>, String, nil]
9
+ attr_reader :match
10
+
11
+ # @return [String]
12
+ attr_reader :remainder
13
+
14
+ def initialize match, remainder
15
+ @match = match
16
+ @remainder = remainder
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Query
5
+ # A Scoped query uses a Scope to select entities to filter based on their
6
+ # relationship to the entity performing the query. For example,
7
+ # Scope::Children would filter from an array of the entity's descendants.
8
+ #
9
+ # @return [Class<Gamefic::Scope::Base>]
10
+ class Scoped < Base
11
+ attr_reader :scope
12
+
13
+ # @param scope [Class<Gamefic::Scope::Base>]
14
+ def initialize scope, *arguments, ambiguous: false
15
+ super(*arguments, ambiguous: ambiguous)
16
+ @scope = scope
17
+ end
18
+
19
+ # @return [Result]
20
+ def query(subject, token)
21
+ available = @scope.matches(subject)
22
+ .that_are(*@arguments)
23
+ return Result.new(token, nil) if available.include?(token)
24
+
25
+ scan = Scanner.scan(available, token)
26
+
27
+ return ambiguous_result(scan) if ambiguous?
28
+
29
+ unambiguous_result(scan)
30
+ end
31
+
32
+ def precision
33
+ @precision ||= @scope.precision + calculate_precision
34
+ end
35
+
36
+ def ambiguous?
37
+ @ambiguous
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,46 +1,45 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  module Query
3
- class Text < Base
4
- def initialize *arguments
5
- arguments.each do |a|
6
- if (a.kind_of?(Symbol) || a.kind_of?(String)) && !a.to_s.end_with?('?')
7
- raise ArgumentError.new("Text query arguments can only be boolean method names (:method?) or regular expressions")
8
- end
9
- end
10
- super
5
+ # A special query that handles text instead of entities.
6
+ #
7
+ class Text
8
+ # @param argument [String, Regexp, nil]
9
+ def initialize argument = nil
10
+ @argument = argument
11
+ validate
11
12
  end
12
13
 
13
- def resolve _subject, token, continued: false
14
- return Matches.new([], '', token) unless accept?(token)
15
- parts = token.split(Keywords::SPLIT_REGEXP)
16
- cursor = []
17
- matches = []
18
- i = 0
19
- parts.each { |w|
20
- cursor.push w
21
- matches = cursor if accept?(cursor.join(' '))
22
- i += 1
23
- }
24
- if continued
25
- Matches.new([matches.join(' ')], matches.join(' '), parts[i..-1].join(' '))
26
- elsif matches.length == parts.length
27
- Matches.new([matches.join(' ')], matches.join(' '), '')
14
+ def query _subject, token
15
+ if match? token
16
+ Result.new(token, '')
28
17
  else
29
- Matches.new([], '', parts.join(' '))
18
+ Result.new(nil, token)
30
19
  end
31
20
  end
32
21
 
33
- def include? _subject, token
34
- accept?(token)
22
+ def precision
23
+ 0
35
24
  end
36
25
 
37
- def accept? entity
38
- return false unless entity.kind_of?(String) and !entity.empty?
39
- super
26
+ private
27
+
28
+ def match? token
29
+ return true if @argument.nil?
30
+
31
+ case @argument
32
+ when Regexp
33
+ token =~ @argument
34
+ else
35
+ token == @argument
36
+ end
40
37
  end
41
38
 
42
- def precision
43
- 0
39
+ def validate
40
+ return if @argument.nil? || @argument.is_a?(String) || @argument.is_a?(Regexp)
41
+
42
+ raise ArgumentError, 'Invalid text query argument'
44
43
  end
45
44
  end
46
45
  end
data/lib/gamefic/query.rb CHANGED
@@ -1,15 +1,7 @@
1
- module Gamefic
2
- module Query
3
- autoload :Base, 'gamefic/query/base'
4
- autoload :Children, 'gamefic/query/children'
5
- autoload :Descendants, 'gamefic/query/descendants'
6
- autoload :External, 'gamefic/query/external'
7
- autoload :Family, 'gamefic/query/family'
8
- autoload :Tree, 'gamefic/query/tree'
9
- autoload :Itself, 'gamefic/query/itself'
10
- autoload :Matches, 'gamefic/query/matches'
11
- autoload :Parent, 'gamefic/query/parent'
12
- autoload :Siblings, 'gamefic/query/siblings'
13
- autoload :Text, 'gamefic/query/text'
14
- end
15
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'gamefic/query/base'
4
+ require 'gamefic/query/general'
5
+ require 'gamefic/query/scoped'
6
+ require 'gamefic/query/text'
7
+ require 'gamefic/query/result'
@@ -0,0 +1,118 @@
1
+ # frozen_literal_string: true
2
+
3
+ module Gamefic
4
+ # A proc to be executed in response to a command that matches its verb and
5
+ # queries.
6
+ #
7
+ class Response
8
+ # @return [Symbol]
9
+ attr_reader :verb
10
+
11
+ # @return [Array<Query::Base>]
12
+ attr_reader :queries
13
+
14
+ # @return [Proc]
15
+ # attr_reader :block
16
+
17
+ # @param verb [Symbol]
18
+ # @param stage [Object]
19
+ # @param queries [Array<Query::Base>]
20
+ # @param meta [Boolean]
21
+ # @param block [Proc]
22
+ def initialize verb, stage, *queries, meta: false, &block
23
+ @verb = verb
24
+ @stage = stage
25
+ @queries = map_queryable_objects(queries)
26
+ @meta = meta
27
+ @block = block
28
+ end
29
+
30
+ # The `meta?` flag is just a way for authors to identify responses that
31
+ # serve a purpose other than performing in-game actions. Out-of-game
32
+ # responses can include features like displaying help documentation or
33
+ # listing credits.
34
+ #
35
+ def meta?
36
+ @meta
37
+ end
38
+
39
+ def hidden?
40
+ @hidden ||= verb.to_s.start_with?('_')
41
+ end
42
+
43
+ def syntax
44
+ @syntax ||= generate_default_syntax
45
+ end
46
+
47
+ # Return an Action if the Response can accept the actor's command.
48
+ #
49
+ # @param actor [Entity]
50
+ # @param command [Command]
51
+ # @param with_hooks [Boolean]
52
+ # @return [Action, nil]
53
+ def attempt actor, command
54
+ return nil if command.verb != verb
55
+
56
+ tokens = command.arguments.clone
57
+ result = []
58
+ remainder = ''
59
+
60
+ queries.each do |qd|
61
+ token = tokens.shift
62
+ txt = "#{remainder} #{token}".strip
63
+ return nil if txt.empty?
64
+
65
+ response = qd.query(actor, txt)
66
+ return nil if response.match.nil?
67
+
68
+ result.push response.match
69
+
70
+ remainder = response.remainder
71
+ end
72
+
73
+ return nil unless tokens.empty? && remainder.empty?
74
+
75
+ Action.new(actor, result, self)
76
+ end
77
+
78
+ def execute *args
79
+ Stage.run(@stage, *args, &@block)
80
+ end
81
+
82
+ def precision
83
+ @precision ||= calculate_precision
84
+ end
85
+
86
+ private
87
+
88
+ def generate_default_syntax
89
+ user_friendly = verb.to_s.gsub(/_/, ' ')
90
+ args = []
91
+ used_names = []
92
+ queries.each do |_c|
93
+ num = 1
94
+ new_name = ":var"
95
+ while used_names.include? new_name
96
+ num += 1
97
+ new_name = ":var#{num}"
98
+ end
99
+ used_names.push new_name
100
+ user_friendly += " #{new_name}"
101
+ args.push new_name
102
+ end
103
+ Syntax.new(user_friendly.strip, "#{verb} #{args.join(' ')}".strip)
104
+ end
105
+
106
+ def calculate_precision
107
+ total = 0
108
+ queries.each { |q| total += q.precision }
109
+ total -= 1000 if verb.nil?
110
+ total
111
+ end
112
+
113
+ def map_queryable_objects queries
114
+ # @todo Considering moving mapping from Actions to here
115
+ queries
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Rulebook
5
+ # A collection of responses and syntaxes that constitute the actions
6
+ # available to actors.
7
+ #
8
+ class Calls
9
+ def initialize
10
+ @verb_response_map = Hash.new { |hash, key| hash[key] = [] }
11
+ @synonym_syntax_map = Hash.new { |hash, key| hash[key] = [] }
12
+ end
13
+
14
+ def freeze
15
+ super
16
+ @verb_response_map.freeze
17
+ @verb_response_map.values.map(&:freeze)
18
+ @synonym_syntax_map.freeze
19
+ @synonym_syntax_map.values.map(&:freeze)
20
+ self
21
+ end
22
+
23
+ def syntaxes
24
+ synonym_syntax_map.values.flatten
25
+ end
26
+
27
+ def synonyms
28
+ synonym_syntax_map.keys.compact.sort
29
+ end
30
+
31
+ def responses
32
+ verb_response_map.values.flatten
33
+ end
34
+
35
+ def verbs
36
+ verb_response_map.keys.compact.sort
37
+ end
38
+
39
+ def responses_for *verbs
40
+ verbs.flat_map { |verb| verb_response_map.fetch(verb, []) }
41
+ end
42
+
43
+ def syntaxes_for *synonyms
44
+ synonyms.flat_map { |syn| synonym_syntax_map.fetch(syn, []) }
45
+ end
46
+
47
+ def add_response response
48
+ verb_response_map[response.verb].unshift response
49
+ sort_responses_for response.verb
50
+ add_syntax response.syntax unless response.verb.to_s.start_with?('_')
51
+ response
52
+ end
53
+
54
+ # @param syntax [Syntax]
55
+ # @return [Syntax]
56
+ def add_syntax syntax
57
+ raise "No responses exist for \"#{syntax.verb}\"" unless verb_response_map.key?(syntax.verb)
58
+
59
+ return if synonym_syntax_map[syntax.synonym].include?(syntax)
60
+
61
+ synonym_syntax_map[syntax.synonym].unshift syntax
62
+ sort_syntaxes_for syntax.synonym
63
+ syntax
64
+ end
65
+
66
+ def empty?
67
+ verb_response_map.empty? && synonym_syntax_map.empty?
68
+ end
69
+
70
+ def self.new_array_map
71
+ Hash.new { |hash, key| hash[key] = [] }
72
+ end
73
+
74
+ private
75
+
76
+ attr_reader :verb_response_map
77
+
78
+ attr_reader :synonym_syntax_map
79
+
80
+ # @param responses [Array<Response>]
81
+ def sort_responses_for verb
82
+ verb_response_map[verb].sort_by!.with_index { |a, i| [a.precision, -i] }.reverse!
83
+ end
84
+
85
+ def sort_syntaxes_for synonym
86
+ synonym_syntax_map[synonym].sort! { |a, b| a.compare b }
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Rulebook
5
+ # Blocks of code to be executed for various narrative events, such as
6
+ # on_ready and on_update.
7
+ #
8
+ class Events
9
+ attr_reader :player_output_blocks
10
+
11
+ attr_reader :player_conclude_blocks
12
+
13
+ attr_reader :ready_blocks
14
+
15
+ attr_reader :update_blocks
16
+
17
+ attr_reader :conclude_blocks
18
+
19
+ def initialize
20
+ @ready_blocks = []
21
+ @update_blocks = []
22
+ @conclude_blocks = []
23
+ @player_conclude_blocks = []
24
+ @player_output_blocks = []
25
+ end
26
+
27
+ def empty?
28
+ [player_output_blocks, player_conclude_blocks, ready_blocks, update_blocks, conclude_blocks].all?(&:empty?)
29
+ end
30
+
31
+ def freeze
32
+ super
33
+ instance_variables.each { |k| instance_variable_get(k).freeze }
34
+ self
35
+ end
36
+
37
+ # @return [Proc]
38
+ def on_ready &block
39
+ @ready_blocks.push block
40
+ end
41
+
42
+ # @yieldparam [Actor]
43
+ # @return [Proc]
44
+ def on_player_ready &block
45
+ @ready_blocks.push(proc do
46
+ players.each { |plyr| block.call plyr }
47
+ end)
48
+ end
49
+
50
+ def on_update &block
51
+ @update_blocks.push block
52
+ end
53
+
54
+ def on_player_update &block
55
+ @update_blocks.push(proc do
56
+ players.each { |plyr| block.call plyr }
57
+ end)
58
+ end
59
+
60
+ # @return [Proc]
61
+ def on_conclude &block
62
+ @conclude_blocks.push block
63
+ end
64
+
65
+ # @yieldparam [Actor]
66
+ # @return [Proc]
67
+ def on_player_conclude &block
68
+ @player_conclude_blocks.push block
69
+ end
70
+
71
+ # @yieldparam [Actor]
72
+ # @yieldparam [Hash]
73
+ # @return [Proc]
74
+ def on_player_output &block
75
+ @player_output_blocks.push block
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Rulebook
5
+ # A collection of hooks that can be executed before and after an action.
6
+ #
7
+ class Hooks
8
+ attr_reader :before_actions
9
+
10
+ attr_reader :after_actions
11
+
12
+ def initialize
13
+ @before_actions = []
14
+ @after_actions = []
15
+ end
16
+
17
+ def freeze
18
+ super
19
+ @before_actions.freeze
20
+ @after_actions.freeze
21
+ self
22
+ end
23
+
24
+ def before_action *verbs, &block
25
+ before_actions.push Action::Hook.new(*verbs, &block)
26
+ end
27
+
28
+ def after_action *verbs, &block
29
+ after_actions.push Action::Hook.new(*verbs, &block)
30
+ end
31
+
32
+ def empty?
33
+ before_actions.empty? && after_actions.empty?
34
+ end
35
+
36
+ def run_before action, narrative
37
+ run_action_hooks action, narrative, before_actions
38
+ end
39
+
40
+ def run_after action, narrative
41
+ run_action_hooks action, narrative, after_actions
42
+ end
43
+
44
+ private
45
+
46
+ def run_action_hooks action, narrative, hooks
47
+ hooks.each do |hook|
48
+ break if action.cancelled?
49
+
50
+ next unless hook.match?(action.verb)
51
+
52
+ Stage.run(narrative) { instance_exec(action, &hook.block) }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Rulebook
5
+ # The scene manager for rulebooks.
6
+ #
7
+ class Scenes
8
+ attr_reader :introductions
9
+
10
+ def initialize
11
+ @scene_map = {}
12
+ @introductions = []
13
+ end
14
+
15
+ def freeze
16
+ super
17
+ @scene_map.freeze
18
+ @introductions.freeze
19
+ self
20
+ end
21
+
22
+ # Add a scene to the scenebook.
23
+ #
24
+ # @param [Scene]
25
+ def add scene
26
+ raise ArgumentError, "A scene named `#{scene.name} already exists" if @scene_map.key?(scene.name)
27
+
28
+ @scene_map[scene.name] = scene
29
+ end
30
+
31
+ def scene? name
32
+ @scene_map.key? name
33
+ end
34
+
35
+ # @return [Scene, nil]
36
+ def [](name)
37
+ @scene_map[name]
38
+ end
39
+
40
+ # @return [Array<Symbol>]
41
+ def names
42
+ @scene_map.keys
43
+ end
44
+
45
+ # @return [Array<Scene>]
46
+ def all
47
+ @scene_map.values
48
+ end
49
+
50
+ def introduction scene
51
+ introductions.push scene
52
+ end
53
+
54
+ def with_defaults narrative
55
+ maybe_add :default_scene, Scene::Activity, narrative
56
+ maybe_add :default_conclusion, Scene::Conclusion, narrative
57
+ end
58
+
59
+ def maybe_add name, klass, narrative
60
+ add klass.new(name, narrative) unless names.include?(name)
61
+ end
62
+
63
+ def empty?
64
+ @scene_map.empty? && introductions.empty?
65
+ end
66
+ end
67
+ end
68
+ end