gamefic 3.4.0 → 3.5.0

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