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