gamefic 3.4.0 → 3.6.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 +22 -0
- data/README.md +1 -1
- data/lib/gamefic/action.rb +1 -0
- data/lib/gamefic/active/epic.rb +1 -0
- data/lib/gamefic/active.rb +4 -0
- data/lib/gamefic/chapter.rb +25 -42
- data/lib/gamefic/command.rb +49 -1
- data/lib/gamefic/dispatcher.rb +8 -31
- data/lib/gamefic/entity.rb +26 -0
- data/lib/gamefic/expression.rb +3 -0
- data/lib/gamefic/narrative.rb +9 -5
- data/lib/gamefic/node.rb +3 -5
- data/lib/gamefic/plot.rb +5 -0
- data/lib/gamefic/proxy/base.rb +27 -0
- data/lib/gamefic/proxy/config.rb +16 -0
- data/lib/gamefic/proxy/pick.rb +11 -0
- data/lib/gamefic/proxy/plot_pick.rb +11 -0
- data/lib/gamefic/proxy.rb +79 -0
- data/lib/gamefic/query/abstract.rb +12 -0
- data/lib/gamefic/query/base.rb +62 -16
- data/lib/gamefic/query/general.rb +6 -15
- data/lib/gamefic/query/result.rb +4 -1
- data/lib/gamefic/query/scoped.rb +3 -21
- data/lib/gamefic/query/text.rb +17 -15
- data/lib/gamefic/query.rb +1 -0
- data/lib/gamefic/response.rb +75 -34
- data/lib/gamefic/scanner/base.rb +44 -0
- data/lib/gamefic/scanner/fuzzy.rb +17 -0
- data/lib/gamefic/scanner/fuzzy_nesting.rb +14 -0
- data/lib/gamefic/scanner/nesting.rb +39 -0
- data/lib/gamefic/scanner/result.rb +60 -0
- data/lib/gamefic/scanner/strict.rb +31 -0
- data/lib/gamefic/scanner.rb +33 -111
- data/lib/gamefic/scope/descendants.rb +16 -0
- data/lib/gamefic/scope/family.rb +31 -8
- data/lib/gamefic/scope.rb +1 -0
- data/lib/gamefic/scriptable/actions.rb +4 -23
- data/lib/gamefic/scriptable/entities.rb +32 -17
- data/lib/gamefic/scriptable/plot_proxies.rb +29 -0
- data/lib/gamefic/scriptable/proxies.rb +31 -0
- data/lib/gamefic/scriptable/queries.rb +27 -8
- data/lib/gamefic/scriptable/scenes.rb +7 -1
- data/lib/gamefic/scriptable.rb +78 -16
- data/lib/gamefic/stage.rb +2 -2
- data/lib/gamefic/subplot.rb +33 -1
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic.rb +1 -1
- metadata +17 -4
- data/lib/gamefic/composer.rb +0 -70
- data/lib/gamefic/scriptable/proxy.rb +0 -69
data/lib/gamefic/query/base.rb
CHANGED
@@ -13,25 +13,23 @@ module Gamefic
|
|
13
13
|
# @return [Boolean]
|
14
14
|
attr_reader :ambiguous
|
15
15
|
|
16
|
+
attr_accessor :narrative
|
17
|
+
|
16
18
|
# @raise [ArgumentError] if any of the arguments are nil
|
17
19
|
#
|
18
20
|
# @param arguments [Array<Object>]
|
19
21
|
# @param ambiguous [Boolean]
|
20
|
-
|
22
|
+
# @param name [String]
|
23
|
+
def initialize *arguments, ambiguous: false, name: self.class.to_s
|
21
24
|
raise ArgumentError, "nil argument in query" if arguments.any?(&:nil?)
|
22
25
|
|
23
26
|
@arguments = arguments
|
24
27
|
@ambiguous = ambiguous
|
28
|
+
@name = name
|
25
29
|
end
|
26
30
|
|
27
31
|
# Get a query result for a given subject and token.
|
28
32
|
#
|
29
|
-
# @note This method is retained as a convenience for authors. Narratives
|
30
|
-
# should use Composer to build commands, as it provides more precise
|
31
|
-
# matching of tokens to valid response arguments. Authors can use
|
32
|
-
# #query to find entities that match a token regardless of whether the
|
33
|
-
# result matches an available response.
|
34
|
-
#
|
35
33
|
# @example
|
36
34
|
# respond :reds do |actor|
|
37
35
|
# reds = available(ambiguous: true).query(actor, 'red').match
|
@@ -41,19 +39,44 @@ module Gamefic
|
|
41
39
|
# @param subject [Gamefic::Entity]
|
42
40
|
# @param token [String]
|
43
41
|
# @return [Result]
|
44
|
-
def query(
|
45
|
-
|
42
|
+
def query(subject, token)
|
43
|
+
first_pass = Scanner.scan(span(subject), token)
|
44
|
+
if ambiguous?
|
45
|
+
ambiguous_result(first_pass.filter(*normalized_arguments))
|
46
|
+
elsif first_pass.match.one?
|
47
|
+
unambiguous_result(first_pass.filter(*normalized_arguments))
|
48
|
+
else
|
49
|
+
unambiguous_result(first_pass)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
alias filter query
|
53
|
+
|
54
|
+
# Get an array of entities that match the arguments from the context of
|
55
|
+
# the subject.
|
56
|
+
#
|
57
|
+
# @param subject [Entity]
|
58
|
+
# @return [Array<Entity>]
|
59
|
+
def select subject
|
60
|
+
span(subject).that_are(*normalized_arguments)
|
46
61
|
end
|
47
62
|
|
48
|
-
# Get an array of entities that
|
49
|
-
# subject.
|
63
|
+
# Get an array of entities that are candidates for selection from the
|
64
|
+
# context of the subject. These are the entities that #select will
|
65
|
+
# filter through query's arguments.
|
66
|
+
#
|
67
|
+
# Subclasses should override this method.
|
50
68
|
#
|
51
69
|
# @param subject [Entity]
|
52
70
|
# @return [Array<Entity>]
|
53
|
-
def
|
54
|
-
|
71
|
+
def span _subject
|
72
|
+
[]
|
55
73
|
end
|
56
74
|
|
75
|
+
# True if the object is selectable by the subject.
|
76
|
+
#
|
77
|
+
# @param subject [Entity]
|
78
|
+
# @param object [Array<Entity>, Entity]
|
79
|
+
# @return [Boolean]
|
57
80
|
def accept?(subject, object)
|
58
81
|
available = select(subject)
|
59
82
|
if ambiguous?
|
@@ -72,12 +95,20 @@ module Gamefic
|
|
72
95
|
@ambiguous
|
73
96
|
end
|
74
97
|
|
98
|
+
def name
|
99
|
+
@name || self.class.to_s
|
100
|
+
end
|
101
|
+
|
102
|
+
def inspect
|
103
|
+
"##{ambiguous? ? '*' : ''}#{name}(#{normalized_arguments.map(&:inspect).join(', ')})"
|
104
|
+
end
|
105
|
+
|
75
106
|
private
|
76
107
|
|
77
108
|
def calculate_precision
|
78
|
-
|
109
|
+
normalized_arguments.sum(@ambiguous ? -1000 : 0) do |arg|
|
79
110
|
case arg
|
80
|
-
when Entity,
|
111
|
+
when Entity, Proxy, Proxy::Base
|
81
112
|
1000
|
82
113
|
when Class, Module
|
83
114
|
class_depth(arg) * 100
|
@@ -107,7 +138,22 @@ module Gamefic
|
|
107
138
|
def unambiguous_result scan
|
108
139
|
return Result.new(nil, scan.token) unless scan.matched.one?
|
109
140
|
|
110
|
-
Result.new(scan.matched.first, scan.remainder)
|
141
|
+
Result.new(scan.matched.first, scan.remainder, scan.strictness)
|
142
|
+
end
|
143
|
+
|
144
|
+
def normalized_arguments
|
145
|
+
@normalized_arguments ||= arguments.map do |arg|
|
146
|
+
case arg
|
147
|
+
when Proxy, Proxy::Base
|
148
|
+
arg.fetch(narrative)
|
149
|
+
when String
|
150
|
+
proc do |entity|
|
151
|
+
arg.keywords.all? { |word| entity.keywords.include?(word) }
|
152
|
+
end
|
153
|
+
else
|
154
|
+
arg
|
155
|
+
end
|
156
|
+
end
|
111
157
|
end
|
112
158
|
end
|
113
159
|
end
|
@@ -14,22 +14,13 @@ module Gamefic
|
|
14
14
|
# @param entities [Array, Proc]
|
15
15
|
# @param arguments [Array<Object>]
|
16
16
|
# @param ambiguous [Boolean]
|
17
|
-
def initialize entities, *arguments, ambiguous: false
|
18
|
-
super(*arguments, ambiguous: ambiguous)
|
17
|
+
def initialize entities, *arguments, ambiguous: false, name: nil
|
18
|
+
super(*arguments, ambiguous: ambiguous, name: name)
|
19
19
|
@entities = entities
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
available_entities(subject)
|
24
|
-
end
|
25
|
-
|
26
|
-
def query subject, token
|
27
|
-
filtered = available_entities(subject).that_are(*@arguments)
|
28
|
-
return Result.new(token, nil) if filtered.include?(token)
|
29
|
-
|
30
|
-
scan = Scanner.scan(filtered, token)
|
31
|
-
|
32
|
-
ambiguous? ? ambiguous_result(scan) : unambiguous_result(scan)
|
22
|
+
def span subject
|
23
|
+
available_entities(subject)
|
33
24
|
end
|
34
25
|
|
35
26
|
private
|
@@ -37,9 +28,9 @@ module Gamefic
|
|
37
28
|
def available_entities(subject)
|
38
29
|
if @entities.is_a?(Proc)
|
39
30
|
if @entities.arity.zero?
|
40
|
-
|
31
|
+
Stage.run narrative, &@entities
|
41
32
|
else
|
42
|
-
|
33
|
+
Stage.run narrative, subject, &@entities
|
43
34
|
end
|
44
35
|
else
|
45
36
|
@entities
|
data/lib/gamefic/query/result.rb
CHANGED
@@ -11,9 +11,12 @@ module Gamefic
|
|
11
11
|
# @return [String]
|
12
12
|
attr_reader :remainder
|
13
13
|
|
14
|
-
|
14
|
+
attr_reader :strictness
|
15
|
+
|
16
|
+
def initialize match, remainder, strictness = 0
|
15
17
|
@match = match
|
16
18
|
@remainder = remainder
|
19
|
+
@strictness = strictness
|
17
20
|
end
|
18
21
|
end
|
19
22
|
end
|
data/lib/gamefic/query/scoped.rb
CHANGED
@@ -10,36 +10,18 @@ module Gamefic
|
|
10
10
|
attr_reader :scope
|
11
11
|
|
12
12
|
# @param scope [Class<Gamefic::Scope::Base>]
|
13
|
-
def initialize scope, *arguments, ambiguous: false
|
14
|
-
super(*arguments, ambiguous: ambiguous)
|
13
|
+
def initialize scope, *arguments, ambiguous: false, name: nil
|
14
|
+
super(*arguments, ambiguous: ambiguous, name: name)
|
15
15
|
@scope = scope
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
18
|
+
def span(subject)
|
19
19
|
@scope.matches(subject)
|
20
|
-
.that_are(*@arguments)
|
21
|
-
end
|
22
|
-
|
23
|
-
# @return [Result]
|
24
|
-
def query(subject, token)
|
25
|
-
available = @scope.matches(subject)
|
26
|
-
.that_are(*@arguments)
|
27
|
-
return Result.new(token, nil) if available.include?(token)
|
28
|
-
|
29
|
-
scan = Scanner.scan(available, token)
|
30
|
-
|
31
|
-
return ambiguous_result(scan) if ambiguous?
|
32
|
-
|
33
|
-
unambiguous_result(scan)
|
34
20
|
end
|
35
21
|
|
36
22
|
def precision
|
37
23
|
@precision ||= @scope.precision + calculate_precision
|
38
24
|
end
|
39
|
-
|
40
|
-
def ambiguous?
|
41
|
-
@ambiguous
|
42
|
-
end
|
43
25
|
end
|
44
26
|
end
|
45
27
|
end
|
data/lib/gamefic/query/text.rb
CHANGED
@@ -4,16 +4,21 @@ module Gamefic
|
|
4
4
|
module Query
|
5
5
|
# A special query that handles text instead of entities.
|
6
6
|
#
|
7
|
-
class Text
|
7
|
+
class Text < Base
|
8
8
|
# @param argument [String, Regexp]
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
# @param name [String, nil]
|
10
|
+
def initialize argument = /.*/, name: self.class.name
|
11
|
+
super(argument, name: name)
|
12
|
+
validate_argument
|
13
|
+
end
|
14
|
+
|
15
|
+
def argument
|
16
|
+
arguments.first
|
12
17
|
end
|
13
18
|
|
14
19
|
# @return [String, Regexp]
|
15
20
|
def select(_subject)
|
16
|
-
|
21
|
+
argument
|
17
22
|
end
|
18
23
|
|
19
24
|
def query _subject, token
|
@@ -23,6 +28,7 @@ module Gamefic
|
|
23
28
|
Result.new(nil, token)
|
24
29
|
end
|
25
30
|
end
|
31
|
+
alias filter query
|
26
32
|
|
27
33
|
def precision
|
28
34
|
0
|
@@ -32,25 +38,21 @@ module Gamefic
|
|
32
38
|
match? argument
|
33
39
|
end
|
34
40
|
|
35
|
-
def ambiguous?
|
36
|
-
true
|
37
|
-
end
|
38
|
-
|
39
41
|
private
|
40
42
|
|
41
43
|
def match? token
|
42
|
-
return false unless token.is_a?(String)
|
44
|
+
return false unless token.is_a?(String) && !token.empty?
|
43
45
|
|
44
|
-
case
|
46
|
+
case argument
|
45
47
|
when Regexp
|
46
|
-
token =~
|
48
|
+
token =~ argument
|
47
49
|
else
|
48
|
-
token ==
|
50
|
+
token == argument
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
52
|
-
def
|
53
|
-
return if
|
54
|
+
def validate_argument
|
55
|
+
return if argument.is_a?(String) || argument.is_a?(Regexp)
|
54
56
|
|
55
57
|
raise ArgumentError, 'Invalid text query argument'
|
56
58
|
end
|
data/lib/gamefic/query.rb
CHANGED
data/lib/gamefic/response.rb
CHANGED
@@ -8,19 +8,18 @@ module Gamefic
|
|
8
8
|
# @return [Symbol]
|
9
9
|
attr_reader :verb
|
10
10
|
|
11
|
-
# @return [Array<Query::Base>]
|
11
|
+
# @return [Array<Query::Base, Query::Text>]
|
12
12
|
attr_reader :queries
|
13
13
|
|
14
14
|
# @param verb [Symbol]
|
15
15
|
# @param narrative [Narrative]
|
16
|
-
# @param
|
16
|
+
# @param args [Array<Object>]
|
17
17
|
# @param meta [Boolean]
|
18
18
|
# @param block [Proc]
|
19
|
-
def initialize verb, narrative, *
|
19
|
+
def initialize verb, narrative, *args, meta: false, &block
|
20
20
|
@verb = verb
|
21
|
-
@queries =
|
21
|
+
@queries = map_queries(args, narrative)
|
22
22
|
@meta = meta
|
23
|
-
@block = block
|
24
23
|
@callback = Callback.new(narrative, block)
|
25
24
|
end
|
26
25
|
|
@@ -57,53 +56,95 @@ module Gamefic
|
|
57
56
|
# @param actor [Active]
|
58
57
|
# @param command [Command]
|
59
58
|
def accept? actor, command
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
return false unless query.accept?(actor, command.arguments[idx])
|
64
|
-
end
|
65
|
-
|
66
|
-
true
|
59
|
+
command.verb == verb &&
|
60
|
+
command.arguments.length == queries.length &&
|
61
|
+
queries.zip(command.arguments).all? { |query, argument| query.accept?(actor, argument) }
|
67
62
|
end
|
68
63
|
|
69
64
|
def execute *args
|
70
|
-
@callback.run
|
65
|
+
@callback.run(*args)
|
71
66
|
end
|
72
67
|
|
73
68
|
def precision
|
74
69
|
@precision ||= calculate_precision
|
75
70
|
end
|
76
71
|
|
72
|
+
# Turn an actor and an expression into a command by matching the
|
73
|
+
# expression's tokens to queries. Return nil if the expression
|
74
|
+
# could not be matched.
|
75
|
+
#
|
76
|
+
# @param actor [Actor]
|
77
|
+
# @param expression [Expression]
|
78
|
+
# @return [Command, nil]
|
79
|
+
def to_command actor, expression
|
80
|
+
return log_and_discard unless expression.verb == verb && expression.tokens.length <= queries.length
|
81
|
+
|
82
|
+
results = filter(actor, expression)
|
83
|
+
return log_and_discard unless results
|
84
|
+
|
85
|
+
Gamefic.logger.info "Accepted #{inspect}"
|
86
|
+
Command.new(
|
87
|
+
verb,
|
88
|
+
results.map(&:match),
|
89
|
+
results.sum(&:strictness),
|
90
|
+
precision
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def inspect
|
95
|
+
"#<#{self.class} #{([verb] + queries).map(&:inspect).join(', ')}>"
|
96
|
+
end
|
97
|
+
|
77
98
|
private
|
78
99
|
|
100
|
+
def log_and_discard
|
101
|
+
Gamefic.logger.info "Discarded #{inspect}"
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def filter actor, expression
|
106
|
+
remainder = ''
|
107
|
+
result = queries.zip(expression.tokens)
|
108
|
+
.map do |query, token|
|
109
|
+
token = "#{remainder} #{token}".strip
|
110
|
+
result = query.filter(actor, token)
|
111
|
+
return nil unless result.match
|
112
|
+
|
113
|
+
remainder = result.remainder
|
114
|
+
result
|
115
|
+
end
|
116
|
+
result if remainder.empty?
|
117
|
+
end
|
118
|
+
|
79
119
|
def generate_default_syntax
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
queries.each do |_c|
|
84
|
-
num = 1
|
85
|
-
new_name = ":var"
|
86
|
-
while used_names.include? new_name
|
87
|
-
num += 1
|
88
|
-
new_name = ":var#{num}"
|
89
|
-
end
|
90
|
-
used_names.push new_name
|
91
|
-
user_friendly += " #{new_name}"
|
92
|
-
args.push new_name
|
93
|
-
end
|
94
|
-
Syntax.new(user_friendly.strip, "#{verb} #{args.join(' ')}".strip)
|
120
|
+
args = queries.length.times.map { |num| num.zero? ? ':var' : ":var#{num + 1}" }
|
121
|
+
tmpl = "#{verb} #{args.join(' ')}".strip
|
122
|
+
Syntax.new(tmpl.gsub('_', ' '), tmpl)
|
95
123
|
end
|
96
124
|
|
97
125
|
def calculate_precision
|
98
|
-
total =
|
99
|
-
|
100
|
-
total -= 1000 if verb.nil?
|
126
|
+
total = queries.sum(&:precision)
|
127
|
+
total -= 1000 unless verb
|
101
128
|
total
|
102
129
|
end
|
103
130
|
|
104
|
-
def
|
105
|
-
|
106
|
-
|
131
|
+
def map_queries args, narrative
|
132
|
+
args.map do |arg|
|
133
|
+
select_query(arg, narrative).tap { |qry| qry.narrative = narrative }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def select_query arg, narrative
|
138
|
+
case arg
|
139
|
+
when Entity, Class, Module, Proc, Proxy, Proxy::Base
|
140
|
+
narrative.available(arg)
|
141
|
+
when String, Regexp
|
142
|
+
narrative.plaintext(arg)
|
143
|
+
when Query::Base, Query::Text
|
144
|
+
arg
|
145
|
+
else
|
146
|
+
raise ArgumentError, "invalid argument in response: #{arg.inspect}"
|
147
|
+
end
|
107
148
|
end
|
108
149
|
end
|
109
150
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scanner
|
5
|
+
# A base class for scanners that match tokens to entities.
|
6
|
+
#
|
7
|
+
class Base
|
8
|
+
# @return [Array<Entity>]
|
9
|
+
attr_reader :selection
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :token
|
13
|
+
|
14
|
+
# @param selection [Array<Entity>]
|
15
|
+
# @param token [String]
|
16
|
+
def initialize selection, token
|
17
|
+
@selection = selection
|
18
|
+
@token = token
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Result]
|
22
|
+
def scan
|
23
|
+
unmatched_result
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param selection [Array<Entity>]
|
27
|
+
# @param token [String]
|
28
|
+
# @return [Result]
|
29
|
+
def self.scan selection, token
|
30
|
+
new(selection, token).scan
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def unmatched_result
|
36
|
+
Result.unmatched(selection, token, self.class)
|
37
|
+
end
|
38
|
+
|
39
|
+
def matched_result matched, remainder
|
40
|
+
Result.new(selection, token, matched, remainder, self.class)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scanner
|
5
|
+
# Fuzzy token matching.
|
6
|
+
#
|
7
|
+
# An entity will match a word in a fuzzy scan if it matches the beginning
|
8
|
+
# of one of the entity's keywords, e.g., `pen` is a fuzzy token match for
|
9
|
+
# the keyword `pencil`.
|
10
|
+
#
|
11
|
+
class Fuzzy < Strict
|
12
|
+
def match_word available, word
|
13
|
+
available.select { |obj| obj.keywords.any? { |wrd| wrd.start_with?(word) } }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scanner
|
5
|
+
# Fuzzy scanning for entities inside of other entities, e.g., `soc in draw`
|
6
|
+
# would match `sock in drawer`.
|
7
|
+
#
|
8
|
+
class FuzzyNesting < Nesting
|
9
|
+
def subprocessor
|
10
|
+
Fuzzy
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scanner
|
5
|
+
# Strict scanning for entities inside of other entities, e.g., `sock inside drawer`.
|
6
|
+
#
|
7
|
+
class Nesting < Base
|
8
|
+
NEST_REGEXP = / in | on | of | from | inside | inside of | from inside | off | out | out of /.freeze
|
9
|
+
|
10
|
+
def subprocessor
|
11
|
+
Strict
|
12
|
+
end
|
13
|
+
|
14
|
+
def scan
|
15
|
+
return unmatched_result unless token =~ NEST_REGEXP
|
16
|
+
|
17
|
+
denest selection, token
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def denest objects, token
|
23
|
+
near = objects
|
24
|
+
far = objects
|
25
|
+
parts = token.split(NEST_REGEXP)
|
26
|
+
until parts.empty?
|
27
|
+
current = parts.pop
|
28
|
+
last_result = subprocessor.scan(near, current)
|
29
|
+
last_result = subprocessor.scan(far, current) if last_result.matched.empty? && near != far
|
30
|
+
return unmatched_result if last_result.matched.empty? || last_result.matched.length > 1
|
31
|
+
|
32
|
+
near = last_result.matched.first.children & objects
|
33
|
+
far = last_result.matched.first.flatten & objects
|
34
|
+
end
|
35
|
+
last_result
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scanner
|
5
|
+
# The result of an attempt to scan objects against a token in a Scanner. It
|
6
|
+
# provides an array of matching objects, the text that matched them, and the
|
7
|
+
# text that remains unmatched.
|
8
|
+
#
|
9
|
+
class Result
|
10
|
+
# The scanned objects
|
11
|
+
#
|
12
|
+
# @return [Array<Entity>, String, Regexp]
|
13
|
+
attr_reader :scanned
|
14
|
+
|
15
|
+
# The scanned token
|
16
|
+
#
|
17
|
+
# @return [String]
|
18
|
+
attr_reader :token
|
19
|
+
|
20
|
+
# The matched objects
|
21
|
+
#
|
22
|
+
# @return [Array<Entity>, String]
|
23
|
+
attr_reader :matched
|
24
|
+
alias match matched
|
25
|
+
|
26
|
+
# The remaining (unmatched) portion of the token
|
27
|
+
#
|
28
|
+
# @return [String]
|
29
|
+
attr_reader :remainder
|
30
|
+
|
31
|
+
attr_reader :processor
|
32
|
+
|
33
|
+
def initialize scanned, token, matched, remainder, processor
|
34
|
+
@scanned = scanned
|
35
|
+
@token = token
|
36
|
+
@matched = matched
|
37
|
+
@remainder = remainder
|
38
|
+
@processor = processor
|
39
|
+
end
|
40
|
+
|
41
|
+
def strictness
|
42
|
+
@strictness ||= Scanner.strictness(processor)
|
43
|
+
end
|
44
|
+
|
45
|
+
def filter *args
|
46
|
+
Scanner::Result.new(
|
47
|
+
scanned,
|
48
|
+
token,
|
49
|
+
match.that_are(*args),
|
50
|
+
remainder,
|
51
|
+
processor
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.unmatched scanned, token, processor
|
56
|
+
new(scanned, token, [], token, processor)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scanner
|
5
|
+
# Strict token matching.
|
6
|
+
#
|
7
|
+
# An entity will only match a word in a strict scan if the entire word
|
8
|
+
# matches one of the entity's keywords.
|
9
|
+
#
|
10
|
+
class Strict < Base
|
11
|
+
# @return [Result]
|
12
|
+
def scan
|
13
|
+
words = token.keywords
|
14
|
+
available = selection.clone
|
15
|
+
filtered = []
|
16
|
+
words.each_with_index do |word, idx|
|
17
|
+
tested = match_word(available, word)
|
18
|
+
return matched_result(filtered, words[idx..].join(' ')) if tested.empty?
|
19
|
+
|
20
|
+
filtered = tested
|
21
|
+
available = filtered
|
22
|
+
end
|
23
|
+
matched_result filtered, ''
|
24
|
+
end
|
25
|
+
|
26
|
+
def match_word available, word
|
27
|
+
available.select { |obj| obj.keywords.include?(word) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|