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
@@ -4,16 +4,20 @@ 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
9
  def initialize argument = /.*/
10
- @argument = argument
10
+ super
11
11
  validate
12
12
  end
13
13
 
14
+ def argument
15
+ arguments.first
16
+ end
17
+
14
18
  # @return [String, Regexp]
15
19
  def select(_subject)
16
- @argument
20
+ argument
17
21
  end
18
22
 
19
23
  def query _subject, token
@@ -23,6 +27,7 @@ module Gamefic
23
27
  Result.new(nil, token)
24
28
  end
25
29
  end
30
+ alias filter query
26
31
 
27
32
  def precision
28
33
  0
@@ -32,25 +37,21 @@ module Gamefic
32
37
  match? argument
33
38
  end
34
39
 
35
- def ambiguous?
36
- true
37
- end
38
-
39
40
  private
40
41
 
41
42
  def match? token
42
- return false unless token.is_a?(String)
43
+ return false unless token.is_a?(String) && !token.empty?
43
44
 
44
- case @argument
45
+ case argument
45
46
  when Regexp
46
- token =~ @argument
47
+ token =~ argument
47
48
  else
48
- token == @argument
49
+ token == argument
49
50
  end
50
51
  end
51
52
 
52
53
  def validate
53
- return if @argument.is_a?(String) || @argument.is_a?(Regexp)
54
+ return if argument.is_a?(String) || argument.is_a?(Regexp)
54
55
 
55
56
  raise ArgumentError, 'Invalid text query argument'
56
57
  end
@@ -8,23 +8,19 @@ 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
- # @return [Narrative]
15
- attr_reader :narrative
16
-
17
14
  # @param verb [Symbol]
18
15
  # @param narrative [Narrative]
19
- # @param queries [Array<Query::Base>]
16
+ # @param args [Array<Object>]
20
17
  # @param meta [Boolean]
21
18
  # @param block [Proc]
22
- def initialize verb, narrative, *queries, meta: false, &block
19
+ def initialize verb, narrative, *args, meta: false, &block
23
20
  @verb = verb
24
- @narrative = narrative
25
- @queries = map_queryable_objects(queries)
21
+ @queries = map_queries(args, narrative)
26
22
  @meta = meta
27
- @block = block
23
+ @callback = Callback.new(narrative, block)
28
24
  end
29
25
 
30
26
  # The `meta?` flag is just a way for authors to identify responses that
@@ -60,53 +56,85 @@ module Gamefic
60
56
  # @param actor [Active]
61
57
  # @param command [Command]
62
58
  def accept? actor, command
63
- return false if command.verb != verb || command.arguments.length != queries.length
64
-
65
- queries.each_with_index do |query, idx|
66
- return false unless query.accept?(actor, command.arguments[idx])
67
- end
68
-
69
- true
59
+ command.verb == verb &&
60
+ command.arguments.length == queries.length &&
61
+ queries.zip(command.arguments).all? { |query, argument| query.accept?(actor, argument) }
70
62
  end
71
63
 
72
64
  def execute *args
73
- Stage.run(narrative, *args, &@block)
65
+ @callback.run(*args)
74
66
  end
75
67
 
76
68
  def precision
77
69
  @precision ||= calculate_precision
78
70
  end
79
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
+
80
93
  private
81
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
+
82
109
  def generate_default_syntax
83
- user_friendly = verb.to_s.gsub(/_/, ' ')
84
- args = []
85
- used_names = []
86
- queries.each do |_c|
87
- num = 1
88
- new_name = ":var"
89
- while used_names.include? new_name
90
- num += 1
91
- new_name = ":var#{num}"
92
- end
93
- used_names.push new_name
94
- user_friendly += " #{new_name}"
95
- args.push new_name
96
- end
97
- 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)
98
113
  end
99
114
 
100
115
  def calculate_precision
101
- total = 0
102
- queries.each { |q| total += q.precision }
103
- total -= 1000 if verb.nil?
116
+ total = queries.sum(&:precision)
117
+ total -= 1000 unless verb
104
118
  total
105
119
  end
106
120
 
107
- def map_queryable_objects queries
108
- # @todo Considering moving mapping from Actions to here
109
- 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
110
138
  end
111
139
  end
112
140
  end
@@ -67,10 +67,6 @@ module Gamefic
67
67
  verb_response_map.empty? && synonym_syntax_map.empty?
68
68
  end
69
69
 
70
- def self.new_array_map
71
- Hash.new { |hash, key| hash[key] = [] }
72
- end
73
-
74
70
  private
75
71
 
76
72
  attr_reader :verb_response_map
@@ -35,44 +35,30 @@ module Gamefic
35
35
  end
36
36
 
37
37
  # @return [void]
38
- def on_ready &block
39
- @ready_blocks.push block
38
+ def on_ready callback
39
+ @ready_blocks.push callback
40
40
  end
41
41
 
