gamefic 3.4.0 → 3.5.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +1 -1
  4. data/lib/gamefic/active/epic.rb +1 -0
  5. data/lib/gamefic/active.rb +4 -0
  6. data/lib/gamefic/chapter.rb +25 -42
  7. data/lib/gamefic/command.rb +40 -1
  8. data/lib/gamefic/dispatcher.rb +5 -31
  9. data/lib/gamefic/entity.rb +26 -0
  10. data/lib/gamefic/narrative.rb +9 -5
  11. data/lib/gamefic/plot.rb +5 -0
  12. data/lib/gamefic/proxy.rb +46 -0
  13. data/lib/gamefic/query/base.rb +28 -12
  14. data/lib/gamefic/query/general.rb +3 -12
  15. data/lib/gamefic/query/result.rb +4 -1
  16. data/lib/gamefic/query/scoped.rb +1 -18
  17. data/lib/gamefic/query/text.rb +13 -12
  18. data/lib/gamefic/response.rb +65 -34
  19. data/lib/gamefic/scanner/base.rb +44 -0
  20. data/lib/gamefic/scanner/fuzzy.rb +17 -0
  21. data/lib/gamefic/scanner/fuzzy_nesting.rb +14 -0
  22. data/lib/gamefic/scanner/nesting.rb +39 -0
  23. data/lib/gamefic/scanner/result.rb +50 -0
  24. data/lib/gamefic/scanner/strict.rb +31 -0
  25. data/lib/gamefic/scanner.rb +33 -111
  26. data/lib/gamefic/scope/descendants.rb +16 -0
  27. data/lib/gamefic/scope/family.rb +31 -8
  28. data/lib/gamefic/scope.rb +1 -0
  29. data/lib/gamefic/scriptable/actions.rb +4 -23
  30. data/lib/gamefic/scriptable/entities.rb +4 -1
  31. data/lib/gamefic/scriptable/plot_proxies.rb +16 -0
  32. data/lib/gamefic/scriptable/proxies.rb +31 -0
  33. data/lib/gamefic/scriptable/queries.rb +15 -7
  34. data/lib/gamefic/scriptable/scenes.rb +7 -1
  35. data/lib/gamefic/scriptable.rb +67 -15
  36. data/lib/gamefic/stage.rb +2 -2
  37. data/lib/gamefic/subplot.rb +27 -1
  38. data/lib/gamefic/version.rb +1 -1
  39. data/lib/gamefic.rb +1 -1
  40. metadata +12 -4
  41. data/lib/gamefic/composer.rb +0 -70
  42. data/lib/gamefic/scriptable/proxy.rb +0 -69
@@ -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 queries [Array<Query::Base>]
16
+ # @param args [Array<Object>]
17
17
  # @param meta [Boolean]
18
18
  # @param block [Proc]
19
- def initialize verb, narrative, *queries, meta: false, &block
19
+ def initialize verb, narrative, *args, meta: false, &block
20
20
  @verb = verb
21
- @queries = map_queryable_objects(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,85 @@ module Gamefic
57
56
  # @param actor [Active]
58
57
  # @param command [Command]
59
58
  def accept? actor, command
60
- return false if command.verb != verb || command.arguments.length != queries.length
61
-
62
- queries.each_with_index do |query, idx|
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 *args
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 nil unless expression.verb == verb && expression.tokens.length <= queries.length
81
+
82
+ results = filter(actor, expression)
83
+ return nil unless results
84
+
85
+ Command.new(
86
+ verb,
87
+ results.map(&:match),
88
+ results.sum(&:strictness),
89
+ precision
90
+ )
91
+ end
92
+
77
93
  private
78
94
 
95
+ def filter actor, expression
96
+ remainder = ''
97
+ result = queries.zip(expression.tokens)
98
+ .map do |query, token|
99
+ token = "#{remainder} #{token}".strip
100
+ result = query.filter(actor, token)
101
+ return nil unless result.match
102
+
103
+ remainder = result.remainder
104
+ result
105
+ end
106
+ result if remainder.empty?
107
+ end
108
+
79
109
  def generate_default_syntax
80
- user_friendly = verb.to_s.gsub(/_/, ' ')
81
- args = []
82
- used_names = []
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)
110
+ args = queries.length.times.map { |num| num.zero? ? ':var' : ":var#{num + 1}" }
111
+ tmpl = "#{verb} #{args.join(' ')}".strip
112
+ Syntax.new(tmpl.gsub('_', ' '), tmpl)
95
113
  end
