gamefic 3.3.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +1 -1
  4. data/lib/gamefic/action.rb +4 -4
  5. data/lib/gamefic/active/epic.rb +1 -0
  6. data/lib/gamefic/active.rb +4 -0
  7. data/lib/gamefic/callback.rb +16 -0
  8. data/lib/gamefic/chapter.rb +71 -0
  9. data/lib/gamefic/command.rb +40 -1
  10. data/lib/gamefic/dispatcher.rb +5 -31
  11. data/lib/gamefic/entity.rb +26 -0
  12. data/lib/gamefic/narrative.rb +30 -8
  13. data/lib/gamefic/plot.rb +28 -1
  14. data/lib/gamefic/proxy.rb +46 -0
  15. data/lib/gamefic/query/base.rb +28 -12
  16. data/lib/gamefic/query/general.rb +3 -12
  17. data/lib/gamefic/query/result.rb +4 -1
  18. data/lib/gamefic/query/scoped.rb +1 -18
  19. data/lib/gamefic/query/text.rb +13 -12
  20. data/lib/gamefic/response.rb +66 -38
  21. data/lib/gamefic/rulebook/calls.rb +0 -4
  22. data/lib/gamefic/rulebook/events.rb +10 -24
  23. data/lib/gamefic/rulebook/hooks.rb +10 -10
  24. data/lib/gamefic/rulebook.rb +8 -22
  25. data/lib/gamefic/scanner/base.rb +44 -0
  26. data/lib/gamefic/scanner/fuzzy.rb +17 -0
  27. data/lib/gamefic/scanner/fuzzy_nesting.rb +14 -0
  28. data/lib/gamefic/scanner/nesting.rb +39 -0
  29. data/lib/gamefic/scanner/result.rb +50 -0
  30. data/lib/gamefic/scanner/strict.rb +31 -0
  31. data/lib/gamefic/scanner.rb +33 -111
  32. data/lib/gamefic/scope/descendants.rb +16 -0
  33. data/lib/gamefic/scope/family.rb +31 -8
  34. data/lib/gamefic/scope.rb +1 -0
  35. data/lib/gamefic/scriptable/actions.rb +8 -27
  36. data/lib/gamefic/scriptable/entities.rb +4 -1
  37. data/lib/gamefic/scriptable/events.rb +13 -7
  38. data/lib/gamefic/scriptable/plot_proxies.rb +16 -0
  39. data/lib/gamefic/scriptable/proxies.rb +31 -0
  40. data/lib/gamefic/scriptable/queries.rb +15 -7
  41. data/lib/gamefic/scriptable/scenes.rb +10 -4
  42. data/lib/gamefic/scriptable.rb +73 -42
  43. data/lib/gamefic/stage.rb +2 -2
  44. data/lib/gamefic/subplot.rb +31 -7
  45. data/lib/gamefic/version.rb +1 -1
  46. data/lib/gamefic.rb +3 -1
  47. metadata +14 -4
  48. data/lib/gamefic/composer.rb +0 -70
  49. data/lib/gamefic/scriptable/proxy.rb +0 -69
@@ -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,12 +26,11 @@ 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)
34
- rulebook.calls.add_response Response.new(verb, rulebook.narrative, *args, &proc)
32
+ def respond(verb, *args, &proc)
33
+ rulebook.calls.add_response Response.new(verb, self, *args, &proc)
35
34
  verb
36
35
  end
37
36
 
@@ -47,12 +46,11 @@ 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)
55
- rulebook.calls.add_response Response.new(verb, rulebook.narrative, *args, meta: true, &proc)
52
+ def meta(verb, *args, &proc)
53
+ rulebook.calls.add_response Response.new(verb, self, *args, meta: true, &proc)
56
54
  verb
57
55
  end
58
56
 
@@ -64,7 +62,7 @@ module Gamefic
64
62
  # @yieldparam [Gamefic::Action]
65
63
  # @return [Action::Hook]
66
64
  def before_action *verbs, &block
67
- rulebook.hooks.before_action *verbs, &block
65
+ rulebook.hooks.before_action self, *verbs, &block
68
66
  end
69
67
 
70
68
  # Add a proc to be evaluated after a character executes an action.
