gamefic 3.4.0 → 3.6.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +1 -1
  4. data/lib/gamefic/action.rb +1 -0
  5. data/lib/gamefic/active/epic.rb +1 -0
  6. data/lib/gamefic/active.rb +4 -0
  7. data/lib/gamefic/chapter.rb +25 -42
  8. data/lib/gamefic/command.rb +49 -1
  9. data/lib/gamefic/dispatcher.rb +8 -31
  10. data/lib/gamefic/entity.rb +26 -0
  11. data/lib/gamefic/expression.rb +3 -0
  12. data/lib/gamefic/narrative.rb +9 -5
  13. data/lib/gamefic/node.rb +3 -5
  14. data/lib/gamefic/plot.rb +5 -0
  15. data/lib/gamefic/proxy/base.rb +27 -0
  16. data/lib/gamefic/proxy/config.rb +16 -0
  17. data/lib/gamefic/proxy/pick.rb +11 -0
  18. data/lib/gamefic/proxy/plot_pick.rb +11 -0
  19. data/lib/gamefic/proxy.rb +79 -0
  20. data/lib/gamefic/query/abstract.rb +12 -0
  21. data/lib/gamefic/query/base.rb +62 -16
  22. data/lib/gamefic/query/general.rb +6 -15
  23. data/lib/gamefic/query/result.rb +4 -1
  24. data/lib/gamefic/query/scoped.rb +3 -21
  25. data/lib/gamefic/query/text.rb +17 -15
  26. data/lib/gamefic/query.rb +1 -0
  27. data/lib/gamefic/response.rb +75 -34
  28. data/lib/gamefic/scanner/base.rb +44 -0
  29. data/lib/gamefic/scanner/fuzzy.rb +17 -0
  30. data/lib/gamefic/scanner/fuzzy_nesting.rb +14 -0
  31. data/lib/gamefic/scanner/nesting.rb +39 -0
  32. data/lib/gamefic/scanner/result.rb +60 -0
  33. data/lib/gamefic/scanner/strict.rb +31 -0
  34. data/lib/gamefic/scanner.rb +33 -111
  35. data/lib/gamefic/scope/descendants.rb +16 -0
  36. data/lib/gamefic/scope/family.rb +31 -8
  37. data/lib/gamefic/scope.rb +1 -0
  38. data/lib/gamefic/scriptable/actions.rb +4 -23
  39. data/lib/gamefic/scriptable/entities.rb +32 -17
  40. data/lib/gamefic/scriptable/plot_proxies.rb +29 -0
  41. data/lib/gamefic/scriptable/proxies.rb +31 -0
  42. data/lib/gamefic/scriptable/queries.rb +27 -8
  43. data/lib/gamefic/scriptable/scenes.rb +7 -1
  44. data/lib/gamefic/scriptable.rb +78 -16
  45. data/lib/gamefic/stage.rb +2 -2
  46. data/lib/gamefic/subplot.rb +33 -1
  47. data/lib/gamefic/version.rb +1 -1
  48. data/lib/gamefic.rb +1 -1
  49. metadata +17 -4
  50. data/lib/gamefic/composer.rb +0 -70
  51. data/lib/gamefic/scriptable/proxy.rb +0 -69
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79b61813b56cbbabb76ecbef9abb370b2fe77b58ea3e155377f8c4dfd2677684
4
- data.tar.gz: 611c108a0c080796c8308bee5f3e361c60fc4ddd316234bf9468c09742b1a52d
3
+ metadata.gz: d5c61efc096ecbacdd43431dd3f0b755120650258a4a3e5d724692957ee45f53
4
+ data.tar.gz: 499d740594a35e767c86fae7610a51dafa9ba647658af7b3ab46c61de4335501
5
5
  SHA512:
6
- metadata.gz: 85732c76dae467bd5d032462ca23d34b03b7be59c6327202fd9bef307ef3d06bc40af085f32010cfae9dfd93a85b20524785313388964772534632cc95159625
7
- data.tar.gz: 5c3db85ff44b268bd7604d33e28b92ca454c82c2a812f023af39a3da816a4252dfea01869d313f1ab6d24a1f062782ac002f5bf8fc40fd435efaa0c1ac14d337
6
+ metadata.gz: dd430f5dfa970cacfe12915ff09284ef286b1116aa701ce5484472fdf880b68a5de91953182189c647e90c527653bde92db26016045ac5ea5304e660082979fe
7
+ data.tar.gz: 1726fc542b40c55443bc9b452df85d94fe0831fa19bc73e40076cd6768e92bc71e504fd2da7998ef65b913c21f8b3ccb7bd4f04329186b88e71eb9dc675b0b69
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## 3.6.0 - October 6, 2024
2
+ - Normalized arguments accept strings
3
+ - Smarter picks and proxies
4
+ - Commands prefer strictness over precision
5
+ - Queries scan for ambiguity before filtering through arguments
6
+ - Abstract queries
7
+ - Command logging
8
+
9
+ ## 3.5.0 - October 5, 2024
10
+ - Configurable scanners
11
+ - Refactored scanners and queries
12
+ - Allow assignment to nil instance variables in stage
13
+ - Lazy proxies
14
+ - Remove buggy index proxies
15
+ - Chapter inherits Narrative
16
+ - Plot attribute proxy for chapters and subplots
17
+ - Entity#leave
18
+ - Descendants query
19
+ - Persistent subplots
20
+ - Entity#broadcast
21
+ - Ascendants in family query
22
+
1
23
  ## 3.4.0 - September 10, 2024
2
24
  - Chapters
3
25
  - Subplots and chapters do not repeat plot scripts
data/README.md CHANGED
@@ -16,7 +16,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
16
16
 
17
17
  ## Contributing
18
18
 
19
- Bug reports and pull requests are welcome on GitHub at https://github.com/castwide/gamefic-sdk.
19
+ Bug reports and pull requests are welcome on GitHub at https://github.com/castwide/gamefic.
20
20
 
21
21
  ## License
22
22
 
@@ -51,6 +51,7 @@ module Gamefic
51
51
  def execute
52
52
  return self if cancelled? || executed?
53
53
 
54
+ Gamefic.logger.info "Executing #{([verb] + [arguments]).flatten.map(&:inspect).join(', ')}"
54
55
  @executed = true
55
56
  response.execute actor, *arguments
56
57
  self
@@ -51,6 +51,7 @@ module Gamefic
51
51
  rulebooks.flat_map(&:syntaxes)
52
52
  end
53
53
 
54
+ # @return [Array<Response>]
54
55
  def responses_for(*verbs)
55
56
  rulebooks.to_a
56
57
  .reverse
@@ -212,6 +212,10 @@ module Gamefic
212
212
  false
213
213
  end
214
214
 
215
+ def acting?
216
+ !epic.empty?
217
+ end
218
+
215
219
  private
216
220
 
217
221
  # @return [Array<Dispatcher>]
@@ -6,17 +6,15 @@ module Gamefic
6
6
  # adding the required instance variables, methods, and attributes to the
7
7
  # plot.
8
8
  #
9
- # Chapters are similar to subplots with a few important exceptions:
10
- # * Chapters persist for the duration of the plot.
9
+ # Chapters are similar to subplots with three important exceptions:
10
+ # * Chapters normally persist for the duration of a plot.
11
11
  # * Players do not need to be introduced to a chapter.
12
- # * Scripts in chapters apply to the parent plot's rulebook.
13
- # * Using `make` to create an entity in a chapter adds it to the parent
14
- # plot's entity list.
12
+ # * Chapters share their plot's entities, players, and rulebook.
15
13
  #
16
14
  # @example
17
15
  # class MyChapter < Gamefic::Chapter
18
- # seed do
19
- # @thing = make Gamefic::Entity, name: 'chapter thing'
16
+ # def thing
17
+ # @thing ||= make Gamefic::Entity, name: 'chapter thing'
20
18
  # end
21
19
  # end
22
20
  #
@@ -25,64 +23,49 @@ module Gamefic
25
23
  # end
26
24
  #
27
25
  # plot = MyPlot.new
28
- # plot.entities #=> [<#Gamefic::Entity a chapter thing>]
29
- # plot.instance_exec { @thing } #=> nil
26
+ # plot.entities #=> [<#Gamefic::Entity a chapter thing>]
27
+ # plot.thing # raises NoMethodError
28
+ # plot.chapters.first.thing #=> <#Gamefic::Entity a chapter thing>
30
29
  #
31
- class Chapter
32
- extend Scriptable
33
-
34
- include Scriptable::Actions
35
- include Scriptable::Events
36
- include Scriptable::Proxy
37
- include Scriptable::Queries
38
- include Scriptable::Scenes
30
+ class Chapter < Narrative
31
+ extend Scriptable::PlotProxies
39
32
 
40
33
  # @return [Plot]
41
34
  attr_reader :plot
42
35
 
43
36
  # @param plot [Plot]
44
- def initialize plot
37
+ def initialize(plot)
45
38
  @plot = plot
46
- end
47
-
48
- def included_blocks
49
- self.class.included_blocks - plot.included_blocks
50
- end
51
-
52
- def seed
53
- included_blocks.select(&:seed?).each { |blk| Stage.run self, &blk.code }
39
+ # The plot is responsible for hydrating chapters
40
+ super(hydrate: false)
54
41
  end