42
- # @yieldparam [Actor]
43
- # @return [void]
44
- def on_player_ready &block
45
- @ready_blocks.push(proc do
46
- players.each { |plyr| instance_exec plyr, &block }
47
- end)
48
- end
49
-
50
- def on_update &block
51
- @update_blocks.push block
52
- end
53
-
54
- def on_player_update &block
55
- @update_blocks.push(proc do
56
- players.each { |plyr| instance_exec plyr, &block }
57
- end)
42
+ def on_update callback
43
+ @update_blocks.push callback
58
44
  end
59
45
 
60
46
  # @return [void]
61
- def on_conclude &block
62
- @conclude_blocks.push block
47
+ def on_conclude callback
48
+ @conclude_blocks.push callback
63
49
  end
64
50
 
65
51
  # @yieldparam [Actor]
66
52
  # @return [void]
67
- def on_player_conclude &block
68
- @player_conclude_blocks.push block
53
+ def on_player_conclude callback
54
+ @player_conclude_blocks.push callback
69
55
  end
70
56
 
71
57
  # @yieldparam [Actor]
72
58
  # @yieldparam [Hash]
73
59
  # @return [void]
74
- def on_player_output &block
75
- @player_output_blocks.push block
60
+ def on_player_output callback
61
+ @player_output_blocks.push callback
76
62
  end
77
63
  end
78
64
  end
@@ -21,35 +21,35 @@ module Gamefic
21
21
  self
22
22
  end
23
23
 
24
- def before_action *verbs, &block
25
- before_actions.push Action::Hook.new(*verbs, &block)
24
+ def before_action narrative, *verbs, &block
25
+ before_actions.push Action::Hook.new(verbs, Callback.new(narrative, block))
26
26
  end
27
27
 
28
- def after_action *verbs, &block
29
- after_actions.push Action::Hook.new(*verbs, &block)
28
+ def after_action narrative, *verbs, &block
29
+ after_actions.push Action::Hook.new(verbs, Callback.new(narrative, block))
30
30
  end
31
31
 
32
32
  def empty?
33
33
  before_actions.empty? && after_actions.empty?
34
34
  end
35
35
 
36
- def run_before action, narrative
37
- run_action_hooks action, narrative, before_actions
36
+ def run_before action
37
+ run_action_hooks action, before_actions
38
38
  end
39
39
 
40
- def run_after action, narrative
41
- run_action_hooks action, narrative, after_actions
40
+ def run_after action
41
+ run_action_hooks action, after_actions
42
42
  end
43
43
 
44
44
  private
45
45
 
46
- def run_action_hooks action, narrative, hooks
46
+ def run_action_hooks action, hooks
47
47
  hooks.each do |hook|
48
48
  break if action.cancelled?
49
49
 
50
50
  next unless hook.match?(action.verb)
51
51
 
52
- Stage.run(narrative) { instance_exec(action, &hook.block) }
52
+ hook.callback.run action
53
53
  end
54
54
  end
55
55
  end
@@ -25,12 +25,7 @@ module Gamefic
25
25
  # @return [Scenes]
26
26
  attr_reader :scenes
27
27
 
28
- # @return [Narrative]
29
- attr_reader :narrative
30
-
31
- # @param narrative [Narrative]
32
- def initialize(narrative)
33
- @narrative = narrative
28
+ def initialize
34
29
  @calls = Calls.new
35
30
  @events = Events.new
36
31
  @hooks = Hooks.new
@@ -96,44 +91,35 @@ module Gamefic
96
91
  end
97
92
 
98
93
  def run_ready_blocks
99
- events.ready_blocks.each { |blk| Stage.run narrative, &blk }
94
+ events.ready_blocks.each(&:run)
100
95
  end
101
96
 
102
97
  def run_update_blocks
103
- events.update_blocks.each { |blk| Stage.run narrative, &blk }
98
+ events.update_blocks.each(&:run)
104
99
  end
105
100
 
106
101
  def run_before_actions action
107
- hooks.run_before action, narrative
102
+ hooks.run_before action
108
103
  end
109
104
 
110
105
  def run_after_actions action
111
- hooks.run_after action, narrative
106
+ hooks.run_after action
112
107
  end
113
108
 
114
109
  def run_conclude_blocks
115
- events.conclude_blocks.each { |blk| Stage.run narrative, &blk }
110
+ events.conclude_blocks.each(&:run)
116
111
  end
117
112
 
118
113
  def run_player_conclude_blocks player
119
- events.player_conclude_blocks.each { |blk| Stage.run(narrative, player, &blk) }
114
+ events.player_conclude_blocks.each { |blk| blk.run(player) }
120
115
  end
121
116
 
122
117
  def run_player_output_blocks player, output
123
- events.player_output_blocks.each { |blk| Stage.run(narrative, player, output, &blk) }
118
+ events.player_output_blocks.each { |blk| blk.run(player, output) }
124
119
  end
125
120
 
126
121
  def empty?
127
122
  calls.empty? && hooks.empty? && scenes.empty? && events.empty?
128
123
  end
129
-
130
- def script
131
- narrative.class.included_blocks.select(&:script?).each { |blk| Stage.run(narrative, &blk.code) }
132
- end
133
-
134
- def script_with_defaults
135
- script
136
- scenes.with_defaults narrative
137
- end
138
124
  end
139
125
  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