gamefic 2.3.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +41 -0
  3. data/.rspec-opal +2 -0
  4. data/.rubocop.yml +4 -1
  5. data/.solargraph.yml +20 -3
  6. data/CHANGELOG.md +15 -0
  7. data/Gemfile +0 -4
  8. data/Rakefile +11 -1
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/gamefic.gemspec +5 -2
  12. data/lib/gamefic/action.rb +53 -173
  13. data/lib/gamefic/active/cue.rb +25 -0
  14. data/lib/gamefic/active/epic.rb +68 -0
  15. data/lib/gamefic/active/messaging.rb +43 -0
  16. data/lib/gamefic/active/take.rb +69 -0
  17. data/lib/gamefic/active.rb +97 -192
  18. data/lib/gamefic/actor.rb +2 -0
  19. data/lib/gamefic/block.rb +28 -0
  20. data/lib/gamefic/command.rb +16 -6
  21. data/lib/gamefic/core_ext/array.rb +4 -4
  22. data/lib/gamefic/core_ext/string.rb +10 -5
  23. data/lib/gamefic/describable.rb +39 -65
  24. data/lib/gamefic/dispatcher.rb +67 -29
  25. data/lib/gamefic/entity.rb +44 -19
  26. data/lib/gamefic/logging.rb +32 -0
  27. data/lib/gamefic/messenger.rb +66 -0
  28. data/lib/gamefic/narrative.rb +104 -0
  29. data/lib/gamefic/node.rb +44 -53
  30. data/lib/gamefic/plot.rb +60 -93
  31. data/lib/gamefic/props/default.rb +41 -0
  32. data/lib/gamefic/props/multiple_choice.rb +65 -0
  33. data/lib/gamefic/props/pause.rb +11 -0
  34. data/lib/gamefic/props/yes_or_no.rb +21 -0
  35. data/lib/gamefic/props.rb +10 -0
  36. data/lib/gamefic/query/base.rb +45 -126
  37. data/lib/gamefic/query/general.rb +46 -0
  38. data/lib/gamefic/query/result.rb +20 -0
  39. data/lib/gamefic/query/scoped.rb +41 -0
  40. data/lib/gamefic/query/text.rb +30 -31
  41. data/lib/gamefic/query.rb +7 -15
  42. data/lib/gamefic/response.rb +118 -0
  43. data/lib/gamefic/rulebook/calls.rb +90 -0
  44. data/lib/gamefic/rulebook/events.rb +79 -0
  45. data/lib/gamefic/rulebook/hooks.rb +57 -0
  46. data/lib/gamefic/rulebook/scenes.rb +68 -0
  47. data/lib/gamefic/rulebook.rb +139 -0
  48. data/lib/gamefic/scanner.rb +103 -0
  49. data/lib/gamefic/scene/activity.rb +9 -17
  50. data/lib/gamefic/scene/conclusion.rb +6 -5
  51. data/lib/gamefic/scene/default.rb +88 -0
  52. data/lib/gamefic/scene/multiple_choice.rb +14 -69
  53. data/lib/gamefic/scene/pause.rb +9 -13
  54. data/lib/gamefic/scene/yes_or_no.rb +6 -46
  55. data/lib/gamefic/scene.rb +11 -7
  56. data/lib/gamefic/scope/base.rb +44 -0
  57. data/lib/gamefic/scope/children.rb +16 -0
  58. data/lib/gamefic/scope/family.rb +20 -0
  59. data/lib/gamefic/scope/myself.rb +13 -0
  60. data/lib/gamefic/scope/parent.rb +13 -0
  61. data/lib/gamefic/scope/siblings.rb +14 -0
  62. data/lib/gamefic/scope.rb +8 -0
  63. data/lib/gamefic/scriptable/actions.rb +156 -0
  64. data/lib/gamefic/scriptable/entities.rb +76 -0
  65. data/lib/gamefic/scriptable/events.rb +65 -0
  66. data/lib/gamefic/scriptable/proxy.rb +55 -0
  67. data/lib/gamefic/scriptable/queries.rb +73 -0
  68. data/lib/gamefic/scriptable/scenes.rb +162 -0
  69. data/lib/gamefic/scriptable.rb +167 -68
  70. data/lib/gamefic/snapshot.rb +36 -0
  71. data/lib/gamefic/stage.rb +51 -0
  72. data/lib/gamefic/subplot.rb +51 -79
  73. data/lib/gamefic/syntax/template.rb +67 -0
  74. data/lib/gamefic/syntax.rb +102 -83
  75. data/lib/gamefic/vault.rb +50 -0
  76. data/lib/gamefic/version.rb +3 -1
  77. data/lib/gamefic.rb +26 -15
  78. data/spec-opal/spec_helper.rb +24 -0
  79. metadata +92 -29
  80. data/lib/gamefic/element.rb +0 -46
  81. data/lib/gamefic/keywords.rb +0 -52
  82. data/lib/gamefic/messaging.rb +0 -43
  83. data/lib/gamefic/plot/darkroom.rb +0 -120
  84. data/lib/gamefic/plot/host.rb +0 -42
  85. data/lib/gamefic/plot/snapshot.rb +0 -27
  86. data/lib/gamefic/query/children.rb +0 -9
  87. data/lib/gamefic/query/descendants.rb +0 -15
  88. data/lib/gamefic/query/external.rb +0 -39
  89. data/lib/gamefic/query/family.rb +0 -18
  90. data/lib/gamefic/query/itself.rb +0 -13
  91. data/lib/gamefic/query/matches.rb +0 -75
  92. data/lib/gamefic/query/parent.rb +0 -9
  93. data/lib/gamefic/query/siblings.rb +0 -13
  94. data/lib/gamefic/query/tree.rb +0 -17
  95. data/lib/gamefic/scene/base.rb +0 -142
  96. data/lib/gamefic/scene/multiple_scene.rb +0 -29
  97. data/lib/gamefic/serialize.rb +0 -196
  98. data/lib/gamefic/world/callbacks.rb +0 -135
  99. data/lib/gamefic/world/commands.rb +0 -173
  100. data/lib/gamefic/world/entities.rb +0 -98
  101. data/lib/gamefic/world/playbook.rb +0 -225
  102. data/lib/gamefic/world/players.rb +0 -37
  103. data/lib/gamefic/world/scenes.rb +0 -226
  104. 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