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
@@ -1,137 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
+ require 'gamefic/active/cue'
5
+ require 'gamefic/active/epic'
6
+ require 'gamefic/active/messaging'
7
+ require 'gamefic/active/take'
4
8
 
5
9
  module Gamefic
6
- class NotConclusionError < RuntimeError; end
7
-
8
10
  # The Active module gives entities the ability to perform actions and
9
11
  # participate in scenes. The Actor class, for example, is an Entity
10
12
  # subclass that includes this module.
11
13
  #
12
14
  module Active
13
- # The scene in which the entity is currently participating.
14
- #
15
- # @return [Gamefic::Scene::Base]
16
- attr_reader :scene
15
+ include Logging
16
+ include Messaging
17
17
 
18
- # The scene class that will be cued for this entity on the next turn.
19
- # Usually set with the #prepare method.
18
+ # The cue that will be used to create a scene at the beginning of the next
19
+ # turn.
20
20
  #
21
- # @return [Class<Gamefic::Scene::Base>]
22
- attr_reader :next_scene
23
-
24
- attr_reader :next_options
21
+ # @return [Active::Cue, nil]
22
+ attr_reader :next_cue
25
23
 
26
- # The prompt for the previous scene.
27
- #
28
- # @return [String]
29
- attr_accessor :last_prompt
24
+ # @return [Symbol, nil]
25
+ def next_scene
26
+ next_cue&.scene
27
+ end
30
28
 
31
- # The input for the previous scene.
29
+ # The rulebooks that will be used to perform commands. Every plot and
30
+ # subplot has its own rulebook.
32
31
  #
33
- # @return [String]
34
- attr_accessor :last_input
32
+ # @return [Set<Gamefic::World::Rulebook>]
33
+ # def rulebooks
34
+ # @rulebooks ||= Set.new
35
+ # end
35
36
 
36
- # The playbooks that will be used to perform commands.
37
+ # The narratives in which the entity is participating.
37
38
  #
38
- # @return [Array<Gamefic::World::Playbook>]
39
- def playbooks
40
- @playbooks ||= []
41
- end
42
-
43
- def syntaxes
44
- playbooks.flat_map(&:syntaxes)
39
+ # @return [Epic]
40
+ def epic
41
+ @epic ||= Epic.new
45
42
  end
46
43
 
47
- # An array of actions waiting to be performed.
44
+ # An array of commands waiting to be executed.
48
45
  #
49
46
  # @return [Array<String>]
50
47
  def queue
51
48
  @queue ||= []
52
49
  end
53
50
 
54
- # A hash of values representing the state of a performing entity.
51
+ # A hash of data that will be sent to the user. The output is typically
52
+ # sent after a scene has started and before the user is prompted for input.
55
53
  #
56
- # @return [Hash{Symbol => Object}]
57
- def state
58
- @state ||= {}
59
- end
60
-
54
+ # @return [Hash]
61
55
  def output
62
56
  @output ||= {}
63
57
  end
64
58
 
65
- # Send a message to the entity.
66
- # This method will automatically wrap the message in HTML paragraphs.
67
- # To send a message without paragraph formatting, use #stream instead.
68
- #
69
- # @param message [String]
70
- def tell(message)
71
- if buffer_stack > 0
72
- append_buffer format(message)
73
- else
74
- super
75
- end
76
- end
77
-
78
- # Send a message to the Character as raw text.
79
- # Unlike #tell, this method will not wrap the message in HTML paragraphs.
80
- #
81
- # @param message [String]
82
- def stream(message)
83
- if buffer_stack > 0
84
- append_buffer message
85
- else
86
- super
87
- end
88
- end
89
-
90
59
  # Perform a command.
91
60
  #
92
- # The command's action will be executed immediately regardless of the
61
+ # The command's action will be executed immediately, regardless of the
93
62
  # entity's state.
94
63
  #
95
64
  # @example Send a command as a string
96
65
  # character.perform "take the key"
97
66
  #
