gamefic 2.4.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scope
5
+ # A query scope that matches the entity's siblings, i.e., the other
6
+ # entities that share its parent.
7
+ #
8
+ class Siblings < Base
9
+ def matches
10
+ context.parent.children - [context]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gamefic/scope/base'
4
+ require 'gamefic/scope/children'
5
+ require 'gamefic/scope/family'
6
+ require 'gamefic/scope/parent'
7
+ require 'gamefic/scope/siblings'
8
+ require 'gamefic/scope/myself'
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scriptable
5
+ # Scriptable methods related to creating actions.
6
+ #
7
+ module Actions
8
+ include Queries
9
+
10
+ # Create a response to a command.
11
+ # A Response uses the `verb` argument to identify the imperative verb
12
+ # that triggers the action. It can also accept queries to tokenize the
13
+ # remainder of the input and filter for particular entities or
14
+ # properties. The `block`` argument is the proc to execute when the input
15
+ # matches all of the Response's criteria (i.e., verb and queries).
16
+ #
17
+ # @example A simple Response.
18
+ # respond :wave do |actor|
19
+ # actor.tell "Hello!"
20
+ # end
21
+ # # The command "wave" will respond "Hello!"
22
+ #
23
+ # @example A Response that accepts a Character
24
+ # respond :salute, available(Character) do |actor, character|
25
+ # actor.tell "#{The character} returns your salute."
26
+ # end
27
+ #
28
+ # @param verb [Symbol] An imperative verb for the command
29
+ # @param queries [Array<Query::Base, Query::Text>] Filters for the command's tokens
30
+ # @yieldparam [Gamefic::Actor]
31
+ # @return [Symbol]
32
+ def respond(verb, *queries, &proc)
33
+ args = map_response_args(queries)
34
+ rulebook.calls.add_response Response.new(verb, rulebook.narrative, *args, &proc)
35
+ verb
36
+ end
37
+
38
+ # Create a meta response for a command.
39
+ # Meta responses are very similar to standard responses, except they're
40
+ # flagged as meta (`Response#meta?`) to indicate that they provide a
41
+ # feature that is not considered an in-game action, such as displaying
42
+ # help documentation or a scoreboard.
43
+ #
44
+ # @example A simple meta Response
45
+ # meta :credits do |actor|
46
+ # actor.tell "This game was written by John Smith."
47
+ # end
48
+ #
49
+ # @param verb [Symbol] An imperative verb for the command
50
+ # @param queries [Array<Query::Base, Query::Text>] Filters for the command's tokens
51
+ # @yieldparam [Gamefic::Actor]
52
+ # @return [Symbol]
53
+ def meta(verb, *queries, &proc)
54
+ args = map_response_args(queries)
55
+ rulebook.calls.add_response Response.new(verb, rulebook.narrative, *args, meta: true, &proc)
56
+ verb
57
+ end
58
+
59
+ # Add a proc to be evaluated before a character executes an action.
60
+ # When verbs are specified, the proc will only be evaluated if the
61
+ # action's verb matches them.
62
+ #
63
+ # @param verbs [Array<Symbol>]
64
+ # @yieldparam [Gamefic::Action]
65
+ # @return [Action::Hook]
66
+ def before_action *verbs, &block
67
+ rulebook.hooks.before_action *verbs, &block
68
+ end
69
+
70
+ # Add a proc to be evaluated after a character executes an action.
71
+ # When a verbs are specified, the proc will only be evaluated if the
72
+ # action's verb matches them.
73
+ #
74
+ # @param verbs [Array<Symbol>]
75
+ # @yieldparam [Gamefic::Action]
76
+ # @return [Action::Hook]
77
+ def after_action *verbs, &block
78
+ rulebook.hooks.after_action *verbs, &block
79
+ end
80
+
81
+ # Create an alternate Syntax for a response.
82
+ # The command and its translation can be parameterized.
83
+ #
84
+ # @example Create a synonym for an `inventory` response.
85
+ # interpret "catalogue", "inventory"
86
+ # # The command "catalogue" will be translated to "inventory"
87
+ #
88
+ # @example Create a parameterized synonym for a `look` response.
89
+ # interpret "scrutinize :entity", "look :entity"
90
+ # # The command "scrutinize chair" will be translated to "look chair"
91
+ #
92
+ # @param command [String] The format of the original command
93
+ # @param translation [String] The format of the translated command
94
+ # @return [Syntax] the Syntax object
95
+ def interpret command, translation
96
+ rulebook.calls.add_syntax Syntax.new(command, translation)
97
+ end
98
+
99
+ # Verbs are the symbols that have responses defined in the rulebook.
100
+ #
101
+ # @example
102
+ # class MyPlot < Gamefic::Plot
103
+ # script do
104
+ # respond :think { |actor| actor.tell 'You think.' }
105
+ #
106
+ # verbs #=> [:think]
107
+ # end
108
+ # end
109
+ #
110
+ # @return [Array<Symbol>]
111
+ def verbs
112
+ rulebook.verbs
113
+ end
114
+
115
+ # Synonyms are a combination of the rulebook's concrete verbs plus the
116
+ # alternative variants defined in syntaxes.
117
+ #
118
+ # @example
119
+ # class MyPlot < Gamefic::Plot
120
+ # respond :think { |actor| actor.tell 'You think.' }
121
+ # interpret 'ponder', 'think'
122
+ #
123
+ # verbs #=> [:think]
124
+ # synonyms #=> [:think, :ponder]
125
+ # end
126
+ # end
127
+ #
128
+ # @return [Array<Symbol>]
129
+ def synonyms
130
+ rulebook.synonyms
131
+ end
132
+
133
+ # @return [Array<Syntax>]
134
+ def syntaxes
135
+ rulebook.syntaxes
136
+ end
137
+
138
+ private
139
+
140
+ def map_response_args args
141
+ args.map do |arg|
142
+ case arg
143
+ when Entity, Class, Module, Proc, Proxy::Agent
144
+ available(arg)
145
+ when String, Regexp
146
+ plaintext(arg)
147
+ when Gamefic::Query::Base, Gamefic::Query::Text
148
+ arg
149
+ else
150
+ raise ArgumentError, "invalid argument in response: #{arg.inspect}"
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scriptable
5
+ # Scriptable methods related to managing entities.
6
+ #
7
+ # @note The public versions of the entity and player arrays are frozen.
8
+ # Authors need access to them but shouldn't modify them directly. Use
9
+ # #make and #destroy instead.
10
+ #
11
+ module Entities
12
+ include Proxy
13
+
14
+ def entity_vault
15
+ @entity_vault ||= Vault.new
16
+ end
17
+
18
+ def player_vault
19
+ @player_vault ||= Vault.new
20
+ end
21
+
22
+ # @return [Array<Gamefic::Entity>]
23
+ def entities
24
+ entity_vault.array
25
+ end
26
+
27
+ # @return [Array<Gamefic::Actor, Gamefic::Active>]
28
+ def players
29
+ player_vault.array
30
+ end
31
+
32
+ # Create an entity.
33
+ #
34
+ # @example
35
+ # class MyPlot < Gamefic::Plot
36
+ # seed { make Gamefic::Entity, name: 'thing' }
37
+ # end
38
+ #
39
+ # @param [Class<Gamefic::Entity>]
40
+ # @param args [Hash]
41
+ # @return [Gamefic::Entity]
42
+ def make klass, **opts
43
+ entity_vault.add klass.new(**unproxy(opts))
44
+ end
45
+
46
+ def destroy entity
47
+ entity.children.each { |child| destroy child }
48
+ entity.parent = nil
49
+ entity_vault.delete entity
50
+ end
51
+
52
+ # Pick an entity based on a unique name or description. Return nil if an
53
+ # entity could not be found or there is more than one possible match.
54
+ #
55
+ # @param description [String]
56
+ # @return [Gamefic::Entity, nil]
57
+ def pick description
58
+ Gamefic::Query::General.new(entities).query(nil, description).match
59
+ end
60
+
61
+ # Same as #pick, but raise an error if a unique match could not be found.
62
+ #
63
+ # @param description [String]
64
+ # @return [Gamefic::Entity, nil]
65
+ def pick! description
66
+ ary = Gamefic::Query::General.new(entities, ambiguous: true).query(nil, description).match
67
+
68
+ raise "no entity matching '#{description}'" if ary.nil?
69
+
70
+ raise "multiple entities matching '#{description}': #{ary.join_and}" unless ary.one?
71
+
72
+ ary.first
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scriptable
5
+ # Scriptable methods related to creating event callbacks.
6
+ #
7
+ module Events
8
+ # Add a block to be executed on preparation of every turn.
9
+ #
10
+ # @example Increment a turn counter
11
+ # turn = 0
12
+ # on_ready do
13
+ # turn += 1
14
+ # end
15
+ #
16
+ def on_ready &block
17
+ rulebook.events.on_ready(&block)
18
+ end
19
+
20
+ # Add a block to be executed for each player at the beginning of a turn.
21
+ #
22
+ # @example Tell the player how many turns they've played.
23
+ # on_player_ready do |player|
24
+ # player[:turns] ||= 1
25
+ # player.tell "Turn #{player[:turns]}"
26
+ # player[:turns] += 1
27
+ # end
28
+ #
29
+ # @yieldparam [Gamefic::Actor]
30
+ def on_player_ready &block
31
+ rulebook.events.on_player_ready(&block)
32
+ end
33
+
34
+ # Add a block to be executed after the Plot is finished updating a turn.
35
+ #
36
+ def on_update &block
37
+ rulebook.events.on_update(&block)
38
+ end
39
+
40
+ # Add a block to be executed for each player at the end of a turn.
41
+ #
42
+ # @yieldparam [Gamefic::Actor]
43
+ def on_player_update &block
44
+ rulebook.events.on_player_update(&block)
45
+ end
46
+
47
+ def on_conclude &block
48
+ rulebook.events.on_conclude(&block)
49
+ end
50
+
51
+ # @yieldparam [Actor]
52
+ # @return [Proc]
53
+ def on_player_conclude &block
54
+ rulebook.events.on_player_conclude(&block)
55
+ end
56
+
57
+ # @yieldparam [Actor]
58
+ # @yieldparam [Hash]
59
+ # @return [Proc]
60
+ def on_player_output &block
61
+ rulebook.events.on_player_output(&block)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,55 @@
1
+ module Gamefic
2
+ module Scriptable
3
+ # Functions that provide proxies for referencing a narrative's entities
4
+ # from class-level scripts.
5
+ #
6
+ module Proxy
7
+ # The object that fetches a proxied entity.
8
+ #
9
+ class Agent
10
+ attr_reader :symbol
11
+
12
+ # @param symbol [Symbol, Integer]
13
+ def initialize symbol
14
+ @symbol = symbol
15
+ end
16
+
17
+ def fetch container
18
+ if symbol.to_s =~ /^\d+$/
19
+ Stage.run(container, symbol) { |sym| entities[sym] }
20
+ elsif symbol.to_s.start_with?('@')
21
+ Stage.run(container, symbol) { |sym| instance_variable_get(sym) }
22
+ else
23
+ Stage.run(container, symbol) { |sym| send(sym) }
24
+ end
25
+ end
26
+ end
27
+
28
+ # Proxy a method or instance variable.
29
+ #
30
+ # @example
31
+ # proxy(:method_name)
32
+ # proxy(:@instance_variable_name)
33
+ #
34
+ # @param symbol [Symbol]
35
+ def proxy symbol
36
+ Agent.new(symbol)
37
+ end
38
+
39
+ # @param object [Object]
40
+ # @return [Object]
41
+ def unproxy object
42
+ case object
43
+ when Agent
44
+ object.fetch self
45
+ when Array
46
+ object.map { |obj| unproxy obj }
47
+ when Hash
48
+ object.transform_values { |val| unproxy val }
49
+ else
50
+ object
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scriptable
5
+ # Scriptable methods related to creating action queries.
6
+ #
7
+ module Queries
8
+ include Proxy
9
+
10
+ # Define a query that searches the entire plot's entities.
11
+ #
12
+ # @param args [Array<Object>] Query arguments
13
+ # @return [Query::General]
14
+ def anywhere *args, ambiguous: false
15
+ Query::General.new -> { entities }, *unproxy(args), ambiguous: ambiguous
16
+ end
17
+
18
+ # Define a query that searches an actor's family of entities. The
19
+ # results include the parent, siblings, children, and accessible
20
+ # descendants of siblings and children.
21
+ #
22
+ # @param args [Array<Object>] Query arguments
23
+ # @return [Query::Scoped]
24
+ def available *args, ambiguous: false
25
+ Query::Scoped.new Scope::Family, *unproxy(args), ambiguous: ambiguous
26
+ end
27
+ alias family available
28
+
29
+ # Define a query that returns the actor's parent.
30
+ #
31
+ # @param args [Array<Object>] Query arguments
32
+ # @return [Query::Scoped]
33
+ def parent *args, ambiguous: false
34
+ Query::Scoped.new Scope::Parent, *unproxy(args), ambiguous: ambiguous
35
+ end
36
+
37
+ # Define a query that searches an actor's children.
38
+ #
39
+ # @param args [Array<Object>] Query arguments
40
+ # @return [Query::Scoped]
41
+ def children *args, ambiguous: false
42
+ Query::Scoped.new Scope::Children, *unproxy(args), ambiguous: ambiguous
43
+ end
44
+
45
+ # Define a query that searches an actor's siblings.
46
+ #
47
+ # @param args [Array<Object>] Query arguments
48
+ # @return [Query::Scoped]
49
+ def siblings *args, ambiguous: false
50
+ Query::Scoped.new Scope::Siblings, *unproxy(args), ambiguous: ambiguous
51
+ end
52
+
53
+ # Define a query that returns the actor itself.
54
+ #
55
+ # @param args [Array<Object>] Query arguments
56
+ # @return [Query::Scoped]
57
+ def myself *args, ambiguous: false
58
+ Query::Scoped.new Scope::Myself, *unproxy(args), ambiguous: ambiguous
59
+ end
60
+
61
+ # Define a query that performs a plaintext search. It can take a String
62
+ # or a RegExp as an argument. If no argument is provided, it will match
63
+ # any text it finds in the command. A successful query returns the
64
+ # corresponding text instead of an entity.
65
+ #
66
+ # @param arg [String, Regrxp] The string or regular expression to match
67
+ # @return [Query::Text]
68
+ def plaintext arg = nil
69
+ Query::Text.new arg
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scriptable
5
+ # Scriptable methods related to creating scenes.
6
+ #
7
+ module Scenes
8
+ # Block a new scene.
9
+ #
10
+ # @example Prompt the player for a name
11
+ # block :name_of_scene do |scene|
12
+ # # The scene's start occurs before the user gets prompted for input
13
+ # scene.on_start do |actor, props|
14
+ # props.prompt = 'What's your name?'
15
+ # end
16
+ #
17
+ # # The scene's finish is where you can process the user's input
18
+ # scene.on_finish do |actor, props|
19
+ # if props.input.empty?
20
+ # # You can use recue to start the scene again
21
+ # actor.recue
22
+ # else
23
+ # actor.tell "Hello, #{props.input}!"
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # @param name [Symbol]
29
+ # @param klass [Class<Scene::Default>]
30
+ # @param on_start [Proc, nil]
31
+ # @param on_finish [Proc, nil]
32
+ # @param block [Proc]
33
+ # @yieldparam [Scene]
34
+ # @return [Symbol]
35
+ def block name, klass = Scene::Default, on_start: nil, on_finish: nil, &block
36
+ rulebook.scenes.add klass.new(name, rulebook.narrative, on_start: on_start, on_finish: on_finish, &block)
37
+ name
38
+ end
39
+ alias scene block
40
+
41
+ # Add a block to be executed when a player is added to the game.
42
+ # Each Plot should only have one introduction.
43
+ #
44
+ # @example Welcome the player to the game
45
+ # introduction do |actor|
46
+ # actor.tell "Welcome to the game!"
47
+ # end
48
+ #
49
+ # @raise [ArgumentError] if an introduction already exists
50
+ #
51
+ # @yieldparam [Gamefic::Actor]
52
+ # @yieldparam [Props::Default]
53
+ # @return [Symbol]
54
+ def introduction(&start)
55
+ rulebook.scenes
56
+ .introduction Scene::Default.new nil,
57
+ rulebook.narrative,
58
+ on_start: proc { |actor, _props| instance_exec(actor, &start) }
59
+ end
60
+
61
+ # Create a multiple-choice scene.
62
+ # The user will be required to make a choice to continue. The scene
63
+ # will restart if the user input is not a valid choice.
64
+ #
65
+ # @example
66
+ # multiple_choice :go_somewhere, ['Go to work', 'Go to school'] do |actor, props|
67
+ # # Assuming the user selected the first choice:
68
+ # props.selection #=> 'Go to work'
69
+ # props.index #=> 0
70
+ # props.number #=> 1
71
+ # end
72
+ #
73
+ # @param name [Symbol]
74
+ # @param choices [Array<String>]
75
+ # @param prompt [String, nil]
76
+ # @param proc [Proc]
77
+ # @yieldparam [Actor]
78
+ # @yieldparam [Props::MultipleChoice]
79
+ # @return [Symbol]
80
+ def multiple_choice name, choices = [], prompt = 'What is your choice?', &block
81
+ block name,
82
+ Scene::MultipleChoice,
83
+ on_start: proc { |_actor, props|
84
+ props.prompt = prompt
85
+ props.options.concat choices
86
+ },
87
+ on_finish: block
88
+ end
89
+
90
+ # Create a yes-or-no scene.
91
+ # The user will be required to answer Yes or No to continue. The scene
92
+ # will restart if the user input is not a valid choice.
93
+ #
94
+ # @example
95
+ # yes_or_no :answer_scene, 'What is your answer?' do |actor, props|
96
+ # if props.yes?
97
+ # actor.tell "You said yes."
98
+ # else
99
+ # actor.tell "You said no."
100
+ # end
101
+ # end
102
+ #
103
+ # @param name [Symbol]
104
+ # @param prompt [String, nil]
105
+ # @yieldparam [Actor]
106
+ # @yieldparam [Props::YesOrNo]
107
+ # @return [Symbol]
108
+ def yes_or_no name, prompt = 'Answer:', &block
109
+ block name,
110
+ Scene::YesOrNo,
111
+ on_start: proc { |_actor, props|
112
+ props.prompt = prompt
113
+ },
114
+ on_finish: block
115
+ end
116
+
117
+ # Create a scene that pauses the game.
118
+ # This scene will execute the specified block and wait for input from the
119
+ # the user (e.g., pressing Enter) to continue.
120
+ #
121
+ # @example
122
+ # pause :wait do |actor|
123
+ # actor.tell "After you continue, you will be prompted for a command."
124
+ # end
125
+ #
126
+ # @param name [Symbol]
127
+ # @param prompt [String, nil] The text to display when prompting the user to continue
128
+ # @yieldparam [Actor]
129
+ # @return [Symbol]
130
+ def pause name, prompt: 'Press enter to continue...', &start
131
+ block name,
132
+ Scene::Pause,
133
+ on_start: proc { |actor, props|
134
+ props.prompt = prompt if prompt
135
+ instance_exec(actor, props, &start)
136
+ }
137
+ end
138
+
139
+ # Create a conclusion.
140
+ # The game (or the character's participation in it) will end after this
141
+ # scene is complete.
142
+ #
143
+ # @example
144
+ # conclusion :ending do |actor|
145
+ # actor.tell 'GAME OVER'
146
+ # end
147
+ #
148
+ # @param name [Symbol]
149
+ # @yieldparam [Actor]
150
+ # @return [Symbol]
151
+ def conclusion name, &start
152
+ block name,
153
+ Scene::Conclusion,
154
+ on_start: start
155
+ end
156
+
157
+ def scenes
158
+ rulebook.scenes.names
159
+ end
160
+ end
161
+ end
162
+ end