96
114
 
97
115
  def calculate_precision
98
- total = 0
99
- queries.each { |q| total += q.precision }
100
- total -= 1000 if verb.nil?
116
+ total = queries.sum(&:precision)
117
+ total -= 1000 unless verb
101
118
  total
102
119
  end
103
120
 
104
- def map_queryable_objects queries
105
- # @todo Considering moving mapping from Actions to here
106
- queries
121
+ def map_queries args, narrative
122
+ args.map do |arg|
123
+ select_query(arg, narrative).tap { |qry| qry.narrative = narrative }
124
+ end
125
+ end
126
+
127
+ def select_query arg, narrative
128
+ case arg
129
+ when Entity, Class, Module, Proc, Proxy
130
+ narrative.available(arg)
131
+ when String, Regexp
132
+ narrative.plaintext(arg)
133
+ when Query::Base, Query::Text
134
+ arg
135
+ else
136
+ raise ArgumentError, "invalid argument in response: #{arg.inspect}"
137
+ end
107
138
  end
108
139
  end
109
140
  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 Result.unmatched(selection, token, self.class) 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 Result.unmatched(selection, token, self.class) 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,50 @@
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 self.unmatched scanned, token, processor
46
+ new(scanned, token, [], token, processor)
47
+ end
48
+ end
49
+ end
50
+ 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
@@ -1,132 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'gamefic/scanner/result'
4
+ require 'gamefic/scanner/base'
5
+ require 'gamefic/scanner/strict'
6
+ require 'gamefic/scanner/fuzzy'
7
+ require 'gamefic/scanner/nesting'
8
+ require 'gamefic/scanner/fuzzy_nesting'
9
+
3
10
  module Gamefic
4
11
  # A module for matching objects to tokens.
5
12
  #
6
13
  module Scanner
7
- NEST_REGEXP = / in | on | of | from | inside | from inside /
8
-
9
- # The result of an attempt to scan objects against a token in a Scanner. It
10
- # provides an array of matching objects, the text that matched them, and the
11
- # text that remains unmatched.
12
- #
13
- class Result
14
- # The scanned objects
15
- #
16
- # @return [Array<Entity>, String, Regexp]
17
- attr_reader :scanned
18
-
19
- # The scanned token
20
- #
21
- # @return [String]
22
- attr_reader :token
23
-
24
- # The matched objects
25
- #
26
- # @return [Array<Entity>, String]
27
- attr_reader :matched
28
-
29
- # The remaining (unmatched) portion of the token
30
- #
31
- # @return [String]
32
- attr_reader :remainder
33
-
34
- def initialize scanned, token, matched, remainder
35
- @scanned = scanned
36
- @token = token
37
- @matched = matched
38
- @remainder = remainder
39
- end
40
- end
14
+ DEFAULT_PROCESSORS = [Nesting, Strict, FuzzyNesting, Fuzzy].freeze
41
15
 
42
16
  # Scan entities against a token.
43
17
  #
44
- # @param selection [Array<Entity>, String, Regexp]
18
+ # @param selection [Array<Entity>]
45
19
  # @param token [String]
46
20
  # @return [Result]
47
21
  def self.scan selection, token
48
- strict_result = strict(selection, token)
49
- strict_result.matched.empty? ? fuzzy(selection, token) : strict_result
22
+ result = nil
23
+ processors.each do |processor|
24
+ result = processor.scan(selection, token)
25
+ break unless result.matched.empty?
26
+ end
27
+ result
50
28
  end
51
29
 