@@ -75,7 +73,7 @@ module Gamefic
75
73
  # @yieldparam [Gamefic::Action]
76
74
  # @return [Action::Hook]
77
75
  def after_action *verbs, &block
78
- rulebook.hooks.after_action *verbs, &block
76
+ rulebook.hooks.after_action self, *verbs, &block
79
77
  end
80
78
 
81
79
  # Create an alternate Syntax for a response.
@@ -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
@@ -14,7 +14,7 @@ module Gamefic
14
14
  # end
15
15
  #
16
16
  def on_ready &block
17
- rulebook.events.on_ready(&block)
17
+ rulebook.events.on_ready(Callback.new(self, block))
18
18
  end
19
19
 
20
20
  # Add a block to be executed for each player at the beginning of a turn.
@@ -28,37 +28,43 @@ module Gamefic
28
28
  #
29
29
  # @yieldparam [Gamefic::Actor]
30
30
  def on_player_ready &block
31
- rulebook.events.on_player_ready(&block)
31
+ wrapper = proc do
32
+ players.each { |player| Stage.run(self, player, &block) }
33
+ end
34
+ on_ready &wrapper
32
35
  end
33
36
 
34
37
  # Add a block to be executed after the Plot is finished updating a turn.
35
38
  #
36
39
  def on_update &block
37
- rulebook.events.on_update(&block)
40
+ rulebook.events.on_update(Callback.new(self, block))
38
41
  end
39
42
 
40
43
  # Add a block to be executed for each player at the end of a turn.
41
44
  #
42
45
  # @yieldparam [Gamefic::Actor]
43
46
  def on_player_update &block
44
- rulebook.events.on_player_update(&block)
47
+ wrapper = proc do
48
+ players.each { |player| Stage.run(self, player, &block) }
49
+ end
50
+ on_update &wrapper
45
51
  end
46
52
 
47
53
  def on_conclude &block
48
- rulebook.events.on_conclude(&block)
54
+ rulebook.events.on_conclude(Callback.new(self, block))
49
55
  end
50
56
 
51
57
  # @yieldparam [Actor]
52
58
  # @return [Proc]
53
59
  def on_player_conclude &block
54
- rulebook.events.on_player_conclude(&block)
60
+ rulebook.events.on_player_conclude(Callback.new(self, block))
55
61
  end
56
62
 
57
63
  # @yieldparam [Actor]
58
64
  # @yieldparam [Hash]
59
65
  # @return [Proc]
60
66
  def on_player_output &block
61
- rulebook.events.on_player_output(&block)
67
+ rulebook.events.on_player_output Callback.new(self, block)
62
68
  end
63
69
  end
64
70
  end
@@ -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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Scriptable
5
+ # Methods for referencing entities from proxies.
6
+ #
7
+ module Proxies
8
+ # Convert a proxy into its referenced entity.
9
+ #
10
+ # This method can receive any kind of object. If it's a proxy, its entity
11
+ # will be returned. If it's an array, each of its elements will be
12
+ # unproxied. If it's a hash, each of its values will be unproxied. Any
13
+ # other object will be returned unchanged.
14
+ #
15
+ # @param object [Object]
16
+ # @return [Object]
17
+ def unproxy object
18
+ case object
19
+ when Proxy
20
+ object.fetch self
21
+ when Array
22
+ object.map { |obj| unproxy obj }
23
+ when Hash
24
+ object.transform_values { |val| unproxy val }
25
+ else
26
+ object
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -5,14 +5,14 @@ module Gamefic
5
5
  # Scriptable methods related to creating action queries.
6
6
  #
7
7
  module Queries
8
- include Proxy
8
+ include Proxies
9
9
 
10
10
  # Define a query that searches the entire plot's entities.
11
11
  #
12
12
  # @param args [Array<Object>] Query arguments
13
13
  # @return [Query::General]
14
14
  def anywhere *args, ambiguous: false
15
- Query::General.new -> { entities }, *unproxy(args), ambiguous: ambiguous
15
+ Query::General.new -> { entities }, *args, ambiguous: ambiguous
16
16
  end
17
17
 
18
18
  # Define a query that searches an actor's family of entities. The