98
- # @param command [String, Symbol]
99
- # @return [Gamefic::Action]
100
- def perform(*command)
101
- if command.length > 1
102
- STDERR.puts "[WARN] #{caller[0]}: Passing a verb and arguments to #perform is deprecated. Use #execute instead."
103
- execute command.first, *command[1..-1]
104
- else
105
- dispatchers.push Dispatcher.dispatch(self, command.first.to_s)
106
- proceed
107
- dispatchers.pop
108
- end
67
+ # @param command [String]
68
+ # @return [void]
69
+ def perform(command)
70
+ dispatchers.push Dispatcher.dispatch(self, command)
71
+ dispatchers.last.execute
72
+ dispatchers.pop
109
73
  end
110
74
 
111
75
  # Quietly perform a command.
112
76
  # This method executes the command exactly as #perform does, except it
113
- # buffers the resulting output instead of sending it to the user.
77
+ # buffers the resulting output instead of sending it to messages.
114
78
  #
115
- # @param command [String, Symbol]
79
+ # @param command [String]
116
80
  # @return [String] The output that resulted from performing the command.
117
- def quietly(*command)
118
- if command.length > 1
119
- STDERR.puts "#{caller[0]}: Passing a verb and arguments to #quietly is deprecated. Use #execute instead"
120
- execute command.first, *command[1..-1], quietly: true
121
- else
122
- dispatchers.push Dispatcher.dispatch(self, command.first.to_s)
123
- result = proceed quietly: true
124
- dispatchers.pop
125
- result
126
- end
81
+ def quietly(command)
82
+ messenger.buffer { perform command }
127
83
  end
128
84
 
129
85
  # Perform an action.
130
86
  # This is functionally identical to the `perform` method, except the
131
- # action must be declared as a verb with a list of parameters. Use
87
+ # action must be declared as a verb with a list of arguments. Use
132
88
  # `perform` if you need to parse a string as a command.
133
89
  #
134
- # The command will be executed immediately regardless of the entity's
90
+ # The command will be executed immediately, regardless of the entity's
135
91
  # state.
136
92
  #
137
93
  # @example
@@ -139,11 +95,10 @@ module Gamefic
139
95
  #
140
96
  # @param verb [Symbol]
141
97
  # @param params [Array]
142
- # @params quietly [Boolean]
143
98
  # @return [Gamefic::Action]
144
- def execute(verb, *params, quietly: false)
99
+ def execute(verb, *params)
145
100
  dispatchers.push Dispatcher.dispatch_from_params(self, verb, params)
146
- proceed quietly: quietly
101
+ dispatchers.last.execute
147
102
  dispatchers.pop
148
103
  end
149
104
 
@@ -170,138 +125,86 @@ module Gamefic
170
125
  # end
171
126
  # end
172
127
  #
173
- # @param quietly [Boolean] If true, return the action's output instead of appending it to #messages
174
- # @return [String, nil]
175
- def proceed quietly: false
176
- return if dispatchers.empty?
177
- a = dispatchers.last.next
178
- return if a.nil?
179
- prepare_buffer quietly
180
- a.execute
181
- flush_buffer quietly
128
+ # @return [void]
129
+ def proceed
130
+ dispatchers.last&.proceed&.execute
182
131
  end
183
132
 
184
- # Immediately start a new scene for the character.
185
- # Use #prepare if you want to declare a scene to be started at the
186
- # beginning of the next turn.
133
+ # Cue a scene to start in the next turn.
187
134
  #
188
- # @param new_scene [Class<Scene::Base>]
189
- # @param data [Hash] Additional scene data
190
- def cue new_scene, **data
191
- @next_scene = nil
192
- if new_scene.nil?
193
- @scene = nil
194
- else
195
- @scene = new_scene.new(self, **data)
196
- @scene.start
197
- end
198
- end
199
-
200
- # Prepare a scene to be started for this character at the beginning of the
201
- # next turn. As opposed to #cue, a prepared scene will not start until the
202
- # current scene finishes.
135
+ # @raise [ArgumentError] if the scene is not valid
203
136
  #
204
- # @param new_scene [Class<Scene::Base>]
205
- # @oaram data [Hash] Additional scene data
206
- def prepare new_scene, **data
207
- @next_scene = new_scene
208
- @next_options = data
209
- end
210
-
211
- # Return true if the character is expected to be in the specified scene on
212
- # the next turn.
213
- #
214
- # @return [Boolean]
215
- def will_cue? scene
216
- (@scene.class == scene and @next_scene.nil?) || @next_scene == scene
217
- end
137
+ # @param scene [Symbol]
138
+ # @param context [Hash] Extra data to pass to the scene's props
139
+ # @return [Cue]
140
+ def cue scene, **context
141
+ return @next_cue if @next_cue&.scene == scene && @next_cue&.context == context
218
142
 