55
42
 
56
43
  def script
57
44
  included_blocks.select(&:script?).each { |blk| Stage.run self, &blk.code }
58
45
  end
59
46
 
60
- def rulebook
61
- plot.rulebook
62
- end
63
-
64
- def make klass, **opts
65
- plot.make klass, **opts
47
+ def included_blocks
48
+ self.class.included_blocks - plot.included_blocks
66
49
  end
67
50
 
68
- def entities
69
- plot.entities
51
+ def rulebook
52
+ plot.rulebook
70
53
  end
71
54
 
72
- def players
73
- plot.players
55
+ def entity_vault
56
+ plot.entity_vault
74
57
  end
75
58
 
76
- def destroy entity
77
- plot.destroy entity
59
+ def player_vault
60
+ plot.player_vault
78
61
  end
79
62
 
80
- def pick description
81
- plot.pick description
63
+ def subplots
64
+ plot.subplots
82
65
  end
83
66
 
84
- def pick! description
85
- plot.pick! description
67
+ def branch(...)
68
+ plot.branch(...)
86
69
  end
87
70
  end
88
71
  end
@@ -10,11 +10,59 @@ module Gamefic
10
10
  # @return [Array<Array<Entity>, Entity, String>]
11
11
  attr_reader :arguments
12
12
 
13
+ # @return [Integer]
14
+ attr_reader :strictness
15
+
16
+ # @return [Integer]
17
+ attr_reader :precision
18
+
13
19
  # @param verb [Symbol]
14
20
  # @param arguments [Array<Array<Entity>, Entity, String>]
15
- def initialize verb, arguments
21
+ # @param strictness [Integer]
22
+ # @param precision [Integer]
23
+ #
24
+ # @todo Consider making strictness and precision required or providing
25
+ # another generator
26
+ def initialize verb, arguments, strictness = 0, precision = 0
16
27
  @verb = verb
17
28
  @arguments = arguments
29
+ @strictness = strictness
30
+ @precision = precision
31
+ end
32
+
33
+ def substantiality
34
+ @substantiality ||= arguments.that_are(Entity).length + (verb ? 1 : 0)
35
+ end
36
+
37
+ def inspect
38
+ "#<#{self.class} #{([verb] + arguments).map(&:inspect).join(', ')}>"
39
+ end
40
+
41
+ class << self
42
+ # Compose a command from input.
43
+ #
44
+ # @param actor [Actor]
45
+ # @param input [String]
46
+ # @return [Command]
47
+ def compose actor, input
48
+ expressions = Syntax.tokenize(input, actor.epic.syntaxes)
49
+ expressions.flat_map { |expression| expression_to_commands(actor, expression) }
50
+ .first || Command.new(nil, [])
51
+ end
52
+
53
+ private
54
+
55
+ # @param actor [Actor]
56
+ # @param expression [Expression]
57
+ # @return [Array<Command>]
58
+ def expression_to_commands actor, expression
59
+ Gamefic.logger.info "Evaluating #{expression.inspect}"
60
+ actor.epic
61
+ .responses_for(expression.verb)
62
+ .map { |response| response.to_command(actor, expression) }
63
+ .compact
64
+ .sort_by.with_index { |result, idx| [-result.substantiality, -result.strictness, -result.precision, idx] }
65
+ end
18
66
  end
19
67
  end
20
68
  end
@@ -4,13 +4,15 @@ module Gamefic
4
4
  # The action executor for character commands.
5
5
  #
6
6
  class Dispatcher
7
+ include Logging
8
+
7
9
  # @param actor [Actor]
8
10
  # @param command [Command]
9
11
  def initialize actor, command
10
12
  @actor = actor
11
13
  @command = command
12
14
  @executed = false
13
- @finalized = false
15
+ Gamefic.logger.info "Dispatching #{command.inspect}"
14
16
  end
15
17
 
16
18
  # Run the dispatcher.
@@ -23,11 +25,11 @@ module Gamefic
23
25
  action = next_action
24
26
  return unless action
25
27
 
26
- run_before_action_hooks action
28
+ actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_before_actions action }
27
29
  return if action.cancelled?
28
30
 
29
31
  action.execute
30
- run_after_action_hooks action
32
+ actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_after_actions action }
31
33
  action
32
34
  end
33
35
 
@@ -46,9 +48,9 @@ module Gamefic
46
48
  # @param input [String]
47
49
  # @return [Dispatcher]
48
50
  def self.dispatch actor, input
