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