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