gamefic 2.0.2 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|