@@ -22,7 +22,7 @@ module Gamefic
22
22
  # @param args [Array<Object>] Query arguments
23
23
  # @return [Query::Scoped]
24
24
  def available *args, ambiguous: false
25
- Query::Scoped.new Scope::Family, *unproxy(args), ambiguous: ambiguous
25
+ Query::Scoped.new Scope::Family, *args, ambiguous: ambiguous
26
26
  end
27
27
  alias family available
28
28
 
@@ -31,7 +31,7 @@ module Gamefic
31
31
  # @param args [Array<Object>] Query arguments
32
32
  # @return [Query::Scoped]
33
33
  def parent *args, ambiguous: false
34
- Query::Scoped.new Scope::Parent, *unproxy(args), ambiguous: ambiguous
34
+ Query::Scoped.new Scope::Parent, *args, ambiguous: ambiguous
35
35
  end
36
36
 
37
37
  # Define a query that searches an actor's children.
@@ -39,7 +39,15 @@ module Gamefic
39
39
  # @param args [Array<Object>] Query arguments
40
40
  # @return [Query::Scoped]
41
41
  def children *args, ambiguous: false
42
- Query::Scoped.new Scope::Children, *unproxy(args), ambiguous: ambiguous
42
+ Query::Scoped.new Scope::Children, *args, ambiguous: ambiguous
43
+ end
44
+
45
+ # Define a query that searches an actor's descendants.
46
+ #
47
+ # @param args [Array<Object>] Query arguments
48
+ # @return [Query::Scoped]
49
+ def descendants *args, ambiguous: false
50
+ Query::Scoped.new Scope::Descendants, *args, ambiguous: ambiguous
43
51
  end
44
52
 
45
53
  # Define a query that searches an actor's siblings.
@@ -47,7 +55,7 @@ module Gamefic
47
55
  # @param args [Array<Object>] Query arguments
48
56
  # @return [Query::Scoped]
49
57
  def siblings *args, ambiguous: false
50
- Query::Scoped.new Scope::Siblings, *unproxy(args), ambiguous: ambiguous
58
+ Query::Scoped.new Scope::Siblings, *args, ambiguous: ambiguous
51
59
  end
52
60
 
53
61
  # Define a query that returns the actor itself.
@@ -55,7 +63,7 @@ module Gamefic
55
63
  # @param args [Array<Object>] Query arguments
56
64
  # @return [Query::Scoped]
57
65
  def myself *args, ambiguous: false
58
- Query::Scoped.new Scope::Myself, *unproxy(args), ambiguous: ambiguous
66
+ Query::Scoped.new Scope::Myself, *args, ambiguous: ambiguous
59
67
  end
60
68
 
61
69
  # Define a query that performs a plaintext search. It can take a String
@@ -33,13 +33,12 @@ module Gamefic
33
33
  # @yieldparam [Scene]
34
34
  # @return [Symbol]
35
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)
36
+ rulebook.scenes.add klass.new(name, self, on_start: on_start, on_finish: on_finish, &blk)
37
37
  name
38
38
  end
39
- alias scene block
40
39
 
41
40
  def preface name, klass = Scene::Activity, &start
42
- rulebook.scenes.add klass.new(name, rulebook.narrative, on_start: start)
41
+ rulebook.scenes.add klass.new(name, self, on_start: start)
43
42
  name
44
43
  end
45
44
  alias precursor preface
@@ -60,7 +59,7 @@ module Gamefic
60
59
  def introduction(&start)
61
60
  rulebook.scenes
62
61
  .introduction Scene::Default.new nil,
63
- rulebook.narrative,
62
+ self,
64
63
  on_start: proc { |actor, _props| Stage.run(self, actor, &start) }
65
64
  end
66
65
 
@@ -160,9 +159,16 @@ module Gamefic
160
159
  on_start: start
161
160
  end
162
161
 
162
+ # @return [Array<Symbol>]
163
163
  def scenes
164
164
  rulebook.scenes.names
165
165
  end
166
+
167
+ # @param name [Symbol]
168
+ # @return [Scene::Default, nil]
169
+ def scene(name)
170
+ rulebook.scenes[name]
171
+ end
166
172
  end
167
173
  end
168
174
  end