gamefic 2.0.2 → 2.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +15 -1
- data/lib/gamefic/action.rb +37 -41
- data/lib/gamefic/active.rb +49 -76
- data/lib/gamefic/core_ext/array.rb +4 -1
- data/lib/gamefic/core_ext/string.rb +2 -8
- data/lib/gamefic/dispatcher.rb +76 -0
- data/lib/gamefic/element.rb +11 -12
- data/lib/gamefic/entity.rb +3 -3
- data/lib/gamefic/plot.rb +2 -9
- data/lib/gamefic/query/base.rb +25 -24
- data/lib/gamefic/query/descendants.rb +2 -2
- data/lib/gamefic/query/family.rb +2 -0
- data/lib/gamefic/query/text.rb +10 -11
- data/lib/gamefic/query/tree.rb +17 -0
- data/lib/gamefic/query.rb +1 -0
- data/lib/gamefic/scene/activity.rb +1 -3
- data/lib/gamefic/scene/base.rb +4 -2
- data/lib/gamefic/scene/conclusion.rb +1 -1
- data/lib/gamefic/scene/multiple_choice.rb +2 -12
- data/lib/gamefic/scene/pause.rb +1 -1
- data/lib/gamefic/scene/yes_or_no.rb +1 -1
- data/lib/gamefic/scene.rb +0 -1
- data/lib/gamefic/serialize.rb +15 -26
- data/lib/gamefic/subplot.rb +3 -12
- data/lib/gamefic/syntax.rb +25 -18
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic/world/commands.rb +7 -26
- data/lib/gamefic/world/entities.rb +0 -10
- data/lib/gamefic/world/playbook.rb +42 -88
- data/lib/gamefic/world/players.rb +18 -2
- data/lib/gamefic/world/scenes.rb +11 -11
- data/lib/gamefic/world.rb +2 -0
- data/lib/gamefic.rb +2 -2
- metadata +18 -18
- data/lib/gamefic/plot/index.rb +0 -92
- data/lib/gamefic/scene/custom.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 361eaf767babb3c8b5d7e47616d326a85e1b42269da33c518de33f16f9568451
|
4
|
+
data.tar.gz: 96f4d79e833f93c41714c5781cf863d7e0b8832e90c746f6bafe6164e10095b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9ad845f6f0271da17b64268b911f5d3fe40fbfd7fd563b411f6ef02e15b8a117abcd924d3586072040f428aafc0a92827458639f9f2c0ba9fa4883af7f33e05
|
7
|
+
data.tar.gz: 6f65996ac0ee16805763dd25fa89b1809a89acd0e44cb5d55f83ce22636aeadb6e43b733a15d44e6a397a636d5abde899c332d3601af60bcc4322a0c0ad1fb72
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,2 +1,16 @@
|
|
1
|
-
|
1
|
+
## 2.2.0 - September 4, 2021
|
2
|
+
- Dynamically inherit default attributes
|
3
|
+
|
4
|
+
## 2.1.1 - July 23, 2021
|
5
|
+
- Remove gamefic/scene/custom autoload
|
6
|
+
|
7
|
+
## 2.1.0 - June 21, 2021
|
8
|
+
- Remove redundant MultipleChoice prompt
|
9
|
+
- Deprecate Scene::Custom
|
10
|
+
|
11
|
+
## 2.0.3 - December 14, 2020
|
12
|
+
- Remove unused Index class
|
13
|
+
- Active#conclude accepts data argument
|
14
|
+
|
15
|
+
## 2.0.2 - April 25, 2020
|
2
16
|
- Improved snapshot serialization
|
data/lib/gamefic/action.rb
CHANGED
@@ -1,9 +1,4 @@
|
|
1
1
|
module Gamefic
|
2
|
-
# Exception raised when the Action's proc arity is not compatible with the
|
3
|
-
# number of queries
|
4
|
-
class ActionArgumentError < ArgumentError
|
5
|
-
end
|
6
|
-
|
7
2
|
class Action
|
8
3
|
# An array of objects on which the action will operate, e.g., an entity
|
9
4
|
# that is a direct object of a command.
|
@@ -54,25 +49,32 @@ module Gamefic
|
|
54
49
|
self.class.meta?
|
55
50
|
end
|
56
51
|
|
57
|
-
|
52
|
+
# @param verb [Symbol]
|
53
|
+
# @param queries [Array<Gamefic::Query::Base>]
|
54
|
+
# @param meta [Boolean]
|
55
|
+
# @return [Class<Action>]
|
56
|
+
def self.subclass verb, *queries, meta: false, &block
|
58
57
|
act = Class.new(self) do
|
59
58
|
self.verb = verb
|
60
59
|
self.meta = meta
|
61
|
-
|
60
|
+
queries.each do |q|
|
62
61
|
add_query q
|
63
|
-
|
62
|
+
end
|
64
63
|
on_execute &block
|
65
64
|
end
|
66
|
-
if !block.nil?
|
67
|
-
raise
|
65
|
+
if !block.nil? && act.queries.length + 1 != block.arity && block.arity > 0
|
66
|
+
raise ArgumentError.new("Number of parameters is not compatible with proc arguments")
|
68
67
|
end
|
69
68
|
act
|
70
69
|
end
|
71
70
|
|
72
71
|
class << self
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
attr_reader :verb
|
73
|
+
|
74
|
+
# The proc to call when the action is executed
|
75
|
+
#
|
76
|
+
# @return [Proc]
|
77
|
+
attr_reader :executor
|
76
78
|
|
77
79
|
def meta?
|
78
80
|
@meta ||= false
|
@@ -93,7 +95,7 @@ module Gamefic
|
|
93
95
|
|
94
96
|
def signature
|
95
97
|
# @todo This is clearly unfinished
|
96
|
-
"#{verb} #{queries.map{|m| m.signature}.join(', ')}"
|
98
|
+
"#{verb} #{queries.map {|m| m.signature}.join(', ')}"
|
97
99
|
end
|
98
100
|
|
99
101
|
# True if this action is not intended to be performed directly by a
|
@@ -107,19 +109,13 @@ module Gamefic
|
|
107
109
|
verb.to_s.start_with?('_')
|
108
110
|
end
|
109
111
|
|
110
|
-
#
|
111
|
-
#
|
112
|
-
# @return [Proc]
|
113
|
-
def executor
|
114
|
-
@executor
|
115
|
-
end
|
116
|
-
|
112
|
+
# @return [Integer]
|
117
113
|
def rank
|
118
114
|
if @rank.nil?
|
119
115
|
@rank = 0
|
120
|
-
queries.each
|
116
|
+
queries.each do |q|
|
121
117
|
@rank += (q.rank + 1)
|
122
|
-
|
118
|
+
end
|
123
119
|
@rank -= 1000 if verb.nil?
|
124
120
|
end
|
125
121
|
@rank
|
@@ -127,23 +123,28 @@ module Gamefic
|
|
127
123
|
|
128
124
|
def valid? actor, objects
|
129
125
|
return false if objects.length != queries.length
|
130
|
-
|
131
|
-
queries.each { |p|
|
126
|
+
queries.each_with_index do |p, i|
|
132
127
|
return false unless p.include?(actor, objects[i])
|
133
|
-
|
134
|
-
}
|
128
|
+
end
|
135
129
|
true
|
136
130
|
end
|
137
131
|
|
138
|
-
|
139
|
-
|
132
|
+
# Return an instance of this Action if the actor can execute it with the
|
133
|
+
# provided tokens, or nil if the tokens are invalid.
|
134
|
+
#
|
135
|
+
# @param action [Gamefic::Entity]
|
136
|
+
# @param command [Command]
|
137
|
+
# @return [self, nil]
|
138
|
+
def attempt actor, command
|
139
|
+
return nil if command.verb != verb
|
140
|
+
tokens = command.arguments
|
140
141
|
result = []
|
141
142
|
matches = Gamefic::Query::Matches.new([], '', '')
|
142
|
-
queries.
|
143
|
-
return nil if tokens[i].nil?
|
143
|
+
queries.each_with_index do |p, i|
|
144
|
+
return nil if tokens[i].nil? && matches.remaining == ''
|
144
145
|
matches = p.resolve(actor, "#{matches.remaining} #{tokens[i]}".strip, continued: (i < queries.length - 1))
|
145
146
|
return nil if matches.objects.empty?
|
146
|
-
accepted = matches.objects.select{|o| p.accept?(o)}
|
147
|
+
accepted = matches.objects.select { |o| p.accept?(o) }
|
147
148
|
return nil if accepted.empty?
|
148
149
|
if p.ambiguous?
|
149
150
|
result.push accepted
|
@@ -151,20 +152,15 @@ module Gamefic
|
|
151
152
|
return nil if accepted.length != 1
|
152
153
|
result.push accepted.first
|
153
154
|
end
|
154
|
-
|
155
|
-
|
156
|
-
self.new(actor, result)
|
155
|
+
end
|
156
|
+
new(actor, result)
|
157
157
|
end
|
158
158
|
|
159
159
|
protected
|
160
160
|
|
161
|
-
|
162
|
-
@verb = sym
|
163
|
-
end
|
161
|
+
attr_writer :verb
|
164
162
|
|
165
|
-
|
166
|
-
@meta = bool
|
167
|
-
end
|
163
|
+
attr_writer :meta
|
168
164
|
end
|
169
165
|
end
|
170
166
|
end
|
data/lib/gamefic/active.rb
CHANGED
@@ -6,12 +6,6 @@ module Gamefic
|
|
6
6
|
# subclass that includes this module.
|
7
7
|
#
|
8
8
|
module Active
|
9
|
-
# The last action executed by the entity, as reported by the
|
10
|
-
# Active#performed method.
|
11
|
-
#
|
12
|
-
# @return [Gamefic::Action]
|
13
|
-
attr_reader :last_action
|
14
|
-
|
15
9
|
# The scene in which the entity is currently participating.
|
16
10
|
#
|
17
11
|
# @return [Gamefic::Scene::Base]
|
@@ -104,22 +98,32 @@ module Gamefic
|
|
104
98
|
# @example Send a command as a verb with parameters
|
105
99
|
# character.perform :take, @key
|
106
100
|
#
|
101
|
+
# @todo Modify this method so it only accepts a single command.
|
102
|
+
# Verbs with parameters should use Active#execute instead.
|
103
|
+
# It might be necessary to support command splats with a deprecation
|
104
|
+
# warning until version 3.
|
105
|
+
#
|
107
106
|
# @return [Gamefic::Action]
|
108
107
|
def perform(*command)
|
109
|
-
|
110
|
-
|
111
|
-
|
108
|
+
if command.length > 1
|
109
|
+
execute command.first, *command[1..-1]
|
110
|
+
else
|
111
|
+
dispatchers.push Dispatcher.dispatch(self, command.first.to_s)
|
112
|
+
proceed
|
113
|
+
dispatchers.pop
|
114
|
+
end
|
112
115
|
end
|
113
116
|
|
114
117
|
# Quietly perform a command.
|
115
118
|
# This method executes the command exactly as #perform does, except it
|
116
119
|
# buffers the resulting output instead of sending it to the user.
|
117
120
|
#
|
121
|
+
# @todo Modify this method so it only accepts a single command.
|
122
|
+
# See Active#perform for more information.
|
123
|
+
#
|
118
124
|
# @return [String] The output that resulted from performing the command.
|
119
125
|
def quietly(*command)
|
120
|
-
if buffer_stack == 0
|
121
|
-
clear_buffer
|
122
|
-
end
|
126
|
+
clear_buffer if buffer_stack == 0
|
123
127
|
set_buffer_stack buffer_stack + 1
|
124
128
|
self.perform *command
|
125
129
|
set_buffer_stack buffer_stack - 1
|
@@ -139,9 +143,9 @@ module Gamefic
|
|
139
143
|
#
|
140
144
|
# @return [Gamefic::Action]
|
141
145
|
def execute(verb, *params, quietly: false)
|
142
|
-
|
143
|
-
|
144
|
-
|
146
|
+
dispatchers.push Dispatcher.dispatch_from_params(self, verb, params)
|
147
|
+
proceed quietly: quietly
|
148
|
+
dispatchers.pop
|
145
149
|
end
|
146
150
|
|
147
151
|
# Proceed to the next Action in the current stack.
|
@@ -152,10 +156,12 @@ module Gamefic
|
|
152
156
|
# introduction do |actor|
|
153
157
|
# actor[:has_eaten] = false # Initial value
|
154
158
|
# end
|
159
|
+
#
|
155
160
|
# respond :eat do |actor|
|
156
161
|
# actor.tell "You eat something."
|
157
162
|
# actor[:has_eaten] = true
|
158
163
|
# end
|
164
|
+
#
|
159
165
|
# respond :eat do |actor|
|
160
166
|
# # This version will be executed first because it was implemented last
|
161
167
|
# if actor[:has_eaten]
|
@@ -166,20 +172,19 @@ module Gamefic
|
|
166
172
|
# end
|
167
173
|
#
|
168
174
|
def proceed quietly: false
|
169
|
-
return if
|
170
|
-
a =
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
end
|
176
|
-
set_buffer_stack(buffer_stack + 1)
|
177
|
-
end
|
178
|
-
a.execute
|
179
|
-
if quietly
|
180
|
-
set_buffer_stack(buffer_stack - 1)
|
181
|
-
@buffer
|
175
|
+
return if dispatchers.empty?
|
176
|
+
a = dispatchers.last.next
|
177
|
+
return if a.nil?
|
178
|
+
if quietly
|
179
|
+
if buffer_stack == 0
|
180
|
+
@buffer = ""
|
182
181
|
end
|
182
|
+
set_buffer_stack(buffer_stack + 1)
|
183
|
+
end
|
184
|
+
a.execute
|
185
|
+
if quietly
|
186
|
+
set_buffer_stack(buffer_stack - 1)
|
187
|
+
@buffer
|
183
188
|
end
|
184
189
|
end
|
185
190
|
|
@@ -187,13 +192,14 @@ module Gamefic
|
|
187
192
|
# Use #prepare if you want to declare a scene to be started at the
|
188
193
|
# beginning of the next turn.
|
189
194
|
#
|
190
|
-
# @param new_scene [Class]
|
191
|
-
|
195
|
+
# @param new_scene [Class<Scene::Base>]
|
196
|
+
# @param data [Hash] Additional scene data
|
197
|
+
def cue new_scene, **data
|
192
198
|
@next_scene = nil
|
193
199
|
if new_scene.nil?
|
194
200
|
@scene = nil
|
195
201
|
else
|
196
|
-
@scene = new_scene.new(self, **
|
202
|
+
@scene = new_scene.new(self, **data)
|
197
203
|
@scene.start
|
198
204
|
end
|
199
205
|
end
|
@@ -202,10 +208,11 @@ module Gamefic
|
|
202
208
|
# next turn. As opposed to #cue, a prepared scene will not start until the
|
203
209
|
# current scene finishes.
|
204
210
|
#
|
205
|
-
# @param new_scene [Class]
|
206
|
-
|
211
|
+
# @param new_scene [Class<Scene::Base>]
|
212
|
+
# @oaram data [Hash] Additional scene data
|
213
|
+
def prepare new_scene, **data
|
207
214
|
@next_scene = new_scene
|
208
|
-
@next_options =
|
215
|
+
@next_options = data
|
209
216
|
end
|
210
217
|
|
211
218
|
# Return true if the character is expected to be in the specified scene on
|
@@ -219,9 +226,11 @@ module Gamefic
|
|
219
226
|
# Cue a conclusion. This method works like #cue, except it will raise a
|
220
227
|
# NotConclusionError if the scene is not a Scene::Conclusion.
|
221
228
|
#
|
222
|
-
|
223
|
-
|
224
|
-
|
229
|
+
# @param new_scene [Class<Scene::Base>]
|
230
|
+
# @oaram data [Hash] Additional scene data
|
231
|
+
def conclude new_scene, **data
|
232
|
+
raise NotConclusionError unless new_scene <= Scene::Conclusion
|
233
|
+
cue new_scene, **data
|
225
234
|
end
|
226
235
|
|
227
236
|
# True if the character is in a conclusion.
|
@@ -231,14 +240,6 @@ module Gamefic
|
|
231
240
|
!scene.nil? && scene.kind_of?(Scene::Conclusion)
|
232
241
|
end
|
233
242
|
|
234
|
-
# Record the last action the entity executed. This method is typically
|
235
|
-
# called when the entity performs an action in response to user input.
|
236
|
-
#
|
237
|
-
def performed action
|
238
|
-
action.freeze
|
239
|
-
@last_action = action
|
240
|
-
end
|
241
|
-
|
242
243
|
def accessible?
|
243
244
|
false
|
244
245
|
end
|
@@ -266,35 +267,7 @@ module Gamefic
|
|
266
267
|
|
267
268
|
# @return [Array<Gamefic::Scene::Base>]
|
268
269
|
def entered_scenes
|
269
|
-
@entered_scenes ||= []
|
270
|
-
end
|
271
|
-
|
272
|
-
def execute_stack actions, quietly: false
|
273
|
-
return nil if actions.empty?
|
274
|
-
a = actions.first
|
275
|
-
okay = true
|
276
|
-
unless a.meta?
|
277
|
-
playbooks.reverse.each do |playbook|
|
278
|
-
okay = validate_playbook playbook, a
|
279
|
-
break unless okay
|
280
|
-
end
|
281
|
-
end
|
282
|
-
if okay
|
283
|
-
performance_stack.push actions
|
284
|
-
proceed quietly: quietly
|
285
|
-
performance_stack.pop
|
286
|
-
end
|
287
|
-
a
|
288
|
-
end
|
289
|
-
|
290
|
-
def validate_playbook playbook, action
|
291
|
-
okay = true
|
292
|
-
playbook.validators.each { |v|
|
293
|
-
result = v.call(self, action.verb, action.parameters)
|
294
|
-
okay = (result != false)
|
295
|
-
break unless okay
|
296
|
-
}
|
297
|
-
okay
|
270
|
+
@entered_scenes ||= []
|
298
271
|
end
|
299
272
|
|
300
273
|
def buffer_stack
|
@@ -318,8 +291,8 @@ module Gamefic
|
|
318
291
|
@buffer = ''
|
319
292
|
end
|
320
293
|
|
321
|
-
def
|
322
|
-
@
|
294
|
+
def dispatchers
|
295
|
+
@dispatchers ||= []
|
323
296
|
end
|
324
297
|
end
|
325
298
|
end
|
@@ -44,6 +44,9 @@ class Array
|
|
44
44
|
# animals = ['a dog', 'a cat', 'a mouse']
|
45
45
|
# animals.join_and #=> 'a dog, a cat, and a mouse'
|
46
46
|
#
|
47
|
+
# @param sep [String] The separator for all but the last element
|
48
|
+
# @param andSep [String] The separator for the last element
|
49
|
+
# @param serial [Boolean] Use serial separators (e.g., serial commas)
|
47
50
|
# @return [String]
|
48
51
|
def join_and(sep = ', ', andSep = ' and ', serial = true)
|
49
52
|
if self.length < 3
|
@@ -61,7 +64,7 @@ class Array
|
|
61
64
|
join_and(sep, orSep, serial)
|
62
65
|
end
|
63
66
|
|
64
|
-
|
67
|
+
private
|
65
68
|
|
66
69
|
def _keep(arr, cls, bool)
|
67
70
|
if (cls.kind_of?(Class) or cls.kind_of?(Module))
|
@@ -6,15 +6,9 @@ class String
|
|
6
6
|
#
|
7
7
|
# @return [String] The capitalized text
|
8
8
|
def capitalize_first
|
9
|
-
"#{self[0,1].upcase}#{self[1,self.length]}"
|
10
|
-
end
|
11
|
-
|
12
|
-
# An alias for #capitalize_first.
|
13
|
-
#
|
14
|
-
# @return [String]
|
15
|
-
def cap_first
|
16
|
-
self.capitalize_first
|
9
|
+
"#{self[0, 1].upcase}#{self[1, self.length]}"
|
17
10
|
end
|
11
|
+
alias cap_first capitalize_first
|
18
12
|
|
19
13
|
# Get an array of words split by any whitespace.
|
20
14
|
#
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Gamefic
|
2
|
+
# The action selector for character commands.
|
3
|
+
#
|
4
|
+
class Dispatcher
|
5
|
+
# @param actor [Actor]
|
6
|
+
# @param commands [Array<Command>]
|
7
|
+
# @param actions [Array<Action>]
|
8
|
+
def initialize actor, commands = [], actions = []
|
9
|
+
@actor = actor
|
10
|
+
@commands = commands
|
11
|
+
@actions = actions
|
12
|
+
end
|
13
|
+
|
14
|
+
def merge dispatcher
|
15
|
+
commands.concat dispatcher.commands
|
16
|
+
actions.concat dispatcher.actions
|
17
|
+
end
|
18
|
+
|
19
|
+
def next
|
20
|
+
instance = nil
|
21
|
+
while instance.nil? && !@actions.empty?
|
22
|
+
action = actions.shift
|
23
|
+
commands.each do |cmd|
|
24
|
+
instance = action.attempt(actor, cmd)
|
25
|
+
if instance
|
26
|
+
unless instance.meta?
|
27
|
+
actor.playbooks.reverse.each do |playbook|
|
28
|
+
return nil unless validate_playbook(playbook, instance)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
break
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
instance
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param actor [Active]
|
39
|
+
# @param command [String]
|
40
|
+
# @return [Dispatcher]
|
41
|
+
def self.dispatch actor, command
|
42
|
+
group = actor.playbooks.reverse.map { |p| p.dispatch(actor, command) }
|
43
|
+
dispatcher = Dispatcher.new(actor)
|
44
|
+
group.each { |d| dispatcher.merge d }
|
45
|
+
dispatcher
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param actor [Active]
|
49
|
+
# @param verb [Symbol]
|
50
|
+
# @param params [Array<Object>]
|
51
|
+
# @return [Dispatcher]
|
52
|
+
def self.dispatch_from_params actor, verb, params
|
53
|
+
group = actor.playbooks.reverse.map { |p| p.dispatch_from_params(actor, verb, params) }
|
54
|
+
dispatcher = Dispatcher.new(actor)
|
55
|
+
group.each { |d| dispatcher.merge d }
|
56
|
+
dispatcher
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
# @return [Actor]
|
62
|
+
attr_reader :actor
|
63
|
+
|
64
|
+
# @return [Array<Command>]
|
65
|
+
attr_reader :commands
|
66
|
+
|
67
|
+
# @return [Array<Action>]
|
68
|
+
attr_reader :actions
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def validate_playbook playbook, action
|
73
|
+
playbook.validators.all? { |v| v.call(actor, action.verb, action.parameters) != false }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|