gamefic 3.6.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +0 -3
- data/CHANGELOG.md +19 -0
- data/Rakefile +1 -0
- data/gamefic.gemspec +1 -1
- data/lib/gamefic/action.rb +68 -54
- data/lib/gamefic/active/cue.rb +84 -6
- data/lib/gamefic/active/messaging.rb +8 -0
- data/lib/gamefic/active/narratives.rb +101 -0
- data/lib/gamefic/active.rb +80 -92
- data/lib/gamefic/binding.rb +44 -0
- data/lib/gamefic/chapter.rb +30 -46
- data/lib/gamefic/command.rb +22 -40
- data/lib/gamefic/core_ext/array.rb +7 -7
- data/lib/gamefic/core_ext/string.rb +2 -2
- data/lib/gamefic/describable.rb +13 -0
- data/lib/gamefic/dispatcher.rb +35 -55
- data/lib/gamefic/entity.rb +6 -5
- data/lib/gamefic/expression.rb +1 -11
- data/lib/gamefic/logging.rb +3 -10
- data/lib/gamefic/match.rb +23 -0
- data/lib/gamefic/messenger.rb +1 -1
- data/lib/gamefic/narrative.rb +38 -74
- data/lib/gamefic/narrator.rb +77 -0
- data/lib/gamefic/node.rb +40 -8
- data/lib/gamefic/order.rb +53 -0
- data/lib/gamefic/plot.rb +41 -59
- data/lib/gamefic/props/default.rb +5 -17
- data/lib/gamefic/props/multiple_choice.rb +5 -2
- data/lib/gamefic/props/multiple_partial.rb +16 -0
- data/lib/gamefic/props/output.rb +7 -5
- data/lib/gamefic/props/yes_or_no.rb +2 -2
- data/lib/gamefic/props.rb +1 -0
- data/lib/gamefic/proxy/attr.rb +11 -0
- data/lib/gamefic/proxy/base.rb +3 -15
- data/lib/gamefic/proxy/config.rb +2 -2
- data/lib/gamefic/proxy/pick.rb +3 -3
- data/lib/gamefic/proxy/pick_ex.rb +11 -0
- data/lib/gamefic/proxy.rb +3 -71
- data/lib/gamefic/query/ascendants.rb +16 -0
- data/lib/gamefic/query/base.rb +47 -73
- data/lib/gamefic/query/children.rb +15 -0
- data/lib/gamefic/query/descendants.rb +17 -0
- data/lib/gamefic/query/extended.rb +20 -0
- data/lib/gamefic/query/family.rb +27 -0
- data/lib/gamefic/query/global.rb +22 -0
- data/lib/gamefic/query/integer.rb +32 -0
- data/lib/gamefic/query/myself.rb +13 -0
- data/lib/gamefic/query/parent.rb +13 -0
- data/lib/gamefic/query/result.rb +1 -1
- data/lib/gamefic/query/siblings.rb +12 -0
- data/lib/gamefic/query/subqueries.rb +17 -0
- data/lib/gamefic/query/text.rb +8 -9
- data/lib/gamefic/query.rb +11 -3
- data/lib/gamefic/request.rb +60 -0
- data/lib/gamefic/response.rb +46 -72
- data/lib/gamefic/scanner/nesting.rb +6 -6
- data/lib/gamefic/scanner/result.rb +3 -0
- data/lib/gamefic/scanner/strict.rb +14 -4
- data/lib/gamefic/scanner.rb +11 -6
- data/lib/gamefic/scene/active_choice.rb +75 -0
- data/lib/gamefic/scene/activity.rb +7 -3
- data/lib/gamefic/scene/base.rb +123 -0
- data/lib/gamefic/scene/conclusion.rb +4 -1
- data/lib/gamefic/scene/multiple_choice.rb +14 -11
- data/lib/gamefic/scene/pause.rb +5 -1
- data/lib/gamefic/scene/yes_or_no.rb +9 -0
- data/lib/gamefic/scene.rb +2 -1
- data/lib/gamefic/scriptable/hooks.rb +161 -0
- data/lib/gamefic/scriptable/queries.rb +38 -29
- data/lib/gamefic/scriptable/responses.rb +70 -0
- data/lib/gamefic/scriptable/scenes.rb +88 -115
- data/lib/gamefic/scriptable/seeds.rb +69 -0
- data/lib/gamefic/scriptable/syntaxes.rb +29 -0
- data/lib/gamefic/scriptable.rb +14 -199
- data/lib/gamefic/{scriptable → scripting}/entities.rb +22 -22
- data/lib/gamefic/scripting/hooks.rb +45 -0
- data/lib/gamefic/{scriptable → scripting}/proxies.rb +5 -3
- data/lib/gamefic/scripting/responses.rb +21 -0
- data/lib/gamefic/scripting/scenes.rb +57 -0
- data/lib/gamefic/scripting/seeds.rb +10 -0
- data/lib/gamefic/scripting/syntaxes.rb +13 -0
- data/lib/gamefic/scripting.rb +43 -0
- data/lib/gamefic/subplot.rb +11 -22
- data/lib/gamefic/syntax.rb +39 -24
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic.rb +6 -7
- metadata +38 -41
- data/lib/gamefic/active/epic.rb +0 -74
- data/lib/gamefic/active/take.rb +0 -67
- data/lib/gamefic/block.rb +0 -28
- data/lib/gamefic/callback.rb +0 -16
- data/lib/gamefic/proxy/plot_pick.rb +0 -11
- data/lib/gamefic/query/abstract.rb +0 -12
- data/lib/gamefic/query/general.rb +0 -41
- data/lib/gamefic/query/scoped.rb +0 -27
- data/lib/gamefic/rulebook/calls.rb +0 -86
- data/lib/gamefic/rulebook/events.rb +0 -65
- data/lib/gamefic/rulebook/hooks.rb +0 -57
- data/lib/gamefic/rulebook/scenes.rb +0 -68
- data/lib/gamefic/rulebook.rb +0 -125
- data/lib/gamefic/scene/default.rb +0 -88
- data/lib/gamefic/scope/base.rb +0 -44
- data/lib/gamefic/scope/children.rb +0 -16
- data/lib/gamefic/scope/descendants.rb +0 -16
- data/lib/gamefic/scope/family.rb +0 -43
- data/lib/gamefic/scope/myself.rb +0 -13
- data/lib/gamefic/scope/parent.rb +0 -13
- data/lib/gamefic/scope/siblings.rb +0 -14
- data/lib/gamefic/scope.rb +0 -9
- data/lib/gamefic/scriptable/actions.rb +0 -137
- data/lib/gamefic/scriptable/events.rb +0 -71
- data/lib/gamefic/scriptable/plot_proxies.rb +0 -29
- data/lib/gamefic/snapshot.rb +0 -44
- data/lib/gamefic/stage.rb +0 -51
- data/lib/gamefic/syntax/template.rb +0 -67
- data/lib/gamefic/vault.rb +0 -52
data/lib/gamefic/query/text.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Gamefic
|
|
|
7
7
|
class Text < Base
|
|
8
8
|
# @param argument [String, Regexp]
|
|
9
9
|
# @param name [String, nil]
|
|
10
|
-
def initialize
|
|
10
|
+
def initialize(argument = /.*/, name: self.class.name)
|
|
11
11
|
super(argument, name: name)
|
|
12
12
|
validate_argument
|
|
13
13
|
end
|
|
@@ -21,33 +21,32 @@ module Gamefic
|
|
|
21
21
|
argument
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def
|
|
24
|
+
def filter(_subject, token)
|
|
25
25
|
if match? token
|
|
26
26
|
Result.new(token, '')
|
|
27
27
|
else
|
|
28
28
|
Result.new(nil, token)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
|
-
alias filter query
|
|
32
31
|
|
|
33
32
|
def precision
|
|
34
|
-
|
|
33
|
+
-10_000
|
|
35
34
|
end
|
|
36
35
|
|
|
37
|
-
def accept?
|
|
38
|
-
match?
|
|
36
|
+
def accept?(_subject, token)
|
|
37
|
+
match?(token)
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
private
|
|
42
41
|
|
|
43
|
-
def match?
|
|
42
|
+
def match?(token)
|
|
44
43
|
return false unless token.is_a?(String) && !token.empty?
|
|
45
44
|
|
|
46
45
|
case argument
|
|
47
46
|
when Regexp
|
|
48
|
-
token
|
|
47
|
+
token.match?(argument)
|
|
49
48
|
else
|
|
50
|
-
|
|
49
|
+
argument == token
|
|
51
50
|
end
|
|
52
51
|
end
|
|
53
52
|
|
data/lib/gamefic/query.rb
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'gamefic/query/base'
|
|
4
|
-
require 'gamefic/query/general'
|
|
5
|
-
require 'gamefic/query/abstract'
|
|
6
|
-
require 'gamefic/query/scoped'
|
|
7
4
|
require 'gamefic/query/text'
|
|
5
|
+
require 'gamefic/query/integer'
|
|
8
6
|
require 'gamefic/query/result'
|
|
7
|
+
require 'gamefic/query/subqueries'
|
|
8
|
+
require 'gamefic/query/global'
|
|
9
|
+
require 'gamefic/query/children'
|
|
10
|
+
require 'gamefic/query/myself'
|
|
11
|
+
require 'gamefic/query/descendants'
|
|
12
|
+
require 'gamefic/query/ascendants'
|
|
13
|
+
require 'gamefic/query/parent'
|
|
14
|
+
require 'gamefic/query/siblings'
|
|
15
|
+
require 'gamefic/query/extended'
|
|
16
|
+
require 'gamefic/query/family'
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gamefic
|
|
4
|
+
# Build actions from text.
|
|
5
|
+
#
|
|
6
|
+
# Active#perform uses Request to parse user input into actions for execution
|
|
7
|
+
# by the Dispatcher.
|
|
8
|
+
#
|
|
9
|
+
class Request
|
|
10
|
+
# @param actor [Actor]
|
|
11
|
+
# @param input [String]
|
|
12
|
+
def initialize(actor, input)
|
|
13
|
+
@actor = actor
|
|
14
|
+
@input = input
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Array<Action>]
|
|
18
|
+
def to_actions
|
|
19
|
+
Action.sort(
|
|
20
|
+
Syntax.tokenize(input, actor.narratives.syntaxes)
|
|
21
|
+
.flat_map { |expression| expression_to_actions(expression) }
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @return [Actor]
|
|
28
|
+
attr_reader :actor
|
|
29
|
+
|
|
30
|
+
# @return [String]
|
|
31
|
+
attr_reader :input
|
|
32
|
+
|
|
33
|
+
def expression_to_actions(expression)
|
|
34
|
+
Gamefic.logger.info "Evaluating #{expression.inspect}"
|
|
35
|
+
actor.narratives
|
|
36
|
+
.responses_for(expression.verb)
|
|
37
|
+
.map { |response| match_expression response, expression }
|
|
38
|
+
.compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def match_expression(response, expression)
|
|
42
|
+
return nil if expression.tokens.length > response.queries.length
|
|
43
|
+
|
|
44
|
+
remainder = ''
|
|
45
|
+
matches = response.queries
|
|
46
|
+
.zip(expression.tokens)
|
|
47
|
+
.each_with_object([]) do |zipped, results|
|
|
48
|
+
query, token = zipped
|
|
49
|
+
result = query.filter(actor, "#{remainder} #{token}".strip)
|
|
50
|
+
return nil unless result.match
|
|
51
|
+
|
|
52
|
+
results.push Match.new(result.match, token.to_s[0..-result.remainder.length - 1], result.strictness)
|
|
53
|
+
remainder = result.remainder
|
|
54
|
+
end
|
|
55
|
+
return nil unless remainder.empty?
|
|
56
|
+
|
|
57
|
+
Action.new(actor, response, matches, input)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/gamefic/response.rb
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'gamefic/scriptable'
|
|
4
|
+
|
|
3
5
|
module Gamefic
|
|
4
6
|
# A proc to be executed in response to a command that matches its verb and
|
|
5
7
|
# queries.
|
|
6
8
|
#
|
|
7
9
|
class Response
|
|
10
|
+
include Scriptable::Queries
|
|
11
|
+
|
|
8
12
|
# @return [Symbol]
|
|
9
13
|
attr_reader :verb
|
|
10
14
|
|
|
11
15
|
# @return [Array<Query::Base, Query::Text>]
|
|
12
16
|
attr_reader :queries
|
|
13
17
|
|
|
18
|
+
# @return [Proc]
|
|
19
|
+
attr_reader :block
|
|
20
|
+
|
|
14
21
|
# @param verb [Symbol]
|
|
15
|
-
# @param
|
|
16
|
-
# @param args [Array<Object>]
|
|
22
|
+
# @param queries [Array<Object>]
|
|
17
23
|
# @param meta [Boolean]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@verb = verb
|
|
21
|
-
@queries = map_queries(args, narrative)
|
|
24
|
+
def initialize verb, *queries, meta: false, &block
|
|
25
|
+
@verb = verb&.to_sym
|
|
22
26
|
@meta = meta
|
|
23
|
-
@
|
|
27
|
+
@block = block
|
|
28
|
+
@queries = map_queries(queries)
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
# The `meta?` flag is just a way for authors to identify responses that
|
|
@@ -32,115 +37,84 @@ module Gamefic
|
|
|
32
37
|
@meta
|
|
33
38
|
end
|
|
34
39
|
|
|
35
|
-
def hidden?
|
|
36
|
-
@hidden ||= verb.to_s.start_with?('_')
|
|
37
|
-
end
|
|
38
|
-
|
|
39
40
|
def syntax
|
|
40
41
|
@syntax ||= generate_default_syntax
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
# Return an Action if the Response can accept the actor's command.
|
|
44
|
-
#
|
|
45
|
-
# @param actor [Entity]
|
|
46
|
-
# @param command [Command]
|
|
47
|
-
# @return [Action, nil]
|
|
48
|
-
def attempt actor, command
|
|
49
|
-
return nil unless accept?(actor, command)
|
|
50
|
-
|
|
51
|
-
Action.new(actor, command.arguments, self)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
44
|
# True if the Response can be executed for the given actor and command.
|
|
55
45
|
#
|
|
56
46
|
# @param actor [Active]
|
|
57
47
|
# @param command [Command]
|
|
58
|
-
def accept?
|
|
48
|
+
def accept?(actor, command)
|
|
59
49
|
command.verb == verb &&
|
|
60
50
|
command.arguments.length == queries.length &&
|
|
61
51
|
queries.zip(command.arguments).all? { |query, argument| query.accept?(actor, argument) }
|
|
62
52
|
end
|
|
63
53
|
|
|
64
54
|
def execute *args
|
|
65
|
-
|
|
55
|
+
Gamefic.logger.warn "Executing unbound response #{inspect}" unless bound?
|
|
56
|
+
gamefic_binding.call(*args)
|
|
66
57
|
end
|
|
67
58
|
|
|
59
|
+
# The total precision of all the response's queries.
|
|
60
|
+
#
|
|
61
|
+
# @note Precision is decreased if the response has a nil verb.
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer]
|
|
68
64
|
def precision
|
|
69
65
|
@precision ||= calculate_precision
|
|
70
66
|
end
|
|
71
67
|
|
|
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
68
|
def inspect
|
|
95
69
|
"#<#{self.class} #{([verb] + queries).map(&:inspect).join(', ')}>"
|
|
96
70
|
end
|
|
97
71
|
|
|
98
|
-
|
|
72
|
+
def bound?
|
|
73
|
+
!!gamefic_binding.narrative
|
|
74
|
+
end
|
|
99
75
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
nil
|
|
76
|
+
def bind(narrative)
|
|
77
|
+
clone.inject_binding narrative
|
|
103
78
|
end
|
|
104
79
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
80
|
+
protected
|
|
81
|
+
|
|
82
|
+
def inject_binding(narrative)
|
|
83
|
+
@queries = map_queries(narrative.unproxy(@queries))
|
|
84
|
+
@gamefic_binding = Binding.new(narrative, @block)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def gamefic_binding
|
|
91
|
+
@gamefic_binding ||= Binding.new(nil, @block)
|
|
117
92
|
end
|
|
118
93
|
|
|
119
94
|
def generate_default_syntax
|
|
120
95
|
args = queries.length.times.map { |num| num.zero? ? ':var' : ":var#{num + 1}" }
|
|
121
96
|
tmpl = "#{verb} #{args.join(' ')}".strip
|
|
122
|
-
Syntax.new(tmpl
|
|
97
|
+
Syntax.new(tmpl, tmpl)
|
|
123
98
|
end
|
|
124
99
|
|
|
100
|
+
# @return [Integer]
|
|
125
101
|
def calculate_precision
|
|
126
102
|
total = queries.sum(&:precision)
|
|
127
103
|
total -= 1000 unless verb
|
|
128
104
|
total
|
|
129
105
|
end
|
|
130
106
|
|
|
131
|
-
def map_queries
|
|
132
|
-
args.map
|
|
133
|
-
select_query(arg, narrative).tap { |qry| qry.narrative = narrative }
|
|
134
|
-
end
|
|
107
|
+
def map_queries(args)
|
|
108
|
+
args.map { |arg| select_query(arg) }
|
|
135
109
|
end
|
|
136
110
|
|
|
137
|
-
def select_query
|
|
111
|
+
def select_query(arg)
|
|
138
112
|
case arg
|
|
139
|
-
when Entity, Class, Module, Proc, Proxy
|
|
140
|
-
|
|
113
|
+
when Entity, Class, Module, Proc, Proxy::Base
|
|
114
|
+
available(arg)
|
|
141
115
|
when String, Regexp
|
|
142
|
-
|
|
143
|
-
when Query::Base
|
|
116
|
+
plaintext(arg)
|
|
117
|
+
when Query::Base
|
|
144
118
|
arg
|
|
145
119
|
else
|
|
146
120
|
raise ArgumentError, "invalid argument in response: #{arg.inspect}"
|
|
@@ -14,14 +14,14 @@ module Gamefic
|
|
|
14
14
|
def scan
|
|
15
15
|
return unmatched_result unless token =~ NEST_REGEXP
|
|
16
16
|
|
|
17
|
-
denest
|
|
17
|
+
denest
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
-
def denest
|
|
23
|
-
near =
|
|
24
|
-
far =
|
|
22
|
+
def denest
|
|
23
|
+
near = selection
|
|
24
|
+
far = selection
|
|
25
25
|
parts = token.split(NEST_REGEXP)
|
|
26
26
|
until parts.empty?
|
|
27
27
|
current = parts.pop
|
|
@@ -29,8 +29,8 @@ module Gamefic
|
|
|
29
29
|
last_result = subprocessor.scan(far, current) if last_result.matched.empty? && near != far
|
|
30
30
|
return unmatched_result if last_result.matched.empty? || last_result.matched.length > 1
|
|
31
31
|
|
|
32
|
-
near = last_result.matched.first.children &
|
|
33
|
-
far = last_result.matched.first.flatten &
|
|
32
|
+
near = last_result.matched.first.children & selection
|
|
33
|
+
far = last_result.matched.first.flatten & selection
|
|
34
34
|
end
|
|
35
35
|
last_result
|
|
36
36
|
end
|
|
@@ -8,23 +8,33 @@ module Gamefic
|
|
|
8
8
|
# matches one of the entity's keywords.
|
|
9
9
|
#
|
|
10
10
|
class Strict < Base
|
|
11
|
+
NOISE = %w[
|
|
12
|
+
a an the of some
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
11
15
|
# @return [Result]
|
|
12
16
|
def scan
|
|
13
17
|
words = token.keywords
|
|
14
18
|
available = selection.clone
|
|
15
19
|
filtered = []
|
|
16
20
|
words.each_with_index do |word, idx|
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
# @todo This might not be the best way to filter articles, but it works for now
|
|
22
|
+
tested = %w[a an the].include?(word) ? available : match_word(available, word)
|
|
23
|
+
return matched_result(reduce_noise(filtered, words[0, idx]), words[idx..].join(' ')) if tested.empty?
|
|
19
24
|
|
|
20
25
|
filtered = tested
|
|
21
26
|
available = filtered
|
|
22
27
|
end
|
|
23
|
-
matched_result
|
|
28
|
+
matched_result(reduce_noise(filtered, words), '')
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
def match_word available, word
|
|
27
|
-
available.select { |obj| obj.keywords.include?(word) }
|
|
32
|
+
available.select { |obj| (obj.keywords + obj.nuance.keywords).include?(word) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reduce_noise entities, keywords
|
|
36
|
+
noiseless = keywords - NOISE
|
|
37
|
+
entities.reject { |entity| (noiseless - entity.nuance.keywords).empty? }
|
|
28
38
|
end
|
|
29
39
|
end
|
|
30
40
|
end
|
data/lib/gamefic/scanner.rb
CHANGED
|
@@ -17,12 +17,13 @@ module Gamefic
|
|
|
17
17
|
#
|
|
18
18
|
# @param selection [Array<Entity>]
|
|
19
19
|
# @param token [String]
|
|
20
|
+
# @param use [Array<Scanner::Base>]
|
|
20
21
|
# @return [Result]
|
|
21
|
-
def self.scan
|
|
22
|
+
def self.scan(selection, token, use = processors)
|
|
22
23
|
result = nil
|
|
23
|
-
|
|
24
|
+
use.each do |processor|
|
|
24
25
|
result = processor.scan(selection, token)
|
|
25
|
-
break unless result.matched.empty?
|
|
26
|
+
break result unless result.matched.empty?
|
|
26
27
|
end
|
|
27
28
|
result
|
|
28
29
|
end
|
|
@@ -34,8 +35,8 @@ module Gamefic
|
|
|
34
35
|
# Processor classes should be in order from most to least strict rules
|
|
35
36
|
# for matching tokens to entities.
|
|
36
37
|
#
|
|
37
|
-
# @param klasses [Array<Class<Base>>]
|
|
38
|
-
# @return [Array<Class<Base>>]
|
|
38
|
+
# @param klasses [Array<Class<Scanner::Base>>]
|
|
39
|
+
# @return [Array<Class<Scanner::Base>>]
|
|
39
40
|
def self.use *klasses
|
|
40
41
|
processors.replace klasses.flatten
|
|
41
42
|
end
|
|
@@ -45,7 +46,11 @@ module Gamefic
|
|
|
45
46
|
@processors ||= []
|
|
46
47
|
end
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
# A measure of a scan processor's strictness based on its order of use.
|
|
50
|
+
# Higher values indicate higher strictness.
|
|
51
|
+
#
|
|
52
|
+
# @return [Integer]
|
|
53
|
+
def self.strictness(processor)
|
|
49
54
|
(processors.length - (processors.find_index(processor) || processors.length)) * 100
|
|
50
55
|
end
|
|
51
56
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gamefic
|
|
4
|
+
module Scene
|
|
5
|
+
# A scene that presents a list of optional choices. The scene can still
|
|
6
|
+
# attempt to process input that does not match any of the options.
|
|
7
|
+
#
|
|
8
|
+
# Authors can use the `without_selection` class method to select one of
|
|
9
|
+
# three actions to take when the user does not enter one of the options:
|
|
10
|
+
# `:perform`, `:recue`, or `:continue`.
|
|
11
|
+
#
|
|
12
|
+
class ActiveChoice < MultipleChoice
|
|
13
|
+
WITHOUT_SELECTION_ACTIONS = %i[perform recue continue].freeze
|
|
14
|
+
|
|
15
|
+
use_props_class Props::MultipleChoice
|
|
16
|
+
|
|
17
|
+
def finish
|
|
18
|
+
return super if props.selected?
|
|
19
|
+
|
|
20
|
+
send(self.class.without_selection_action)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def without_selection_action
|
|
24
|
+
self.class.without_selection_action
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.type
|
|
28
|
+
'ActiveChoice'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Select the behavior for input that does not match a selectable option.
|
|
32
|
+
# The available settings are `:perform`, `:recue`, and `:continue`.
|
|
33
|
+
#
|
|
34
|
+
# * `:perform` - Skip the `on_finish` blocks and try to perform the input
|
|
35
|
+
# as a command. This is the default behavior.
|
|
36
|
+
# * `:recue` - Restart the scene until the user makes a valid selection.
|
|
37
|
+
# This is the same behavior as a `MultipleChoice` scene.
|
|
38
|
+
# * `:continue` - Execute the `on_finish` blocks regardless of whether the
|
|
39
|
+
# input matches an option.
|
|
40
|
+
#
|
|
41
|
+
# @param action [Symbol]
|
|
42
|
+
def self.without_selection(action)
|
|
43
|
+
WITHOUT_SELECTION_ACTIONS.include?(action) ||
|
|
44
|
+
raise(ArgumentError, "without_selection_action must be one of #{WITHOUT_SELECTION_ACTIONS.map(&:inspect).join_or}")
|
|
45
|
+
|
|
46
|
+
@without_selection_action = action
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Symbol]
|
|
50
|
+
def self.without_selection_action
|
|
51
|
+
@without_selection_action ||= :perform
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.inherited(klass)
|
|
55
|
+
super
|
|
56
|
+
klass.without_selection without_selection_action
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def perform
|
|
62
|
+
actor.perform props.input
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def recue
|
|
66
|
+
actor.tell props.invalid_message
|
|
67
|
+
actor.recue
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def continue
|
|
71
|
+
run_finish_blocks
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -4,10 +4,14 @@ module Gamefic
|
|
|
4
4
|
module Scene
|
|
5
5
|
# A scene that accepts player commands for actors to perform.
|
|
6
6
|
#
|
|
7
|
-
class Activity <
|
|
8
|
-
def finish
|
|
9
|
-
super
|
|
7
|
+
class Activity < Base
|
|
8
|
+
def finish
|
|
10
9
|
actor.perform props.input
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.type
|
|
14
|
+
'Activity'
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gamefic
|
|
4
|
+
module Scene
|
|
5
|
+
# The base class for scenes. Authors can instantiate this class directly
|
|
6
|
+
# and customize it with on_start and on_finish blocks.
|
|
7
|
+
#
|
|
8
|
+
class Base
|
|
9
|
+
# @todo Code smell
|
|
10
|
+
attr_writer :name
|
|
11
|
+
|
|
12
|
+
attr_reader :actor, :narrative, :props, :context
|
|
13
|
+
|
|
14
|
+
# @param actor [Actor]
|
|
15
|
+
# @param narrative [Narrative]
|
|
16
|
+
# @param props [Props::Base]
|
|
17
|
+
def initialize(actor, narrative = nil, props = nil, **context)
|
|
18
|
+
@actor = actor
|
|
19
|
+
@narrative = narrative
|
|
20
|
+
@props = props || self.class.props_class.new
|
|
21
|
+
@context = context
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def name
|
|
25
|
+
@name ||= self.class.nickname
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rename(name)
|
|
29
|
+
@name = name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [String]
|
|
33
|
+
def type
|
|
34
|
+
self.class.type
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Props::Default]
|
|
38
|
+
def start
|
|
39
|
+
run_start_blocks
|
|
40
|
+
props
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [void]
|
|
44
|
+
def finish
|
|
45
|
+
run_finish_blocks
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_hash
|
|
49
|
+
{ name: name, type: type }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.inherited(klass)
|
|
53
|
+
super
|
|
54
|
+
klass.use_props_class props_class
|
|
55
|
+
klass.start_blocks.concat start_blocks
|
|
56
|
+
klass.finish_blocks.concat finish_blocks
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def execute(block)
|
|
62
|
+
Binding.new(narrative, block).call(actor, props, context)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def run_start_blocks
|
|
66
|
+
self.class.start_blocks.each { |blk| execute(blk) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def run_finish_blocks
|
|
70
|
+
self.class.finish_blocks.each { |blk| execute(blk) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
attr_reader :context, :nickname
|
|
75
|
+
|
|
76
|
+
def type
|
|
77
|
+
'Base'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def props_class
|
|
81
|
+
@props_class ||= Props::Default
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def rename(nickname)
|
|
85
|
+
@nickname = nickname
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Array<Proc>]
|
|
89
|
+
def start_blocks
|
|
90
|
+
@start_blocks ||= []
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Array<Proc>]
|
|
94
|
+
def finish_blocks
|
|
95
|
+
@finish_blocks ||= []
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @yieldparam [Actor] The scene's actor
|
|
99
|
+
# @yieldparam [Props::Base] The scene's props
|
|
100
|
+
# @yieldparam [Hash] Additional context
|
|
101
|
+
def on_start(&block)
|
|
102
|
+
start_blocks.push block
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @yieldparam [Actor] The scene's actor
|
|
106
|
+
# @yieldparam [Props::Base] The scene's props
|
|
107
|
+
# @yieldparam [Hash] Additional context
|
|
108
|
+
def on_finish(&block)
|
|
109
|
+
finish_blocks.push block
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
protected
|
|
113
|
+
|
|
114
|
+
attr_writer :context
|
|
115
|
+
|
|
116
|
+
# @param klass [Class<Props::Base>]
|
|
117
|
+
def use_props_class(klass)
|
|
118
|
+
@props_class = klass
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|