gamefic 3.4.0 → 3.6.0

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