219
- # Cue a conclusion. This method works like #cue, except it will raise a
220
- # NotConclusionError if the scene is not a Scene::Conclusion.
221
- #
222
- # @param new_scene [Class<Scene::Base>]
223
- # @oaram data [Hash] Additional scene data
224
- def conclude new_scene, **data
225
- raise NotConclusionError unless new_scene <= Scene::Conclusion
226
- cue new_scene, **data
227
- end
143
+ logger.debug "Overwriting existing cue `#{@next_cue.scene}` with `#{scene}`" if @next_cue
228
144
 
229
- # True if the character is in a conclusion.
230
- #
231
- # @return [Boolean]
232
- def concluded?
233
- !scene.nil? && scene.kind_of?(Scene::Conclusion)
145
+ @next_cue = Cue.new(scene, **context)
234
146
  end
147
+ alias prepare cue
235
148
 
236
- def accessible?
237
- false
149
+ def start_take
150
+ ensure_cue
151
+ @last_cue = @next_cue
152
+ cue :default_scene
153
+ @props = Take.start(self, @last_cue)
238
154
  end
239
155
 
240
- def inspect
241
- to_s
242
- end
156
+ def finish_take
157
+ return unless @last_cue
243
158
 
244
- # Track the entity's performance of a scene.
245
- #
246
- def entered scene
247
- klass = (scene.is_a?(Gamefic::Scene::Base) ? scene.class : scene)
248
- entered_scenes.add klass
159
+ Take.finish(self, @last_cue, @props)
249
160
  end
250
161
 
251
- # Determine whether the entity has performed the specified scene.
162
+ # Restart the scene from the most recent cue.
252
163
  #
253
- # @return [Boolean]
254
- def entered? scene
255
- klass = (scene.kind_of?(Gamefic::Scene::Base) ? scene.class : scene)
256
- entered_scenes.include?(klass)
257
- end
258
-
259
- private
164
+ # @return [Cue, nil]
165
+ def recue
166
+ logger.warn "No scene to recue" unless @last_cue
260
167
 
261
- def prepare_buffer quietly
262
- if quietly
263
- if buffer_stack == 0
264
- @buffer = ""
265
- end
266
- set_buffer_stack(buffer_stack + 1)
267
- end
168
+ @next_cue = @last_cue
268
169
  end
269
170
 
270
- def flush_buffer quietly
271
- if quietly
272
- set_buffer_stack(buffer_stack - 1)
273
- @buffer
274
- end
275
- end
171
+ # Cue a conclusion. This method works like #cue, except it will raise an
172
+ # error if the scene is not a conclusion.
173
+ #
174
+ # @raise [ArgumentError] if the requested scene is not a conclusion
175
+ #
176
+ # @param new_scene [Symbol]
177
+ # @oaram context [Hash] Additional scene data
178
+ def conclude scene, **context
179
+ cue scene, **context
180
+ available = epic.select_scene(scene)
181
+ raise ArgumentError, "`#{scene}` is not a conclusion" unless available.conclusion?
276
182
 
277
- # @return [Set<Gamefic::Scene::Base>]
278
- def entered_scenes
279
- @entered_scenes ||= Set.new
183
+ @next_cue
280
184
  end
281
185
 
282
- def buffer_stack
283
- @buffer_stack ||= 0
186
+ # True if the actor is ready to leave the game.
187
+ #
188
+ def concluding?
189
+ epic.empty? || (@last_cue && epic.conclusion?(@last_cue.scene))
284
190
  end
285
191
 
286
- def set_buffer_stack num
287
- @buffer_stack = num
192
+ def accessible?
193
+ false
288
194
  end
289
195
 
290
- # @return [String]
291
- def buffer
292
- @buffer ||= ''
293
- end
196
+ private
294
197
 
295
- def append_buffer str
296
- @buffer += str
198
+ # @return [Array<Dispatcher>]
199
+ def dispatchers
200
+ @dispatchers ||= []
297
201
  end
298
202
 