49
- expressions = Syntax.tokenize(input, actor.epic.syntaxes)
50
- command = Composer.compose(actor, expressions)
51
- new(actor, command)
51
+ # expressions = Syntax.tokenize(input, actor.epic.syntaxes)
52
+ # new(actor, Command.compose(actor, expressions))
53
+ new(actor, Command.compose(actor, input))
52
54
  end
53
55
 
54
56
  # @param actor [Active]
@@ -82,31 +84,6 @@ module Gamefic
82
84
 
83
85
  return Action.new(actor, @command.arguments, response) if response.accept?(actor, @command)
84
86
  end
85
- finalize
86
- end
87
-
88
- # @return [void]
89
- def run_before_action_hooks action
90
- actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_before_actions action }
91
- end
92
-
93
- # @return [void]
94
- def run_after_action_hooks action
95
- actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_after_actions action }
96
- end
97
-
98
- # If the dispatcher proceeds through all possible responses, it can fall
99
- # back to a nil response as a catchall for commands that could not be
100
- # completed.
101
- #
102
- # @return [Action, nil]
103
- def finalize
104
- return nil if @finalized
105
-
106
- @finalized = true
107
- @command = Command.new(nil, ["#{command.verb} #{command.arguments.join(' ').strip}"])
108
- @responses = actor.epic.responses_for(nil)
109
- next_action
110
87
  end
111
88
  end
112
89
  end
@@ -55,6 +55,32 @@ module Gamefic
55
55
  "#<#{self.class} #{name}>"
56
56
  end
57
57
 
58
+ # Move this entity to its parent entity.
59
+ #
60
+ # @example
61
+ # room = Gamefic::Entity.new(name: 'room')
62
+ # person = Gamefic::Entity.new(name: 'person', parent: room)
63
+ # thing = Gamefic::Entity.new(name: 'thing', parent: person)
64
+ #
65
+ # thing.parent #=> person
66
+ # thing.leave
67
+ # thing.parent #=> room
68
+ #
69
+ # @return [void]
70
+ def leave
71
+ self.parent = parent&.parent
72
+ end
73
+
74
+ # Tell a message to all of this entity's accessible descendants.
75
+ #
76
+ # @param message [String]
77
+ # @return [void]
78
+ def broadcast message
79
+ Query::Scoped.new(Scope::Descendants).select(self)
80
+ .that_are(Active, proc(&:acting?))
81
+ .each { |actor| actor.tell message }
82
+ end
83
+
58
84
  class << self
59
85
  # Set or update the default attributes for new instances.
60
86
  #
@@ -17,6 +17,9 @@ module Gamefic
17
17
  @tokens = tokens
18
18
  end
19
19
 
20
+ def inspect
21
+ "#<#{self.class} #{([verb] + tokens).map(&:inspect).join(', ')}>"
22
+ end
20
23
  # Compare two syntaxes for the purpose of ordering them by relevance while
21
24
  # dispatching.
22
25
  #
@@ -14,16 +14,18 @@ module Gamefic
14
14
  include Scriptable::Actions
15
15
  include Scriptable::Entities
16
16
  include Scriptable::Events
17
- include Scriptable::Proxy
17
+ include Scriptable::Proxies
18
18
  include Scriptable::Queries
19
19
  include Scriptable::Scenes
20
20
 
21
21
  attr_reader :rulebook
22
22
 
23
- def initialize
23
+ def initialize(hydrate: true)
24
+ return unless hydrate
25
+
24
26
  seed
25
27
  script
26
- post_initialize
28
+ post_script
27
29
  end
28
30
 
29
31
  def seed
@@ -35,15 +37,17 @@ module Gamefic
35
37
  included_blocks.select(&:script?).each { |blk| Stage.run self, &blk.code }
36
38
  end
37
39
 
40
+ # @return [Array<Module>]
38
41
  def included_blocks
39
42
  self.class.included_blocks
40
43
  end
41
44
 
42
- def post_initialize
45
+ def post_script
43
46
  entity_vault.lock
44
47
  rulebook.freeze
45
48
  end
46
49
 
50
+ # @return [Array<Symbol>]
47
51
  def scenes
48
52
  rulebook.scenes.names
49
53
  end
@@ -111,7 +115,7 @@ module Gamefic
111
115
 
112
116
  def hydrate
113
117
  script
114
- post_initialize
118
+ post_script
115
119
  end
116
120
 
117
121
  def self.inherited klass
data/lib/gamefic/node.rb CHANGED
@@ -88,11 +88,9 @@ module Gamefic
88
88
  end
89
89
 
90
90
  def validate_parent(node)
