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
@@ -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