52
- # @param selection [Array<Entity>, String, Regexp]
53
- # @param token [String]
54
- # @return [Result]
55
- def self.strict selection, token
56
- return Result.new(selection, token, '', token) unless selection.is_a?(Array)
57
-
58
- scan_strict_or_fuzzy(selection, token, :select_strict)
30
+ # Select the scanner processors to use in entity queries. Each processor
31
+ # will be used in order until one of them returns matches. The default
32
+ # processor list is `DEFAULT_PROCESSORS`.
33
+ #
34
+ # Processor classes should be in order from most to least strict rules
35
+ # for matching tokens to entities.
36
+ #
37
+ # @param klasses [Array<Class<Base>>]
38
+ # @return [Array<Class<Base>>]
39
+ def self.use *klasses
40
+ processors.replace klasses.flatten
59
41
  end
60
42
 
61
- # @param selection [Array<Entity>, String, Regexp]
62
- # @param token [String]
63
- # @return [Result]
64
- def self.fuzzy selection, token
65
- return scan_text(selection, token) unless selection.is_a?(Array)
66
-
67
- scan_strict_or_fuzzy(selection, token, :select_fuzzy)
43
+ # @return [Array<Class<Base>>]
44
+ def self.processors
45
+ @processors ||= []
68
46
  end
69
47
 
70
- class << self
71
- private
72
-
73
- def scan_strict_or_fuzzy objects, token, method
74
- if nested?(token) && objects.all?(&:children)
75
- denest(objects, token)
76
- else
77
- words = token.keywords
78
- available = objects.clone
79
- filtered = []
80
- words.each_with_index do |word, idx|
81
- tested = send(method, available, word)
82
- return Result.new(objects, token, filtered, words[idx..].join(' ')) if tested.empty?
83
-
84
- filtered = tested
85
- available = filtered
86
- end
87
- Result.new(objects, token, filtered, '')
88
- end
89
- end
90
-
91
- def select_strict available, word
92
- available.select { |obj| obj.keywords.include?(word) }
93
- end
94
-
95
- def select_fuzzy available, word
96
- available.select { |obj| obj.keywords.any? { |wrd| wrd.start_with?(word) } }
97
- end
98
-
99
- def nested?(token)
100
- token.match(NEST_REGEXP)
101
- end
102
-
103
- def scan_text selection, token
104
- case selection
105
- when Regexp
106
- return Result.new(selection, token, token, '') if token =~ selection
107
- else
108
- return Result.new(selection, token, selection, token[selection.length..]) if token.start_with?(selection)
109
- end
110
- Result.new(selection, token, '', token)
111
- end
112
-
113
- def denest(objects, token)
114
- parts = token.split(NEST_REGEXP)
115
- current = parts.pop
116
- last_result = scan(objects, current)
117
- until parts.empty?
118
- current = "#{parts.last} #{current}"
119
- result = scan(last_result.matched, current)
120
- break if result.matched.empty?
121
-
122
- parts.pop
123
- last_result = result
124
- end
125
- return Result.new(objects, token, [], '') if last_result.matched.empty? || last_result.matched.length > 1
126
- return last_result if parts.empty?
127
-
128
- denest(last_result.matched.first.children, parts.join(' '))
129
- end
48
+ def self.strictness processor
49
+ (processors.length - (processors.find_index(processor) || processors.length)) * 100
130
50
  end
51
+
52
+ use DEFAULT_PROCESSORS
131
53
  end
132
54
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scope
5
+ # The Descendants scope returns an entity's children and accessible
6
+ # descendants.
7
+ #
8
+ class Descendants < Base
9
+ def matches
10
+ context.children.flat_map do |child|
11
+ [child] + subquery_accessible(child)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -2,18 +2,41 @@
2
2
 
3
3
  module Gamefic
4
4
  module Scope
5
- # The Family scope returns an entity's parent, siblings, and descendants.
5
+ # The Family scope returns an entity's ascendants, descendants, siblings,
6
+ # and siblings' descendants.
6
7
  #
7
8
  class Family < Base
8
9
  def matches
9
- result = context.parent ? [context.parent] : []
10
- result.concat subquery_accessible(context.parent)
11
- result.delete context
12
- context.children.each do |c|
13
- result.push c
14
- result.concat subquery_accessible(c)
10
+ match_ascendants + match_descendants + match_siblings
11
+ end
12
+
13
+ private
14
+
15
+ def match_ascendants
16
+ [].tap do |result|
17
+ here = context.parent
18
+ while here
19
+ result.push here
20
+ here = here.parent
21
+ end
15
22
  end