91
- raise NodeError, 'Parent must be a Node' unless node.is_a?(Node) || node.nil?
92
-
93
- raise NodeError, "Node cannot be its own parent" if node == self
94
-
95
- raise NodeError, 'Node cannot be a child of a descendant' if flatten.include?(node)
91
+ raise NodeError, "Parent of #{inspect} must be a Node, received #{node.inspect}" unless node.is_a?(Node) || node.nil?
92
+ raise NodeError, "#{inspect} cannot be its own parent" if node == self
93
+ raise NodeError, "#{inspect} cannot be a child of descendant #{node.inspect}" if flatten.include?(node)
96
94
  end
97
95
  end
98
96
  end
data/lib/gamefic/plot.rb CHANGED
@@ -16,6 +16,11 @@ module Gamefic
16
16
  rulebook.scenes.with_defaults self
17
17
  end
18
18
 
19
+ def post_script
20
+ super
21
+ chapters.freeze
22
+ end
23
+
19
24
  def chapters
20
25
  @chapters ||= self.class.appended_chapters.map { |klass| klass.new(self) }
21
26
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Proxy
5
+ class Base
6
+ attr_reader :args
7
+
8
+ def initialize *args, raise: false
9
+ @args = args
10
+ @raise = raise
11
+ end
12
+
13
+ def raise?
14
+ @raise
15
+ end
16
+
17
+ def fetch narrative
18
+ result = select(narrative)
19
+ return result if result
20
+ raise "#{self.class} failed for #{args.inspect}" if raise?
21
+ end
22
+
23
+ def select narrative
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Proxy
5
+ class Config < Base
6
+ def select narrative
7
+ args.inject(narrative.config) { |hash, key| hash[key] }
8
+ end
9
+
10
+ def [](key)
11
+ args.push key
12
+ self
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Proxy
5
+ class Pick < Base
6
+ def select narrative
7
+ raise? ? narrative.pick!(*args) : narrative.pick(*args)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Proxy
5
+ class PlotPick < Base
6
+ def select narrative
7
+ raise? ? narrative.plot.pick!(*args) : narrative.plot.pick(*args)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ # @todo Turn this into a module after the old proxies are completely deprecated
5
+ #
6
+ class Proxy
7
+ require 'gamefic/proxy/base'
8
+ require 'gamefic/proxy/config'
9
+ require 'gamefic/proxy/pick'
10
+ require 'gamefic/proxy/plot_pick'
11
+
12
+ TYPES = %i[attr ivar pick pick! plot_pick plot_pick! config].freeze
13
+
14
+ # @return [Symbol]
15
+ attr_reader :type
16
+
17
+ # @return [Symbol, Array<Symbol>, String, Integer]
18
+ attr_reader :key
19
+
20
+ # @param type [Symbol]
21
+ # @param key [Symbol, String, Array]
22
+ def initialize type, key
23
+ Gamefic.logger.debug "Using deprecated #{type} proxy"
24
+ @type = type
25
+ validate_type
26
+ @key = type == :config ? [key].compact : key
27
+ end
28
+
29
+ def fetch narrative
30
+ send(type, narrative) ||
31
+ raise(ArgumentError, "Unable to fetch entity from proxy agent symbol `#{key}`")
32
+ end
33
+
34
+ def [](key)
35
+ raise ArgumentError, 'Invalid []' unless type == :config
36
+
37
+ @key.push key
38
+ self
39
+ end
40
+
41
+ private
42
+
43
+ def attr narrative
44
+ Stage.run(narrative, [key].flatten) { |keys| keys.inject(self) { |obj, key| obj.send key } }
45
+ rescue NoMethodError
46
+ nil
47
+ end
48
+
49
+ def ivar narrative
50
+ narrative.instance_variable_get key
51
+ end
52
+
53
+ def pick narrative
54
+ narrative.pick *key
55
+ end
56
+
57
+ def pick! narrative
58
+ narrative.pick! *key
59
+ end
60
+
61
+ def plot_pick narrative
62
+ narrative.plot.pick *key
63
+ end
64
+
65
+ def plot_pick! narrative
66
+ narrative.plot.pick! *key
67
+ end
68
+
69
+ def config narrative
70
+ key.inject(narrative.config) { |hash, key| hash[key] }
71
+ end
72
+
73
+ def validate_type
74
+ return if TYPES.include?(type)
75
+
76
+ raise ArgumentError, "Invalid proxy type `#{type}` (must be #{TYPES.join_or})"
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ module Query
5
+ class Abstract < General
6
+ def span subject
7
+ available_entities(subject).that_are(Describable)
8
+ .that_are_not(Entity)
9
+ end
10
+ end
11
+ end
12
+ end