gamefic 3.0.0 → 3.1.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/CHANGELOG.md +10 -1
- data/lib/gamefic/action.rb +9 -1
- data/lib/gamefic/active/epic.rb +8 -3
- data/lib/gamefic/active/take.rb +5 -5
- data/lib/gamefic/active.rb +30 -13
- data/lib/gamefic/command.rb +4 -15
- data/lib/gamefic/composer.rb +68 -0
- data/lib/gamefic/dispatcher.rb +47 -40
- data/lib/gamefic/entity.rb +3 -5
- data/lib/gamefic/expression.rb +31 -0
- data/lib/gamefic/plot.rb +1 -1
- data/lib/gamefic/props/default.rb +12 -4
- data/lib/gamefic/props/output.rb +82 -0
- data/lib/gamefic/props.rb +1 -0
- data/lib/gamefic/query/base.rb +24 -0
- data/lib/gamefic/query/general.rb +4 -0
- data/lib/gamefic/query/scoped.rb +5 -0
- data/lib/gamefic/query/text.rb +16 -5
- data/lib/gamefic/response.rb +19 -25
- data/lib/gamefic/rulebook/events.rb +7 -7
- data/lib/gamefic/rulebook.rb +2 -2
- data/lib/gamefic/scanner.rb +52 -25
- data/lib/gamefic/scene/default.rb +3 -3
- data/lib/gamefic/scene/multiple_choice.rb +1 -1
- data/lib/gamefic/scriptable/entities.rb +8 -5
- data/lib/gamefic/scriptable/proxy.rb +11 -0
- data/lib/gamefic/scriptable/queries.rb +2 -2
- data/lib/gamefic/scriptable/scenes.rb +6 -6
- data/lib/gamefic/snapshot.rb +9 -1
- data/lib/gamefic/syntax.rb +4 -4
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic.rb +2 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3900923aaef12a43321ce6f9cc5957953b12bba7d939e0a1666d8c6c75703855
|
4
|
+
data.tar.gz: 6023310c5e26e9632ed4c053a9bea619e1455dbb66966d82fc9e772044d19d3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aea9a57211541f056c730707227020f2c35376d59cc4e69c3c7463e8a8bafa7b6fcd4585a99bf2ce235fdc4c583393f53e060554e06f6e5912358959305627eb
|
7
|
+
data.tar.gz: 5351ea7cc8b3ff4e01e064eb12a594dc2d928641b19e8593243da00b672780acff0d808d73eca1a80cf2e2139fd657b870fa7a5a5315e0a2a3998bc306db0c05
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,13 @@
|
|
1
|
-
## 3.
|
1
|
+
## 3.1.0 - April 8, 2024
|
2
|
+
- Dispatcher prioritizes strict token matches
|
3
|
+
- Scanner builds commands
|
4
|
+
- Tokenize expressions and execute commands
|
5
|
+
- Delete concluded subplots last in Plot#ready
|
6
|
+
- Fix plot conclusion check after subplots conclude
|
7
|
+
- Correct contexts for conclude and output blocks
|
8
|
+
- Reinstate Active#last_input
|
9
|
+
|
10
|
+
## 3.0.0 - January 27, 2024
|
2
11
|
- Instantiate subplots from snapshots
|
3
12
|
- Split Action into Response and Action
|
4
13
|
- Logging
|
data/lib/gamefic/action.rb
CHANGED
@@ -13,8 +13,10 @@ module Gamefic
|
|
13
13
|
# action's performance.
|
14
14
|
#
|
15
15
|
class Hook
|
16
|
+
# @param [Array<Symbol>]
|
16
17
|
attr_reader :verbs
|
17
18
|
|
19
|
+
# @param [Proc]
|
18
20
|
attr_reader :block
|
19
21
|
|
20
22
|
def initialize *verbs, &block
|
@@ -45,11 +47,13 @@ module Gamefic
|
|
45
47
|
@response = response
|
46
48
|
end
|
47
49
|
|
50
|
+
# @return [self]
|
48
51
|
def execute
|
49
|
-
return if cancelled?
|
52
|
+
return self if cancelled? || executed?
|
50
53
|
|
51
54
|
@executed = true
|
52
55
|
response.execute actor, *arguments
|
56
|
+
self
|
53
57
|
end
|
54
58
|
|
55
59
|
# True if the response has been executed. False typically means that the
|
@@ -75,6 +79,10 @@ module Gamefic
|
|
75
79
|
response.verb
|
76
80
|
end
|
77
81
|
|
82
|
+
def narrative
|
83
|
+
response.narrative
|
84
|
+
end
|
85
|
+
|
78
86
|
def meta?
|
79
87
|
response.meta?
|
80
88
|
end
|
data/lib/gamefic/active/epic.rb
CHANGED
@@ -47,9 +47,14 @@ module Gamefic
|
|
47
47
|
narratives.one?
|
48
48
|
end
|
49
49
|
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
def syntaxes
|
51
|
+
rulebooks.flat_map(&:syntaxes)
|
52
|
+
end
|
53
|
+
|
54
|
+
def responses_for(*verbs)
|
55
|
+
rulebooks.to_a
|
56
|
+
.reverse
|
57
|
+
.flat_map { |rb| rb.responses_for(*verbs) }
|
53
58
|
end
|
54
59
|
|
55
60
|
# @param name [Symbol]
|
data/lib/gamefic/active/take.rb
CHANGED
@@ -16,7 +16,7 @@ module Gamefic
|
|
16
16
|
|
17
17
|
# @param actor [Active]
|
18
18
|
# @param cue [Active::Cue]
|
19
|
-
# @param props [Props::Default]
|
19
|
+
# @param props [Props::Default, nil]
|
20
20
|
def initialize actor, cue, props = nil
|
21
21
|
@actor = actor
|
22
22
|
@cue = cue
|
@@ -31,12 +31,12 @@ module Gamefic
|
|
31
31
|
|
32
32
|
# @return [Props::Default]
|
33
33
|
def start
|
34
|
-
|
34
|
+
props.output[:scene] = scene.to_hash
|
35
35
|
scene.run_start_blocks actor, props
|
36
36
|
scene.start actor, props
|
37
37
|
# @todo See if this can be handled better
|
38
|
-
actor.epic.rulebooks.each { |rlbk| rlbk.run_player_output_blocks actor,
|
39
|
-
|
38
|
+
actor.epic.rulebooks.each { |rlbk| rlbk.run_player_output_blocks actor, props.output }
|
39
|
+
props.output.merge!({
|
40
40
|
messages: actor.messages,
|
41
41
|
queue: actor.queue
|
42
42
|
})
|
@@ -47,7 +47,7 @@ module Gamefic
|
|
47
47
|
def finish
|
48
48
|
actor.flush
|
49
49
|
scene.finish(actor, props)
|
50
|
-
|
50
|
+
props.output.replace(last_prompt: props.prompt, last_input: props.input)
|
51
51
|
scene.run_finish_blocks actor, props
|
52
52
|
end
|
53
53
|
|
data/lib/gamefic/active.rb
CHANGED
@@ -21,6 +21,9 @@ module Gamefic
|
|
21
21
|
# @return [Active::Cue, nil]
|
22
22
|
attr_reader :next_cue
|
23
23
|
|
24
|
+
# @return [String, nil]
|
25
|
+
attr_reader :last_input
|
26
|
+
|
24
27
|
# @return [Symbol, nil]
|
25
28
|
def next_scene
|
26
29
|
next_cue&.scene
|
@@ -48,12 +51,22 @@ module Gamefic
|
|
48
51
|
@queue ||= []
|
49
52
|
end
|
50
53
|
|
51
|
-
#
|
52
|
-
#
|
54
|
+
# Data that will be sent to the user. The output is typically sent after a
|
55
|
+
# scene has started and before the user is prompted for input.
|
56
|
+
#
|
57
|
+
# The output object attached to the actor is always frozen. Authors should
|
58
|
+
# use on_player_output blocks to modify output to be sent to the user.
|
53
59
|
#
|
54
|
-
# @return [
|
60
|
+
# @return [Props::Output]
|
55
61
|
def output
|
56
|
-
@output ||=
|
62
|
+
@output ||= Props::Output.new.freeze
|
63
|
+
end
|
64
|
+
|
65
|
+
# The output from the previous turn.
|
66
|
+
#
|
67
|
+
# @return [Props::Output]
|
68
|
+
def last_output
|
69
|
+
@last_output ||= output
|
57
70
|
end
|
58
71
|
|
59
72
|
# Perform a command.
|
@@ -65,11 +78,10 @@ module Gamefic
|
|
65
78
|
# character.perform "take the key"
|
66
79
|
#
|
67
80
|
# @param command [String]
|
68
|
-
# @return [
|
81
|
+
# @return [Action, nil]
|
69
82
|
def perform(command)
|
70
83
|
dispatchers.push Dispatcher.dispatch(self, command)
|
71
|
-
dispatchers.last.execute
|
72
|
-
dispatchers.pop
|
84
|
+
dispatchers.last.execute.tap { dispatchers.pop }
|
73
85
|
end
|
74
86
|
|
75
87
|
# Quietly perform a command.
|
@@ -95,11 +107,10 @@ module Gamefic
|
|
95
107
|
#
|
96
108
|
# @param verb [Symbol]
|
97
109
|
# @param params [Array]
|
98
|
-
# @return [
|
110
|
+
# @return [Action, nil]
|
99
111
|
def execute(verb, *params)
|
100
112
|
dispatchers.push Dispatcher.dispatch_from_params(self, verb, params)
|
101
|
-
dispatchers.last.execute
|
102
|
-
dispatchers.pop
|
113
|
+
dispatchers.last.execute.tap { dispatchers.pop }
|
103
114
|
end
|
104
115
|
|
105
116
|
# Proceed to the next Action in the current stack.
|
@@ -125,9 +136,9 @@ module Gamefic
|
|
125
136
|
# end
|
126
137
|
# end
|
127
138
|
#
|
128
|
-
# @return [
|
139
|
+
# @return [Action, nil]
|
129
140
|
def proceed
|
130
|
-
dispatchers.last&.proceed
|
141
|
+
dispatchers.last&.proceed
|
131
142
|
end
|
132
143
|
|
133
144
|
# Cue a scene to start in the next turn.
|
@@ -146,17 +157,22 @@ module Gamefic
|
|
146
157
|
end
|
147
158
|
alias prepare cue
|
148
159
|
|
160
|
+
# @return [void]
|
149
161
|
def start_take
|
150
162
|
ensure_cue
|
151
163
|
@last_cue = @next_cue
|
152
164
|
cue :default_scene
|
153
165
|
@props = Take.start(self, @last_cue)
|
166
|
+
@last_output = self.output
|
167
|
+
@output = @props.output.dup.freeze
|
154
168
|
end
|
155
169
|
|
170
|
+
# @return [void]
|
156
171
|
def finish_take
|
157
172
|
return unless @last_cue
|
158
173
|
|
159
174
|
Take.finish(self, @last_cue, @props)
|
175
|
+
@last_input = @props.input
|
160
176
|
end
|
161
177
|
|
162
178
|
# Restart the scene from the most recent cue.
|
@@ -175,6 +191,7 @@ module Gamefic
|
|
175
191
|
#
|
176
192
|
# @param new_scene [Symbol]
|
177
193
|
# @oaram context [Hash] Additional scene data
|
194
|
+
# @return [Cue]
|
178
195
|
def conclude scene, **context
|
179
196
|
cue scene, **context
|
180
197
|
available = epic.select_scene(scene)
|
@@ -186,7 +203,7 @@ module Gamefic
|
|
186
203
|
# True if the actor is ready to leave the game.
|
187
204
|
#
|
188
205
|
def concluding?
|
189
|
-
epic.empty? ||
|
206
|
+
epic.empty? || @props&.scene&.type == 'Conclusion'
|
190
207
|
end
|
191
208
|
|
192
209
|
def accessible?
|
data/lib/gamefic/command.rb
CHANGED
@@ -1,31 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Gamefic
|
4
|
-
# A
|
5
|
-
#
|
6
|
-
# Commands are typically derived from tokenization against syntaxes.
|
4
|
+
# A concrete representation of an input as a verb and an array of arguments.
|
7
5
|
#
|
8
6
|
class Command
|
9
7
|
# @return [Symbol]
|
10
8
|
attr_reader :verb
|
11
9
|
|
12
|
-
# @return [Array<String>]
|
10
|
+
# @return [Array<Array<Entity>, Entity, String>]
|
13
11
|
attr_reader :arguments
|
14
12
|
|
13
|
+
# @param verb [Symbol]
|
14
|
+
# @param arguments [Array<Array<Entity>, Entity, String>]
|
15
15
|
def initialize verb, arguments
|
16
16
|
@verb = verb
|
17
17
|
@arguments = arguments
|
18
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
|
30
19
|
end
|
31
20
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Gamefic
|
2
|
+
# A function module for creating commands from expressions.
|
3
|
+
#
|
4
|
+
module Composer
|
5
|
+
# Create a command from the first expression that matches a response.
|
6
|
+
#
|
7
|
+
# @param actor [Actor]
|
8
|
+
# @param expressions [Array<Expression>]
|
9
|
+
# @return [Command]
|
10
|
+
def self.compose actor, expressions
|
11
|
+
%i[strict fuzzy].each do |method|
|
12
|
+
result = match_expressions_to_response actor, expressions, method
|
13
|
+
return result if result
|
14
|
+
end
|
15
|
+
Command.new(nil, [])
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
private
|
20
|
+
|
21
|
+
def match_expressions_to_response actor, expressions, method
|
22
|
+
expressions.each do |expression|
|
23
|
+
result = match_response_arguments actor, expression, method
|
24
|
+
return result if result
|
25
|
+
end
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def match_response_arguments actor, expression, method
|
30
|
+
actor.epic.responses_for(expression.verb).each do |response|
|
31
|
+
next unless response.queries.length >= expression.tokens.length
|
32
|
+
|
33
|
+
result = match_query_arguments(actor, expression, response, method)
|
34
|
+
return result if result
|
35
|
+
end
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def match_query_arguments actor, expression, response, method
|
40
|
+
remainder = response.verb ? '' : expression.verb.to_s
|
41
|
+
arguments = []
|
42
|
+
response.queries.each_with_index do |query, idx|
|
43
|
+
result = Scanner.send(method, query.select(actor), "#{remainder} #{expression.tokens[idx]}".strip)
|
44
|
+
break unless validate_result_from_query(result, query)
|
45
|
+
|
46
|
+
if query.ambiguous?
|
47
|
+
arguments.push result.matched
|
48
|
+
else
|
49
|
+
arguments.push result.matched.first
|
50
|
+
end
|
51
|
+
remainder = result.remainder
|
52
|
+
end
|
53
|
+
|
54
|
+
return nil if arguments.length != response.queries.length || remainder != ''
|
55
|
+
|
56
|
+
Command.new(response.verb, arguments)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param result [Scanner::Result]
|
60
|
+
# @param query [Query::Base]
|
61
|
+
def validate_result_from_query result, query
|
62
|
+
return false if result.matched.empty?
|
63
|
+
|
64
|
+
result.matched.length == 1 || query.ambiguous?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/gamefic/dispatcher.rb
CHANGED
@@ -1,63 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Gamefic
|
4
|
-
# The action
|
4
|
+
# The action executor for character commands.
|
5
5
|
#
|
6
6
|
class Dispatcher
|
7
7
|
# @param actor [Actor]
|
8
|
-
# @param
|
9
|
-
|
10
|
-
def initialize actor, commands = [], responses = []
|
8
|
+
# @param command [Command]
|
9
|
+
def initialize actor, command
|
11
10
|
@actor = actor
|
12
|
-
@
|
13
|
-
@responses = responses
|
11
|
+
@command = command
|
14
12
|
@executed = false
|
13
|
+
@finalized = false
|
15
14
|
end
|
16
15
|
|
17
16
|
# Run the dispatcher.
|
18
17
|
#
|
18
|
+
# @return [Action, nil]
|
19
19
|
def execute
|
20
20
|
return if @executed
|
21
21
|
|
22
|
-
|
22
|
+
@executed = true
|
23
|
+
action = next_action
|
23
24
|
return unless action
|
24
25
|
|
25
|
-
@executed = action.arguments
|
26
26
|
run_before_action_hooks action
|
27
27
|
return if action.cancelled?
|
28
28
|
|
29
29
|
action.execute
|
30
30
|
run_after_action_hooks action
|
31
|
+
action
|
31
32
|
end
|
32
33
|
|
33
|
-
#
|
34
|
+
# Execute the next available action.
|
35
|
+
#
|
36
|
+
# Actors should run #execute first.
|
34
37
|
#
|
35
38
|
# @return [Action, nil]
|
36
39
|
def proceed
|
37
|
-
|
38
|
-
commands.each do |cmd|
|
39
|
-
action = response.attempt(actor, cmd)
|
40
|
-
next unless action && arguments_match?(action.arguments)
|
40
|
+
return unless @executed
|
41
41
|
|
42
|
-
|
43
|
-
end
|
44
|
-
end
|
45
|
-
nil # Without this, return value in Opal is undefined
|
42
|
+
next_action&.execute
|
46
43
|
end
|
47
44
|
|
48
45
|
# @param actor [Active]
|
49
46
|
# @param input [String]
|
50
47
|
# @return [Dispatcher]
|
51
48
|
def self.dispatch actor, input
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
.rulebooks
|
56
|
-
.to_a
|
57
|
-
.reverse
|
58
|
-
.flat_map { |pb| pb.responses_for(*verbs) }
|
59
|
-
.reject(&:hidden?)
|
60
|
-
new(actor, commands, responses)
|
49
|
+
expressions = Syntax.tokenize(input, actor.epic.syntaxes)
|
50
|
+
command = Composer.compose(actor, expressions)
|
51
|
+
new(actor, command)
|
61
52
|
end
|
62
53
|
|
63
54
|
# @param actor [Active]
|
@@ -66,12 +57,7 @@ module Gamefic
|
|
66
57
|
# @return [Dispatcher]
|
67
58
|
def self.dispatch_from_params actor, verb, params
|
68
59
|
command = Command.new(verb, params)
|
69
|
-
|
70
|
-
.rulebooks
|
71
|
-
.to_a
|
72
|
-
.reverse
|
73
|
-
.flat_map { |pb| pb.responses_for(verb) }
|
74
|
-
new(actor, [command], responses)
|
60
|
+
new(actor, command)
|
75
61
|
end
|
76
62
|
|
77
63
|
protected
|
@@ -79,27 +65,48 @@ module Gamefic
|
|
79
65
|
# @return [Actor]
|
80
66
|
attr_reader :actor
|
81
67
|
|
82
|
-
# @return [
|
83
|
-
attr_reader :
|
68
|
+
# @return [Command]
|
69
|
+
attr_reader :command
|
84
70
|
|
85
71
|
# @return [Array<Response>]
|
86
|
-
|
72
|
+
def responses
|
73
|
+
@responses ||= actor.epic.responses_for(command.verb)
|
74
|
+
end
|
87
75
|
|
88
76
|
private
|
89
77
|
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
78
|
+
# @return [Action, nil]
|
79
|
+
def next_action
|
80
|
+
while (response = responses.shift)
|
81
|
+
next if response.queries.length < @command.arguments.length
|
82
|
+
|
83
|
+
return Action.new(actor, @command.arguments, response) if response.accept?(actor, @command)
|
84
|
+
end
|
85
|
+
finalize
|
95
86
|
end
|
96
87
|
|
88
|
+
# @return [void]
|
97
89
|
def run_before_action_hooks action
|
98
90
|
actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_before_actions action }
|
99
91
|
end
|
100
92
|
|
93
|
+
# @return [void]
|
101
94
|
def run_after_action_hooks action
|
102
95
|
actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_after_actions action }
|
103
96
|
end
|
97
|
+
|
98
|
+
# If the dispatcher proceeds through all possible responses, it can fall
|
99
|
+
# back to a nil response as a catchall for commands that could not be
|
100
|
+
# completed.
|
101
|
+
#
|
102
|
+
# @return [Action, nil]
|
103
|
+
def finalize
|
104
|
+
return nil if @finalized
|
105
|
+
|
106
|
+
@finalized = true
|
107
|
+
@command = Command.new(nil, ["#{command.verb} #{command.arguments.join(' ').strip}"])
|
108
|
+
@responses = actor.epic.responses_for(nil)
|
109
|
+
next_action
|
110
|
+
end
|
104
111
|
end
|
105
112
|
end
|
data/lib/gamefic/entity.rb
CHANGED
@@ -9,7 +9,6 @@ module Gamefic
|
|
9
9
|
class Entity
|
10
10
|
include Describable
|
11
11
|
include Node
|
12
|
-
# include Messaging
|
13
12
|
|
14
13
|
def initialize **args
|
15
14
|
klass = self.class
|
@@ -57,14 +56,13 @@ module Gamefic
|
|
57
56
|
end
|
58
57
|
|
59
58
|
class << self
|
60
|
-
# Set or update the default
|
59
|
+
# Set or update the default attributes for new instances.
|
61
60
|
#
|
62
|
-
|
63
|
-
def set_default attrs = {}
|
61
|
+
def set_default **attrs
|
64
62
|
default_attributes.merge! attrs
|
65
63
|
end
|
66
64
|
|
67
|
-
# A hash of default
|
65
|
+
# A hash of default attributes when creating an instance.
|
68
66
|
#
|
69
67
|
# @return [Hash]
|
70
68
|
def default_attributes
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
# A tokenization of an input from available syntaxes.
|
5
|
+
#
|
6
|
+
class Expression
|
7
|
+
# @return [Symbol]
|
8
|
+
attr_reader :verb
|
9
|
+
|
10
|
+
# @return [Array<String>]
|
11
|
+
attr_reader :tokens
|
12
|
+
|
13
|
+
# @param verb [Symbol, nil]
|
14
|
+
# @param tokens [Array<String>]
|
15
|
+
def initialize verb, tokens
|
16
|
+
@verb = verb
|
17
|
+
@tokens = tokens
|
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.tokens.compact.length <=> tokens.compact.length
|
26
|
+
else
|
27
|
+
(other.verb ? 1 : 0) <=> (verb ? 1 : 0)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/gamefic/plot.rb
CHANGED
@@ -9,8 +9,8 @@ module Gamefic
|
|
9
9
|
super
|
10
10
|
subplots.each(&:ready)
|
11
11
|
players.each(&:start_take)
|
12
|
-
subplots.delete_if(&:concluding?)
|
13
12
|
players.select(&:concluding?).each { |plyr| rulebook.run_player_conclude_blocks plyr }
|
13
|
+
subplots.delete_if(&:concluding?)
|
14
14
|
end
|
15
15
|
|
16
16
|
def update
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module Gamefic
|
4
4
|
module Props
|
5
|
+
SceneData = Struct.new(:name, :type)
|
6
|
+
|
5
7
|
# A collection of data related to a scene. Scenes define which Props class
|
6
8
|
# they use. Props can be accessed in a scene's on_start and on_finish
|
7
9
|
# callbacks.
|
@@ -25,17 +27,23 @@ module Gamefic
|
|
25
27
|
attr_reader :context
|
26
28
|
alias data context
|
27
29
|
|
28
|
-
# @
|
30
|
+
# @return [SceneData]
|
31
|
+
attr_reader :scene
|
32
|
+
|
33
|
+
# @param scene [Scene]
|
29
34
|
# @param context [Hash]
|
30
|
-
def initialize
|
31
|
-
@
|
32
|
-
@scene_type = type
|
35
|
+
def initialize scene, **context
|
36
|
+
@scene = SceneData.new(scene.name, scene.type)
|
33
37
|
@context = context
|
34
38
|
end
|
35
39
|
|
36
40
|
def prompt
|
37
41
|
@prompt ||= '>'
|
38
42
|
end
|
43
|
+
|
44
|
+
def output
|
45
|
+
@output ||= Props::Output.new
|
46
|
+
end
|
39
47
|
end
|
40
48
|
end
|
41
49
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Props
|
5
|
+
# A container for output sent to players with a hash interface for custom
|
6
|
+
# data.
|
7
|
+
#
|
8
|
+
class Output
|
9
|
+
# @return [String, nil]
|
10
|
+
attr_reader :last_input
|
11
|
+
|
12
|
+
# @return [String, nil]
|
13
|
+
attr_reader :last_prompt
|
14
|
+
|
15
|
+
def initialize **data
|
16
|
+
@raw_data = {
|
17
|
+
messages: '',
|
18
|
+
options: [],
|
19
|
+
queue: [],
|
20
|
+
scene: {},
|
21
|
+
prompt: ''
|
22
|
+
}
|
23
|
+
merge! data
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String]
|
27
|
+
def messages
|
28
|
+
raw_data[:messages]
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Array<String>]
|
32
|
+
def options
|
33
|
+
raw_data[:options]
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Array<String>]
|
37
|
+
def queue
|
38
|
+
raw_data[:queue]
|
39
|
+
end
|
40
|
+
|
41
|
+
# @todo Should this be a concrete class?
|
42
|
+
# @return [Hash]
|
43
|
+
def scene
|
44
|
+
raw_data[:scene]
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [String]
|
48
|
+
def prompt
|
49
|
+
raw_data[:prompt]
|
50
|
+
end
|
51
|
+
|
52
|
+
def [] key
|
53
|
+
raw_data[key]
|
54
|
+
end
|
55
|
+
|
56
|
+
def []= key, value
|
57
|
+
raw_data[key] = value
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Hash]
|
61
|
+
def to_hash
|
62
|
+
raw_data.dup
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_json _ = nil
|
66
|
+
raw_data.to_json
|
67
|
+
end
|
68
|
+
|
69
|
+
def merge! data
|
70
|
+
data.each { |key, val| self[key] = val }
|
71
|
+
end
|
72
|
+
|
73
|
+
def replace data
|
74
|
+
raw_data.replace data
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
attr_reader :raw_data
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/gamefic/props.rb
CHANGED
data/lib/gamefic/query/base.rb
CHANGED
@@ -24,6 +24,12 @@ module Gamefic
|
|
24
24
|
@ambiguous = ambiguous
|
25
25
|
end
|
26
26
|
|
27
|
+
# @deprecated Queries should only be used to select entities that are
|
28
|
+
# eligible to be response arguments. After a text command is tokenized
|
29
|
+
# into an array of expressions, the composer builds the command that
|
30
|
+
# the dispatcher uses to execute actions. The #accept? method verifies
|
31
|
+
# that the command's arguments match the response's queries.
|
32
|
+
#
|
27
33
|
# @param subject [Gamefic::Entity]
|
28
34
|
# @param token [String]
|
29
35
|
# @return [Result]
|
@@ -31,6 +37,24 @@ module Gamefic
|
|
31
37
|
raise "#query not implemented for #{self.class}"
|
32
38
|
end
|
33
39
|
|
40
|
+
# Get an array of entities that match the query from the context of the
|
41
|
+
# subject.
|
42
|
+
#
|
43
|
+
# @param subject [Entity]
|
44
|
+
# @return [Array<Entity>]
|
45
|
+
def select subject
|
46
|
+
raise "#select not implemented for #{self.class}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def accept?(subject, object)
|
50
|
+
available = select(subject)
|
51
|
+
if ambiguous?
|
52
|
+
object & available == object
|
53
|
+
else
|
54
|
+
available.include?(object)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
34
58
|
# @return [Integer]
|
35
59
|
def precision
|
36
60
|
@precision ||= calculate_precision
|
@@ -19,6 +19,10 @@ module Gamefic
|
|
19
19
|
@entities = entities
|
20
20
|
end
|
21
21
|
|
22
|
+
def select subject
|
23
|
+
available_entities(subject).that_are(*@arguments)
|
24
|
+
end
|
25
|
+
|
22
26
|
def query subject, token
|
23
27
|
filtered = available_entities(subject).that_are(*@arguments)
|
24
28
|
return Result.new(token, nil) if filtered.include?(token)
|
data/lib/gamefic/query/scoped.rb
CHANGED
data/lib/gamefic/query/text.rb
CHANGED
@@ -5,12 +5,17 @@ module Gamefic
|
|
5
5
|
# A special query that handles text instead of entities.
|
6
6
|
#
|
7
7
|
class Text
|
8
|
-
# @param argument [String, Regexp
|
9
|
-
def initialize argument =
|
8
|
+
# @param argument [String, Regexp]
|
9
|
+
def initialize argument = /.*/
|
10
10
|
@argument = argument
|
11
11
|
validate
|
12
12
|
end
|
13
13
|
|
14
|
+
# @return [String, Regexp]
|
15
|
+
def select(_subject)
|
16
|
+
@argument
|
17
|
+
end
|
18
|
+
|
14
19
|
def query _subject, token
|
15
20
|
if match? token
|
16
21
|
Result.new(token, '')
|
@@ -23,11 +28,17 @@ module Gamefic
|
|
23
28
|
0
|
24
29
|
end
|
25
30
|
|
31
|
+
def accept? _subject, argument
|
32
|
+
match? argument
|
33
|
+
end
|
34
|
+
|
35
|
+
def ambiguous?
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
26
39
|
private
|
27
40
|
|
28
41
|
def match? token
|
29
|
-
return true if @argument.nil?
|
30
|
-
|
31
42
|
case @argument
|
32
43
|
when Regexp
|
33
44
|
token =~ @argument
|
@@ -37,7 +48,7 @@ module Gamefic
|
|
37
48
|
end
|
38
49
|
|
39
50
|
def validate
|
40
|
-
return if @argument.
|
51
|
+
return if @argument.is_a?(String) || @argument.is_a?(Regexp)
|
41
52
|
|
42
53
|
raise ArgumentError, 'Invalid text query argument'
|
43
54
|
end
|
data/lib/gamefic/response.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Gamefic
|
4
4
|
# A proc to be executed in response to a command that matches its verb and
|
@@ -11,17 +11,17 @@ module Gamefic
|
|
11
11
|
# @return [Array<Query::Base>]
|
12
12
|
attr_reader :queries
|
13
13
|
|
14
|
-
# @return [
|
15
|
-
|
14
|
+
# @return [Narrative]
|
15
|
+
attr_reader :narrative
|
16
16
|
|
17
17
|
# @param verb [Symbol]
|
18
|
-
# @param
|
18
|
+
# @param narrative [Narrative]
|
19
19
|
# @param queries [Array<Query::Base>]
|
20
20
|
# @param meta [Boolean]
|
21
21
|
# @param block [Proc]
|
22
|
-
def initialize verb,
|
22
|
+
def initialize verb, narrative, *queries, meta: false, &block
|
23
23
|
@verb = verb
|
24
|
-
@
|
24
|
+
@narrative = narrative
|
25
25
|
@queries = map_queryable_objects(queries)
|
26
26
|
@meta = meta
|
27
27
|
@block = block
|
@@ -48,35 +48,29 @@ module Gamefic
|
|
48
48
|
#
|
49
49
|
# @param actor [Entity]
|
50
50
|
# @param command [Command]
|
51
|
-
# @param with_hooks [Boolean]
|
52
51
|
# @return [Action, nil]
|
53
52
|
def attempt actor, command
|
54
|
-
return nil
|
53
|
+
return nil unless accept?(actor, command)
|
55
54
|
|
56
|
-
|
57
|
-
|
58
|
-
remainder = ''
|
59
|
-
|
60
|
-
queries.each do |qd|
|
61
|
-
token = tokens.shift
|
62
|
-
txt = "#{remainder} #{token}".strip
|
63
|
-
return nil if txt.empty?
|
64
|
-
|
65
|
-
response = qd.query(actor, txt)
|
66
|
-
return nil if response.match.nil?
|
55
|
+
Action.new(actor, command.arguments, self)
|
56
|
+
end
|
67
57
|
|
68
|
-
|
58
|
+
# True if the Response can be executed for the given actor and command.
|
59
|
+
#
|
60
|
+
# @param actor [Active]
|
61
|
+
# @param command [Command]
|
62
|
+
def accept? actor, command
|
63
|
+
return false if command.verb != verb || command.arguments.length != queries.length
|
69
64
|
|
70
|
-
|
65
|
+
queries.each_with_index do |query, idx|
|
66
|
+
return false unless query.accept?(actor, command.arguments[idx])
|
71
67
|
end
|
72
68
|
|
73
|
-
|
74
|
-
|
75
|
-
Action.new(actor, result, self)
|
69
|
+
true
|
76
70
|
end
|
77
71
|
|
78
72
|
def execute *args
|
79
|
-
Stage.run(
|
73
|
+
Stage.run(narrative, *args, &@block)
|
80
74
|
end
|
81
75
|
|
82
76
|
def precision
|
@@ -34,16 +34,16 @@ module Gamefic
|
|
34
34
|
self
|
35
35
|
end
|
36
36
|
|
37
|
-
# @return [
|
37
|
+
# @return [void]
|
38
38
|
def on_ready &block
|
39
39
|
@ready_blocks.push block
|
40
40
|
end
|
41
41
|
|
42
42
|
# @yieldparam [Actor]
|
43
|
-
# @return [
|
43
|
+
# @return [void]
|
44
44
|
def on_player_ready &block
|
45
45
|
@ready_blocks.push(proc do
|
46
|
-
players.each { |plyr|
|
46
|
+
players.each { |plyr| instance_exec plyr, &block }
|
47
47
|
end)
|
48
48
|
end
|
49
49
|
|
@@ -53,24 +53,24 @@ module Gamefic
|
|
53
53
|
|
54
54
|
def on_player_update &block
|
55
55
|
@update_blocks.push(proc do
|
56
|
-
players.each { |plyr|
|
56
|
+
players.each { |plyr| instance_exec plyr, &block }
|
57
57
|
end)
|
58
58
|
end
|
59
59
|
|
60
|
-
# @return [
|
60
|
+
# @return [void]
|
61
61
|
def on_conclude &block
|
62
62
|
@conclude_blocks.push block
|
63
63
|
end
|
64
64
|
|
65
65
|
# @yieldparam [Actor]
|
66
|
-
# @return [
|
66
|
+
# @return [void]
|
67
67
|
def on_player_conclude &block
|
68
68
|
@player_conclude_blocks.push block
|
69
69
|
end
|
70
70
|
|
71
71
|
# @yieldparam [Actor]
|
72
72
|
# @yieldparam [Hash]
|
73
|
-
# @return [
|
73
|
+
# @return [void]
|
74
74
|
def on_player_output &block
|
75
75
|
@player_output_blocks.push block
|
76
76
|
end
|
data/lib/gamefic/rulebook.rb
CHANGED
@@ -116,11 +116,11 @@ module Gamefic
|
|
116
116
|
end
|
117
117
|
|
118
118
|
def run_player_conclude_blocks player
|
119
|
-
events.player_conclude_blocks.each { |blk| Stage.run(narrative
|
119
|
+
events.player_conclude_blocks.each { |blk| Stage.run(narrative, player, &blk) }
|
120
120
|
end
|
121
121
|
|
122
122
|
def run_player_output_blocks player, output
|
123
|
-
events.player_output_blocks.each { |blk| Stage.run(narrative
|
123
|
+
events.player_output_blocks.each { |blk| Stage.run(narrative, player, output, &blk) }
|
124
124
|
end
|
125
125
|
|
126
126
|
def empty?
|
data/lib/gamefic/scanner.rb
CHANGED
@@ -11,7 +11,7 @@ module Gamefic
|
|
11
11
|
class Result
|
12
12
|
# The scanned objects
|
13
13
|
#
|
14
|
-
# @return [Array<
|
14
|
+
# @return [Array<Entity>, String, Regexp]
|
15
15
|
attr_reader :scanned
|
16
16
|
|
17
17
|
# The scanned token
|
@@ -21,7 +21,7 @@ module Gamefic
|
|
21
21
|
|
22
22
|
# The matched objects
|
23
23
|
#
|
24
|
-
# @return [Array<
|
24
|
+
# @return [Array<Entity>, String]
|
25
25
|
attr_reader :matched
|
26
26
|
|
27
27
|
# The remaining (unmatched) portion of the token
|
@@ -39,36 +39,53 @@ module Gamefic
|
|
39
39
|
|
40
40
|
# Scan entities against a token.
|
41
41
|
#
|
42
|
-
# @param
|
42
|
+
# @param selection [Array<Entity>, String, Regexp]
|
43
43
|
# @param token [String]
|
44
44
|
# @return [Result]
|
45
|
-
def self.scan
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
end
|
45
|
+
def self.scan selection, token
|
46
|
+
strict_result = strict(selection, token)
|
47
|
+
strict_result.matched.empty? ? fuzzy(selection, token) : strict_result
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param selection [Array<Entity>, String, Regexp]
|
51
|
+
# @param token [String]
|
52
|
+
# @return [Result]
|
53
|
+
def self.strict selection, token
|
54
|
+
return Result.new(selection, token, '', token) unless selection.is_a?(Array)
|
55
|
+
|
56
|
+
scan_strict_or_fuzzy(selection, token, :select_strict)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param selection [Array<Entity>, String, Regexp]
|
60
|
+
# @param token [String]
|
61
|
+
# @return [Result]
|
62
|
+
def self.fuzzy selection, token
|
63
|
+
return scan_text(selection, token) unless selection.is_a?(Array)
|
64
|
+
|
65
|
+
scan_strict_or_fuzzy(selection, token, :select_fuzzy)
|
67
66
|
end
|
68
67
|
|
69
68
|
class << self
|
70
69
|
private
|
71
70
|
|
71
|
+
def scan_strict_or_fuzzy objects, token, method
|
72
|
+
if nested?(token) && objects.all?(&:children)
|
73
|
+
denest(objects, token)
|
74
|
+
else
|
75
|
+
words = token.keywords
|
76
|
+
available = objects.clone
|
77
|
+
filtered = []
|
78
|
+
words.each_with_index do |word, idx|
|
79
|
+
tested = send(method, available, word)
|
80
|
+
return Result.new(objects, token, filtered, words[idx..].join(' ')) if tested.empty?
|
81
|
+
|
82
|
+
filtered = tested
|
83
|
+
available = filtered
|
84
|
+
end
|
85
|
+
Result.new(objects, token, filtered, '')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
72
89
|
def select_strict available, word
|
73
90
|
available.select { |obj| obj.keywords.include?(word) }
|
74
91
|
end
|
@@ -81,6 +98,16 @@ module Gamefic
|
|
81
98
|
token.match(NEST_REGEXP)
|
82
99
|
end
|
83
100
|
|
101
|
+
def scan_text selection, token
|
102
|
+
case selection
|
103
|
+
when Regexp
|
104
|
+
return Result.new(selection, token, token, '') if token =~ selection
|
105
|
+
else
|
106
|
+
return Result.new(selection, token, selection, token[selection.length..]) if token.start_with?(selection)
|
107
|
+
end
|
108
|
+
Result.new(selection, token, '', token)
|
109
|
+
end
|
110
|
+
|
84
111
|
def denest(objects, token)
|
85
112
|
parts = token.split(NEST_REGEXP)
|
86
113
|
current = parts.pop
|
@@ -30,7 +30,7 @@ module Gamefic
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def new_props(**context)
|
33
|
-
self.class.props_class.new(
|
33
|
+
self.class.props_class.new(self, **context)
|
34
34
|
end
|
35
35
|
|
36
36
|
def on_start &block
|
@@ -45,8 +45,8 @@ module Gamefic
|
|
45
45
|
# @param props [Props::Default]
|
46
46
|
# @return [void]
|
47
47
|
def start actor, props
|
48
|
-
|
49
|
-
|
48
|
+
props.output[:scene] = to_hash
|
49
|
+
props.output[:prompt] = props.prompt
|
50
50
|
end
|
51
51
|
|
52
52
|
# @param actor [Gamefic::Actor]
|
@@ -55,7 +55,10 @@ module Gamefic
|
|
55
55
|
# @param description [String]
|
56
56
|
# @return [Gamefic::Entity, nil]
|
57
57
|
def pick description
|
58
|
-
|
58
|
+
result = Scanner.scan(entities, description)
|
59
|
+
return nil unless result.matched.one?
|
60
|
+
|
61
|
+
result.matched.first
|
59
62
|
end
|
60
63
|
|
61
64
|
# Same as #pick, but raise an error if a unique match could not be found.
|
@@ -63,13 +66,13 @@ module Gamefic
|
|
63
66
|
# @param description [String]
|
64
67
|
# @return [Gamefic::Entity, nil]
|
65
68
|
def pick! description
|
66
|
-
|
69
|
+
result = Scanner.scan(entities, description)
|
67
70
|
|
68
|
-
raise "no entity matching '#{description}'" if
|
71
|
+
raise "no entity matching '#{description}'" if result.matched.empty?
|
69
72
|
|
70
|
-
raise "multiple entities matching '#{description}': #{
|
73
|
+
raise "multiple entities matching '#{description}': #{result.matched.join_and}" unless result.matched.one?
|
71
74
|
|
72
|
-
|
75
|
+
result.matched.first
|
73
76
|
end
|
74
77
|
end
|
75
78
|
end
|
@@ -15,6 +15,15 @@ module Gamefic
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def fetch container
|
18
|
+
result = safe_fetch(container)
|
19
|
+
raise ArgumentError, "Unable to fetch entity from proxy agent symbol `#{symbol}`" unless result
|
20
|
+
|
21
|
+
result
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def safe_fetch container
|
18
27
|
if symbol.to_s =~ /^\d+$/
|
19
28
|
Stage.run(container, symbol) { |sym| entities[sym] }
|
20
29
|
elsif symbol.to_s.start_with?('@')
|
@@ -22,6 +31,8 @@ module Gamefic
|
|
22
31
|
else
|
23
32
|
Stage.run(container, symbol) { |sym| send(sym) }
|
24
33
|
end
|
34
|
+
rescue NoMethodError
|
35
|
+
nil
|
25
36
|
end
|
26
37
|
end
|
27
38
|
|
@@ -63,9 +63,9 @@ module Gamefic
|
|
63
63
|
# any text it finds in the command. A successful query returns the
|
64
64
|
# corresponding text instead of an entity.
|
65
65
|
#
|
66
|
-
# @param arg [String,
|
66
|
+
# @param arg [String, Regexp] The string or regular expression to match
|
67
67
|
# @return [Query::Text]
|
68
|
-
def plaintext arg =
|
68
|
+
def plaintext arg = /.*/
|
69
69
|
Query::Text.new arg
|
70
70
|
end
|
71
71
|
end
|
@@ -32,8 +32,8 @@ module Gamefic
|
|
32
32
|
# @param block [Proc]
|
33
33
|
# @yieldparam [Scene]
|
34
34
|
# @return [Symbol]
|
35
|
-
def block name, klass = Scene::Default, on_start: nil, on_finish: nil, &
|
36
|
-
rulebook.scenes.add klass.new(name, rulebook.narrative, on_start: on_start, on_finish: on_finish, &
|
35
|
+
def block name, klass = Scene::Default, on_start: nil, on_finish: nil, &blk
|
36
|
+
rulebook.scenes.add klass.new(name, rulebook.narrative, on_start: on_start, on_finish: on_finish, &blk)
|
37
37
|
name
|
38
38
|
end
|
39
39
|
alias scene block
|
@@ -77,14 +77,14 @@ module Gamefic
|
|
77
77
|
# @yieldparam [Actor]
|
78
78
|
# @yieldparam [Props::MultipleChoice]
|
79
79
|
# @return [Symbol]
|
80
|
-
def multiple_choice name, choices = [], prompt = 'What is your choice?', &
|
80
|
+
def multiple_choice name, choices = [], prompt = 'What is your choice?', &blk
|
81
81
|
block name,
|
82
82
|
Scene::MultipleChoice,
|
83
83
|
on_start: proc { |_actor, props|
|
84
84
|
props.prompt = prompt
|
85
85
|
props.options.concat choices
|
86
86
|
},
|
87
|
-
on_finish:
|
87
|
+
on_finish: blk
|
88
88
|
end
|
89
89
|
|
90
90
|
# Create a yes-or-no scene.
|
@@ -105,13 +105,13 @@ module Gamefic
|
|
105
105
|
# @yieldparam [Actor]
|
106
106
|
# @yieldparam [Props::YesOrNo]
|
107
107
|
# @return [Symbol]
|
108
|
-
def yes_or_no name, prompt = 'Answer:', &
|
108
|
+
def yes_or_no name, prompt = 'Answer:', &blk
|
109
109
|
block name,
|
110
110
|
Scene::YesOrNo,
|
111
111
|
on_start: proc { |_actor, props|
|
112
112
|
props.prompt = prompt
|
113
113
|
},
|
114
|
-
on_finish:
|
114
|
+
on_finish: blk
|
115
115
|
end
|
116
116
|
|
117
117
|
# Create a scene that pauses the game.
|
data/lib/gamefic/snapshot.rb
CHANGED
@@ -27,10 +27,18 @@ module Gamefic
|
|
27
27
|
Marshal.load(binary).tap do |plot|
|
28
28
|
plot.hydrate
|
29
29
|
# @todo Opal marshal dumps are not idempotent
|
30
|
-
next if RUBY_ENGINE == 'opal' ||
|
30
|
+
next if RUBY_ENGINE == 'opal' || match?(plot, snapshot)
|
31
31
|
|
32
32
|
Logging.logger.warn "Scripts modified #{plot.class} data. Snapshot may not have restored properly"
|
33
33
|
end
|
34
34
|
end
|
35
|
+
|
36
|
+
# True if the plot's state matches the snapshot.
|
37
|
+
#
|
38
|
+
# @param plot [Plot]
|
39
|
+
# @param snapshot [String]
|
40
|
+
def self.match?(plot, snapshot)
|
41
|
+
save(plot) == snapshot
|
42
|
+
end
|
35
43
|
end
|
36
44
|
end
|
data/lib/gamefic/syntax.rb
CHANGED
@@ -61,12 +61,12 @@ module Gamefic
|
|
61
61
|
# Convert a String into a Command.
|
62
62
|
#
|
63
63
|
# @param text [String]
|
64
|
-
# @return [
|
64
|
+
# @return [Expression, nil]
|
65
65
|
def tokenize text
|
66
66
|
match = text&.match(template.regexp)
|
67
67
|
return nil unless match
|
68
68
|
|
69
|
-
|
69
|
+
Expression.new(verb, match_to_args(match))
|
70
70
|
end
|
71
71
|
|
72
72
|
# Determine if the specified text matches the syntax's expected pattern.
|
@@ -94,7 +94,7 @@ module Gamefic
|
|
94
94
|
#
|
95
95
|
# @param text [String] The text to tokenize.
|
96
96
|
# @param syntaxes [Array<Syntax>] The syntaxes to use.
|
97
|
-
# @return [Array<
|
97
|
+
# @return [Array<Expression>] The tokenized expressions.
|
98
98
|
def self.tokenize text, syntaxes
|
99
99
|
syntaxes
|
100
100
|
.map { |syn| syn.tokenize(text) }
|
@@ -109,7 +109,7 @@ module Gamefic
|
|
109
109
|
end
|
110
110
|
|
111
111
|
# @param string [String]
|
112
|
-
# @return [
|
112
|
+
# @return [Symbol, nil]
|
113
113
|
def self.literal_or_nil string
|
114
114
|
string.start_with?(':') ? nil : string.to_sym
|
115
115
|
end
|
data/lib/gamefic/version.rb
CHANGED
data/lib/gamefic.rb
CHANGED
@@ -11,6 +11,7 @@ require 'gamefic/rulebook'
|
|
11
11
|
require 'gamefic/query'
|
12
12
|
require 'gamefic/scanner'
|
13
13
|
require 'gamefic/scope'
|
14
|
+
require 'gamefic/expression'
|
14
15
|
require 'gamefic/command'
|
15
16
|
require 'gamefic/action'
|
16
17
|
require 'gamefic/props'
|
@@ -27,6 +28,7 @@ require 'gamefic/node'
|
|
27
28
|
require 'gamefic/describable'
|
28
29
|
require 'gamefic/messenger'
|
29
30
|
require 'gamefic/entity'
|
31
|
+
require 'gamefic/composer'
|
30
32
|
require 'gamefic/dispatcher'
|
31
33
|
require 'gamefic/active'
|
32
34
|
require 'gamefic/active/cue'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gamefic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fred Snyder
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-04-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: opal
|
@@ -136,11 +136,13 @@ files:
|
|
136
136
|
- lib/gamefic/actor.rb
|
137
137
|
- lib/gamefic/block.rb
|
138
138
|
- lib/gamefic/command.rb
|
139
|
+
- lib/gamefic/composer.rb
|
139
140
|
- lib/gamefic/core_ext/array.rb
|
140
141
|
- lib/gamefic/core_ext/string.rb
|
141
142
|
- lib/gamefic/describable.rb
|
142
143
|
- lib/gamefic/dispatcher.rb
|
143
144
|
- lib/gamefic/entity.rb
|
145
|
+
- lib/gamefic/expression.rb
|
144
146
|
- lib/gamefic/logging.rb
|
145
147
|
- lib/gamefic/messenger.rb
|
146
148
|
- lib/gamefic/narrative.rb
|
@@ -149,6 +151,7 @@ files:
|
|
149
151
|
- lib/gamefic/props.rb
|
150
152
|
- lib/gamefic/props/default.rb
|
151
153
|
- lib/gamefic/props/multiple_choice.rb
|
154
|
+
- lib/gamefic/props/output.rb
|
152
155
|
- lib/gamefic/props/pause.rb
|
153
156
|
- lib/gamefic/props/yes_or_no.rb
|
154
157
|
- lib/gamefic/query.rb
|