299
- def clear_buffer
300
- @buffer = ''
301
- end
203
+ def ensure_cue
204
+ return if next_cue
302
205
 
303
- def dispatchers
304
- @dispatchers ||= []
206
+ logger.debug "Using default scene for actor without cue"
207
+ cue :default_scene
305
208
  end
306
209
  end
307
210
  end
data/lib/gamefic/actor.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  # An entity that is capable of performing actions and participating in
3
5
  # scenes.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ # A code container for seeds and scripts.
5
+ #
6
+ class Block
7
+ # @return [Symbol]
8
+ attr_reader :type
9
+
10
+ # @return [Proc]
11
+ attr_reader :code
12
+
13
+ # @param type [Symbol]
14
+ # @param code [Proc]
15
+ def initialize type, code
16
+ @type = type
17
+ @code = code
18
+ end
19
+
20
+ def script?
21
+ type == :script
22
+ end
23
+
24
+ def seed?
25
+ type == :seed
26
+ end
27
+ end
28
+ end
@@ -1,15 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
- # A Command is a collection of tokens parsed from a Syntax.
3
- # Playbooks use Commands to find and execute corresponding Actions.
4
+ # A decomposition of a text-based command into its verb and arguments.
5
+ #
6
+ # Commands are typically derived from tokenization against syntaxes.
4
7
  #
5
8
  class Command
6
- # A Symbol representing the command's verb or verbal phrase.
7
- #
8
9
  # @return [Symbol]
9
10
  attr_reader :verb
10
11
 
11
- # An Array of arguments to be mapped to an Action's Queries.
12
- #
13
12
  # @return [Array<String>]
14
13
  attr_reader :arguments
15
14
 
@@ -17,5 +16,16 @@ module Gamefic
17
16
  @verb = verb
18
17
  @arguments = arguments
19
18
  end
19
+
20
+ # Compare two syntaxes for the purpose of ordering them by relevance while
21
+ # dispatching.
22
+ #
23
+ def compare other
24
+ if verb == other.verb
25
+ other.arguments.compact.length <=> arguments.compact.length
26
+ else
27
+ (other.verb ? 1 : 0) <=> (verb ? 1 : 0)
28
+ end
29
+ end
20
30
  end
21
31
  end
@@ -14,7 +14,7 @@ class Array
14
14
  #
15
15
  # @return [Array]
16
16
  def that_are(*cls)
17
- result = clone
17
+ result = dup
18
18
  cls.each do |c|
19
19
  _keep result, c, true
20
20
  end
@@ -26,7 +26,7 @@ class Array
26
26
  #
27
27
  # @return [Array]
28
28
  def that_are_not(*cls)
29
- result = clone
29
+ result = dup
30
30
  cls.each do |c|
31
31
  _keep result, c, false
32
32
  end
@@ -72,8 +72,8 @@ class Array
72
72
  case cls
73
73
  when Class, Module
74
74
  arr.keep_if { |i| i.is_a?(cls) == bool }
75
- when Symbol
76
- arr.keep_if { |i| i.send(cls) == bool }
75
+ when Proc
76
+ arr.keep_if { |i| !!cls.call(i) == bool }
77
77
  else
78
78
  arr.keep_if { |i| (i == cls) == bool }
79
79
  end
@@ -1,19 +1,24 @@
1
- class String
2
- include Gamefic::Keywords
1
+ # frozen_string_literal: true
3
2
 
3
+ class String
4
4
  # Capitalize the first letter without changing the rest of the string.
5
5
  # (String#capitalize makes the rest of the string lower-case.)
6
6
  #
7
7
  # @return [String] The capitalized text
8
8
  def capitalize_first
9
- "#{self[0, 1].upcase}#{self[1, self.length]}"
9
+ "#{self[0, 1].upcase}#{self[1, length]}"
10
10
  end
11
11
  alias cap_first capitalize_first
12
12
 
13
13
  # Get an array of words split by any whitespace.
14
14
  #
15
15
  # @return [Array]
16
- def split_words
17
- self.gsub(/[\s]+/, ' ').strip.split
16
+ def keywords
17
+ gsub(/[\s-]+/, ' ').strip.downcase.split - %w[a an the]
18
+ end
19
+
20
+ # @return [String]
21
+ def normalize
22
+ keywords.join(' ')
18
23
  end
19
24
  end