16
- result.uniq
23
+ end
24
+
25
+ def match_descendants
26
+ context.children.flat_map do |child|
27
+ [child] + subquery_accessible(child)
28
+ end
29
+ end
30
+
31
+ def match_siblings
32
+ return [] unless context.parent
33
+
34
+ context.parent
35
+ .children
36
+ .that_are_not(context)
37
+ .flat_map do |child|
38
+ [child] + subquery_accessible(child)
39
+ end
17
40
  end
18
41
  end
19
42
  end
data/lib/gamefic/scope.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'gamefic/scope/base'
4
4
  require 'gamefic/scope/children'
5
+ require 'gamefic/scope/descendants'
5
6
  require 'gamefic/scope/family'
6
7
  require 'gamefic/scope/parent'
7
8
  require 'gamefic/scope/siblings'
@@ -26,11 +26,10 @@ module Gamefic
26
26
  # end
27
27
  #
28
28
  # @param verb [Symbol] An imperative verb for the command
29
- # @param queries [Array<Query::Base, Query::Text>] Filters for the command's tokens
29
+ # @param args [Array<Object>] Filters for the command's tokens
30
30
  # @yieldparam [Gamefic::Actor]
31
31
  # @return [Symbol]
32
- def respond(verb, *queries, &proc)
33
- args = map_response_args(queries)
32
+ def respond(verb, *args, &proc)
34
33
  rulebook.calls.add_response Response.new(verb, self, *args, &proc)
35
34
  verb
36
35
  end
@@ -47,11 +46,10 @@ module Gamefic
47
46
  # end
48
47
  #
49
48
  # @param verb [Symbol] An imperative verb for the command
50
- # @param queries [Array<Query::Base, Query::Text>] Filters for the command's tokens
49
+ # @param args [Array<Object>] Filters for the command's tokens
51
50
  # @yieldparam [Gamefic::Actor]
52
51
  # @return [Symbol]
53
- def meta(verb, *queries, &proc)
54
- args = map_response_args(queries)
52
+ def meta(verb, *args, &proc)
55
53
  rulebook.calls.add_response Response.new(verb, self, *args, meta: true, &proc)
56
54
  verb
57
55
  end
@@ -134,23 +132,6 @@ module Gamefic
134
132
  def syntaxes
135
133
  rulebook.syntaxes
136
134
  end
137
-
138
- private
139
-
140
- def map_response_args args
141
- args.map do |arg|
142
- case arg
143
- when Entity, Class, Module, Proc, Proxy::Agent
144
- available(arg)
145
- when String, Regexp
146
- plaintext(arg)
147
- when Gamefic::Query::Base, Gamefic::Query::Text
148
- arg
149
- else
150
- raise ArgumentError, "invalid argument in response: #{arg.inspect}"
151
- end
152
- end
153
- end
154
135
  end
155
136
  end
156
137
  end
@@ -9,7 +9,7 @@ module Gamefic
9
9
  # #make and #destroy instead.
10
10
  #
11
11
  module Entities
12
- include Proxy
12
+ include Proxies
13
13
 
14
14
  def entity_vault
15
15
  @entity_vault ||= Vault.new
@@ -63,6 +63,9 @@ module Gamefic
63
63
 
64
64
  # Same as #pick, but raise an error if a unique match could not be found.
65
65
  #
66
+ #
67
+ # @raise [RuntimeError] if a unique match was not found.
68
+ #
66
69
  # @param description [String]
67
70
  # @return [Gamefic::Entity, nil]
68
71
  def pick! description
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scriptable
5
+ module PlotProxies
6
+ def lazy_plot key
7
+ Proxy.new(:attr, [:plot, key])
8
+ end
9
+ alias _plot lazy_plot
10
+
11
+ def attr_plot attr
12
+ define_method(attr) { plot.send(attr) }
13
+ end
14
+ end
15
+ end
16
+ end