gamefic 2.2.3 → 2.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a57ab533bcfbcbd650cf3ee60c5a639b3ddd40ad3ad96d0d4e42e6658547f3d9
4
- data.tar.gz: 904c5211a6bde65f870ae8bab1218423afd6618c6ddd79c44b01174bb069256b
3
+ metadata.gz: 2288262e86ac4a5cf259b3a37e80ba56a3a4ed334b8842770109e1930799585d
4
+ data.tar.gz: 542aaff9c3f4d0aae1a4471fba5d52ff35b569971c861af006ddc78f0d1cc568
5
5
  SHA512:
6
- metadata.gz: afc3e6a06d6ac451827cea70db94b8c9e4b10c795dac9812df0c24f8c1e0ce654748dfae53c4c0565bbfff8abb5d2f853f866c1aac8b398c5cf8331ee3ea1795
7
- data.tar.gz: dd790a5574ae81e91b61d01c7f18adba0abcd7e43650e72934329dfd84c9515adb54b6e1e0938814c9d6c6582d23801c66a470a28ae99700659fa0c462830660
6
+ metadata.gz: 6a89c7fb5978047517827e3cc2de5f172da5e6905a7e7efe0725763b4ce01d56725df46a443b903906effd962cebdbd238d81719b315eb26f3b8bf289d337089
7
+ data.tar.gz: 9976d0c58957309ba605e645a8c7e214d798c12e114b71e5d5fa2bb4b6d39878537612e670da6e42168dfd31ee5fce7304eee9983d1177d878262af630e6e582
@@ -0,0 +1,40 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with rspec.
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: RSpec
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ test:
21
+
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ matrix:
25
+ ruby-version: ['2.5', '2.6', '2.7', '3.0', '3.1']
26
+
27
+ steps:
28
+ - uses: actions/checkout@v3
29
+ - name: Set up Ruby
30
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
31
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
32
+ uses: ruby/setup-ruby@v1
33
+ # uses: ruby/setup-ruby@2b019609e2b0f1ea1a2bc8ca11cb82ab46ada124
34
+ with:
35
+ ruby-version: ${{ matrix.ruby-version }}
36
+ bundler-cache: false
37
+ - name: Install dependencies
38
+ run: bundle install
39
+ - name: Run tests
40
+ run: bundle exec rspec
data/.rubocop.yml CHANGED
@@ -10,4 +10,7 @@ Style/StringLiterals:
10
10
 
11
11
  Style/Documentation:
12
12
  Description: 'Document classes and non-namespace modules.'
13
- Enabled: false
13
+ Enabled: false
14
+
15
+ Style/MethodDefParentheses:
16
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
+ ## 2.4.0 - February 11, 2023
2
+ - Fix arity of delegated methods in scripts
3
+ - Action hooks accept verb filters
4
+ - Opal exception for delegated methods
5
+ - Support separation of kwargs in Ruby >= 2.7
6
+
7
+ ## 2.3.0 - January 25, 2023
8
+ - Remove unused Active#actions method
9
+ - Add before_action and after_action hooks
10
+
1
11
  ## 2.2.3 - July 9, 2022
2
12
  - Fix Ruby version incompatibilities
13
+ - Fix private attr_accessor call
3
14
 
4
15
  ## 2.2.2 - September 6, 2021
5
16
  - Darkroom indexes non-static elements
data/Gemfile CHANGED
@@ -1,7 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
-
5
- group :test do
6
- gem "simplecov"
7
- end
data/gamefic.gemspec CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
19
19
  end
20
20
  s.require_paths = ['lib']
21
21
 
22
- s.required_ruby_version = '>= 2.1.0'
22
+ s.required_ruby_version = '>= 2.3.0'
23
23
 
24
24
  s.add_development_dependency 'rake', '~> 12.3', '>= 12.3'
25
25
  s.add_development_dependency 'rspec', '~> 3.5', '>= 3.5.0'
@@ -1,5 +1,8 @@
1
1
  module Gamefic
2
2
  class Action
3
+ # @return [Gamefic::Actor]
4
+ attr_reader :actor
5
+
3
6
  # An array of objects on which the action will operate, e.g., an entity
4
7
  # that is a direct object of a command.
5
8
  #
@@ -7,17 +10,34 @@ module Gamefic
7
10
  attr_reader :arguments
8
11
  alias parameters arguments
9
12
 
10
- def initialize actor, arguments
13
+ # @param actor [Gamefic::Actor]
14
+ # @param arguments [Array<Object>]
15
+ def initialize actor, arguments, with_callbacks = false
11
16
  @actor = actor
12
17
  @arguments = arguments
13
18
  @executed = false
19
+ @with_callbacks = with_callbacks
14
20
  end
15
21
 
16
22
  # Perform the action.
17
23
  #
18
24
  def execute
19
- @executed = true
25
+ return if @cancelled
26
+ run_before_actions
27
+ return if @cancelled
20
28
  self.class.executor.call(@actor, *arguments) unless self.class.executor.nil?
29
+ @executed = true
30
+ run_after_actions
31
+ end
32
+
33
+ # Cancel an action. This method can be called in a before_action hook to
34
+ # prevent subsequent hooks and the action itself from being executed.
35
+ # Cancelling an action in an after_action hook has no effect.
36
+ #
37
+ def cancel
38
+ # @todo Emit a warning for attempts to cancel an action after it's been
39
+ # executed
40
+ @cancelled = true
21
41
  end
22
42
 
23
43
  # True if the #execute method has been called for this action.
@@ -27,6 +47,10 @@ module Gamefic
27
47
  @executed
28
48
  end
29
49
 
50
+ def cancelled?
51
+ !@executed && @cancelled
52
+ end
53
+
30
54
  # The verb associated with this action.
31
55
  #
32
56
  # @return [Symbol] The symbol representing the verb
@@ -68,6 +92,29 @@ module Gamefic
68
92
  act
69
93
  end
70
94
 
95
+ private
96
+
97
+ def run_before_actions
98
+ return unless @with_callbacks
99
+ @actor.playbooks
100
+ .flat_map(&:before_actions)
101
+ .each do |hook|
102
+ next unless hook.verb.nil? || hook.verb == verb
103
+ hook.block.call(self)
104
+ break if @cancelled
105
+ end
106
+ end
107
+
108
+ def run_after_actions
109
+ return unless @with_callbacks
110
+ @actor.playbooks
111
+ .flat_map(&:after_actions)
112
+ .each do |hook|
113
+ next unless hook.verb.nil? || hook.verb == verb
114
+ hook.block.call(self)
115
+ end
116
+ end
117
+
71
118
  class << self
72
119
  attr_reader :verb
73
120
 
@@ -135,7 +182,7 @@ module Gamefic
135
182
  # @param action [Gamefic::Entity]
136
183
  # @param command [Command]
137
184
  # @return [self, nil]
138
- def attempt actor, command
185
+ def attempt actor, command, with_callbacks = false
139
186
  return nil if command.verb != verb
140
187
  tokens = command.arguments
141
188
  result = []
@@ -153,7 +200,7 @@ module Gamefic
153
200
  result.push accepted.first
154
201
  end
155
202
  end
156
- new(actor, result)
203
+ new(actor, result, with_callbacks)
157
204
  end
158
205
 
159
206
  protected
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
1
5
  module Gamefic
2
6
  class NotConclusionError < RuntimeError; end
3
7
 
@@ -37,7 +41,7 @@ module Gamefic
37
41
  end
38
42
 
39
43
  def syntaxes
40
- playbooks.map(&:syntaxes).flatten
44
+ playbooks.flat_map(&:syntaxes)
41
45
  end
42
46
 
43
47
  # An array of actions waiting to be performed.
@@ -84,28 +88,18 @@ module Gamefic
84
88
  end
85
89
 
86
90
  # Perform a command.
87
- # The command can be specified as a String or a verb with a list of
88
- # parameters. Either form should yield the same result, but the
89
- # verb/parameter form can yield better performance since it bypasses the
90
- # parser.
91
91
  #
92
- # The command will be executed immediately regardless of the entity's
93
- # state.
92
+ # The command's action will be executed immediately regardless of the
93
+ # entity's state.
94
94
  #
95
95
  # @example Send a command as a string
96
96
  # character.perform "take the key"
97
97
  #
98
- # @example Send a command as a verb with parameters
99
- # character.perform :take, @key
100
- #
101
- # @todo Modify this method so it only accepts a single command.
102
- # Verbs with parameters should use Active#execute instead.
103
- # It might be necessary to support command splats with a deprecation
104
- # warning until version 3.
105
- #
98
+ # @param command [String, Symbol]
106
99
  # @return [Gamefic::Action]
107
100
  def perform(*command)
108
101
  if command.length > 1
102
+ STDERR.puts "[WARN] #{caller[0]}: Passing a verb and arguments to #perform is deprecated. Use #execute instead."
109
103
  execute command.first, *command[1..-1]
110
104
  else
111
105
  dispatchers.push Dispatcher.dispatch(self, command.first.to_s)
@@ -118,16 +112,18 @@ module Gamefic
118
112
  # This method executes the command exactly as #perform does, except it
119
113
  # buffers the resulting output instead of sending it to the user.
120
114
  #
121
- # @todo Modify this method so it only accepts a single command.
122
- # See Active#perform for more information.
123
- #
115
+ # @param command [String, Symbol]
124
116
  # @return [String] The output that resulted from performing the command.
125
117
  def quietly(*command)
126
- clear_buffer if buffer_stack == 0
127
- set_buffer_stack buffer_stack + 1
128
- self.perform *command
129
- set_buffer_stack buffer_stack - 1
130
- buffer
118
+ if command.length > 1
119
+ STDERR.puts "#{caller[0]}: Passing a verb and arguments to #quietly is deprecated. Use #execute instead"
120
+ execute command.first, *command[1..-1], quietly: true
121
+ else
122
+ dispatchers.push Dispatcher.dispatch(self, command.first.to_s)
123
+ result = proceed quietly: true
124
+ dispatchers.pop
125
+ result
126
+ end
131
127
  end
132
128
 
133
129
  # Perform an action.
@@ -141,6 +137,9 @@ module Gamefic
141
137
  # @example
142
138
  # character.execute :take, @key
143
139
  #
140
+ # @param verb [Symbol]
141
+ # @param params [Array]
142
+ # @params quietly [Boolean]
144
143
  # @return [Gamefic::Action]
145
144
  def execute(verb, *params, quietly: false)
146
145
  dispatchers.push Dispatcher.dispatch_from_params(self, verb, params)
@@ -171,21 +170,15 @@ module Gamefic
171
170
  # end
172
171
  # end
173
172
  #
173
+ # @param quietly [Boolean] If true, return the action's output instead of appending it to #messages
174
+ # @return [String, nil]
174
175
  def proceed quietly: false
175
176
  return if dispatchers.empty?
176
177
  a = dispatchers.last.next
177
178
  return if a.nil?
178
- if quietly
179
- if buffer_stack == 0
180
- @buffer = ""
181
- end
182
- set_buffer_stack(buffer_stack + 1)
183
- end
179
+ prepare_buffer quietly
184
180
  a.execute
185
- if quietly
186
- set_buffer_stack(buffer_stack - 1)
187
- @buffer
188
- end
181
+ flush_buffer quietly
189
182
  end
190
183
 
191
184
  # Immediately start a new scene for the character.
@@ -251,8 +244,8 @@ module Gamefic
251
244
  # Track the entity's performance of a scene.
252
245
  #
253
246
  def entered scene
254
- klass = (scene.kind_of?(Gamefic::Scene::Base) ? scene.class : scene)
255
- entered_scenes.push klass unless entered_scenes.include?(klass)
247
+ klass = (scene.is_a?(Gamefic::Scene::Base) ? scene.class : scene)
248
+ entered_scenes.add klass
256
249
  end
257
250
 
258
251
  # Determine whether the entity has performed the specified scene.
@@ -265,9 +258,25 @@ module Gamefic
265
258
 
266
259
  private
267
260
 
268
- # @return [Array<Gamefic::Scene::Base>]
261
+ def prepare_buffer quietly
262
+ if quietly
263
+ if buffer_stack == 0
264
+ @buffer = ""
265
+ end
266
+ set_buffer_stack(buffer_stack + 1)
267
+ end
268
+ end
269
+
270
+ def flush_buffer quietly
271
+ if quietly
272
+ set_buffer_stack(buffer_stack - 1)
273
+ @buffer
274
+ end
275
+ end
276
+
277
+ # @return [Set<Gamefic::Scene::Base>]
269
278
  def entered_scenes
270
- @entered_scenes ||= []
279
+ @entered_scenes ||= Set.new
271
280
  end
272
281
 
273
282
  def buffer_stack
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Array
2
4
  # Get a subset of the array that matches the arguments.
3
5
  # If the argument is a Class or Module, the elements must be of the type.
@@ -12,7 +14,7 @@ class Array
12
14
  #
13
15
  # @return [Array]
14
16
  def that_are(*cls)
15
- result = self.clone
17
+ result = clone
16
18
  cls.each do |c|
17
19
  _keep result, c, true
18
20
  end
@@ -24,7 +26,7 @@ class Array
24
26
  #
25
27
  # @return [Array]
26
28
  def that_are_not(*cls)
27
- result = self.clone
29
+ result = clone
28
30
  cls.each do |c|
29
31
  _keep result, c, false
30
32
  end
@@ -34,7 +36,7 @@ class Array
34
36
  # Pop a random element from the array.
35
37
  #
36
38
  def pop_sample
37
- delete_at(rand(self.length))
39
+ delete_at(rand(length))
38
40
  end
39
41
 
40
42
  # Get a string representation of the array that separates elements with
@@ -48,31 +50,32 @@ class Array
48
50
  # @param andSep [String] The separator for the last element
49
51
  # @param serial [Boolean] Use serial separators (e.g., serial commas)
50
52
  # @return [String]
51
- def join_and(sep = ', ', andSep = ' and ', serial = true)
52
- if self.length < 3
53
- self.join(andSep)
53
+ def join_and(sep = ', ', and_sep = ' and ', serial = true)
54
+ if length < 3
55
+ join(and_sep)
54
56
  else
55
57
  start = self[0..-2]
56
- start.join(sep) + "#{serial ? sep.strip : ''}#{andSep}#{self.last}"
58
+ start.join(sep) + "#{serial ? sep.strip : ''}#{and_sep}#{last}"
57
59
  end
58
60
  end
59
61
 
60
62
  # @see Array#join_and
61
63
  #
62
64
  # @return [String]
63
- def join_or(sep = ', ', orSep = ' or ', serial = true)
64
- join_and(sep, orSep, serial)
65
+ def join_or(sep = ', ', or_sep = ' or ', serial = true)
66
+ join_and(sep, or_sep, serial)
65
67
  end
66
68
 
67
69
  private
68
70
 
69
71
  def _keep(arr, cls, bool)
70
- if (cls.kind_of?(Class) or cls.kind_of?(Module))
72
+ case cls
73
+ when Class, Module
71
74
  arr.keep_if { |i| i.is_a?(cls) == bool }
72
- elsif cls.kind_of?(Symbol)
75
+ when Symbol
73
76
  arr.keep_if { |i| i.send(cls) == bool }
74
77
  else
75
- arr.keep_if {|i| (i == cls) == bool}
78
+ arr.keep_if { |i| (i == cls) == bool }
76
79
  end
77
80
  end
78
81
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  # The action selector for character commands.
3
5
  #
@@ -9,25 +11,27 @@ module Gamefic
9
11
  @actor = actor
10
12
  @commands = commands
11
13
  @actions = actions
14
+ @started = false
12
15
  end
13
16
 
17
+ # @param dispatcher [Dispatcher]
18
+ # @return [void]
14
19
  def merge dispatcher
15
20
  commands.concat dispatcher.commands
16
21
  actions.concat dispatcher.actions
17
22
  end
18
23
 
24
+ # Get the next executable action.
25
+ #
26
+ # @return [Action]
19
27
  def next
20
28
  instance = nil
21
29
  while instance.nil? && !@actions.empty?
22
30
  action = actions.shift
23
31
  commands.each do |cmd|
24
- instance = action.attempt(actor, cmd)
32
+ instance = action.attempt(actor, cmd, !@started)
25
33
  if instance
26
- unless instance.meta?
27
- actor.playbooks.reverse.each do |playbook|
28
- return nil unless validate_playbook(playbook, instance)
29
- end
30
- end
34
+ @started = true
31
35
  break
32
36
  end
33
37
  end
@@ -66,11 +70,5 @@ module Gamefic
66
70
 
67
71
  # @return [Array<Action>]
68
72
  attr_reader :actions
69
-
70
- private
71
-
72
- def validate_playbook playbook, action
73
- playbook.validators.all? { |v| v.call(actor, action.verb, action.parameters) != false }
74
- end
75
73
  end
76
74
  end
data/lib/gamefic/plot.rb CHANGED
@@ -34,6 +34,10 @@ module Gamefic
34
34
  @static = [self] + scene_classes + entities
35
35
  end
36
36
 
37
+ def plot
38
+ self
39
+ end
40
+
37
41
  # Get an Array of the Plot's current Syntaxes.
38
42
  #
39
43
  # @return [Array<Syntax>]
@@ -1,7 +1,7 @@
1
1
  module Gamefic
2
2
  module Query
3
3
  class Base
4
- NEST_REGEXP = / in | on | of | from | inside /
4
+ NEST_REGEXP = / in | on | of | from | inside | from inside /
5
5
 
6
6
  attr_reader :arguments
7
7
 
@@ -31,6 +31,9 @@ module Gamefic
31
31
  # Get a collection of objects that exist in the subject's context and
32
32
  # match the provided token. The result is provided as a Matches object.
33
33
  #
34
+ # @param subject [Entity]
35
+ # @param token [String]
36
+ # @param continued [Boolean]
34
37
  # @return [Gamefic::Query::Matches]
35
38
  def resolve(subject, token, continued: false)
36
39
  available = context_from(subject)
@@ -38,14 +41,13 @@ module Gamefic
38
41
  if continued
39
42
  return Matches.execute(available, token, continued: continued)
40
43
  elsif nested?(token)
41
- drill = denest(available, token)
42
- drill.keep_if{ |e| accept?(e) }
44
+ drill = denest(available, token).select { |e| accept?(e) }
43
45
  return Matches.new(drill, token, '') unless drill.length != 1
44
46
  return Matches.new([], '', token)
45
47
  end
46
- result = available.select{ |e| e.specified?(token) }
47
- result = available.select{ |e| e.specified?(token, fuzzy: true) } if result.empty?
48
- result.keep_if{ |e| accept? e }
48
+ result = available.select { |e| e.specified?(token) }
49
+ result = available.select { |e| e.specified?(token, fuzzy: true) } if result.empty?
50
+ result.keep_if { |e| accept? e }
49
51
  Matches.new(result, (result.empty? ? '' : token), (result.empty? ? token : ''))
50
52
  end
51
53
 
@@ -82,6 +84,7 @@ module Gamefic
82
84
 
83
85
  # Determine whether the specified entity passes the query's arguments.
84
86
  #
87
+ # @param [Entity]
85
88
  # @return [Boolean]
86
89
  def accept?(entity)
87
90
  result = true
@@ -106,6 +109,7 @@ module Gamefic
106
109
  # recursively append its children.
107
110
  # The result will NOT include the original entity itself.
108
111
  #
112
+ # @param [Entity]
109
113
  # @return [Array<Object>]
110
114
  def subquery_accessible entity
111
115
  return [] if entity.nil?
@@ -1,17 +1,38 @@
1
1
  module Gamefic
2
2
  module Query
3
3
  class External < Base
4
- def initialize objects, *args
4
+ # @param container [Plot, Subplot, Array]
5
+ def initialize container, *args
5
6
  super(*args)
6
- @objects = objects
7
+ @container = container
7
8
  end
8
9
 
9
10
  def context_from subject
10
- @objects
11
+ Set.new
12
+ .merge(container_entities)
13
+ .merge(container_subplots_for(@container, subject))
14
+ .to_a
11
15
  end
12
16
 
13
- def accept?(entity)
14
- @objects.include?(entity) && super(entity)
17
+ private
18
+
19
+ # @return [Array<Entity>]
20
+ def container_entities
21
+ if @container.is_a?(World::Entities)
22
+ @container.entities
23
+ elsif @container.is_a?(Enumerable)
24
+ @container
25
+ else
26
+ raise ArgumentError, "Unable to derive entities from #{@container}"
27
+ end
28
+ end
29
+
30
+ # @return [Array<Entity>]
31
+ def container_subplots_for container, subject
32
+ return [] unless container.is_a?(Plot::Host)
33
+ container.subplots_featuring(subject).flat_map do |subplot|
34
+ subplot.entities + container_subplots_for(subplot, subject)
35
+ end
15
36
  end
16
37
  end
17
38
  end
@@ -76,9 +76,14 @@ Gamefic::Scriptable.module_exec do
76
76
  instance = self
77
77
  theater ||= Object.new
78
78
  theater.instance_exec do
79
- define_singleton_method :method_missing do |symbol, *args, **splat, &block|
80
- result = instance.public_send :public_send, symbol, *args, **splat, &block
81
- result
79
+ if RUBY_ENGINE == 'opal' || RUBY_VERSION =~ /^2\.[456]\./
80
+ define_singleton_method :method_missing do |symbol, *args, &block|
81
+ instance.public_send :public_send, symbol, *args, &block
82
+ end
83
+ else
84
+ define_singleton_method :method_missing do |symbol, *args, **splat, &block|
85
+ instance.public_send :public_send, symbol, *args, **splat, &block
86
+ end
82
87
  end
83
88
  end
84
89
  theater.extend Gamefic::Serialize
@@ -1,7 +1,10 @@
1
1
  require 'gamefic/plot'
2
2
 
3
3
  module Gamefic
4
- class Subplot #< Container
4
+ # Subplots are disposable plots that run inside a parent plot. They can be
5
+ # started and concluded at any time during the parent plot's execution.
6
+ #
7
+ class Subplot
5
8
  include World
6
9
  include Scriptable
7
10
  include Gamefic::Serialize
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
- VERSION = '2.2.3'
4
+ VERSION = '2.4.0'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'gamefic/action'
2
4
 
3
5
  module Gamefic
@@ -91,15 +93,29 @@ module Gamefic
91
93
  # @param command [Symbol] An imperative verb for the command
92
94
  # @param queries [Array<Query::Base>] Filters for the command's tokens
93
95
  # @yieldparam [Gamefic::Actor]
94
- def meta(command, *queries, &proc)
95
- playbook.meta command, *queries, &proc
96
+ def meta(command, *queries, &block)
97
+ playbook.meta command, *queries, &block
98
+ end
99
+
100
+ # Add a proc to be evaluated before a character executes an action.
101
+ # When a verb is specified, the proc will only be evaluated if the
102
+ # action's verb matches it.
103
+ #
104
+ # @param verb [Symbol, nil]
105
+ # @yieldparam [Gamefic::Action]
106
+ def before_action verb = nil, &block
107
+ playbook.before_action verb, &block
96
108
  end
109
+ alias validate before_action
97
110
 
98
- # Validate an order before a character can execute its command.
111
+ # Add a proc to be evaluated after a character executes an action.
112
+ # When a verb is specified, the proc will only be evaluated if the
113
+ # action's verb matches it.
99
114
  #
100
- # @yieldparam [Gamefic::Director::Order]
101
- def validate &block
102
- playbook.validate &block
115
+ # @param [Symbol, nil]
116
+ # @yieldparam [Gamefic::Action]
117
+ def after_action verb = nil, &block
118
+ playbook.after_action verb, &block
103
119
  end
104
120
 
105
121
  # Create an alternate Syntax for an Action.
@@ -156,7 +172,7 @@ module Gamefic
156
172
  elsif q.is_a?(Gamefic::Element) || (q.is_a?(Class) && q <= Gamefic::Element)
157
173
  get_default_query.new(q)
158
174
  else
159
- raise ArgumentError.new("Invalid argument for response: #{q}")
175
+ raise ArgumentError, "Invalid argument for response: #{q.inspect}"
160
176
  end
161
177
  end
162
178
  end
@@ -5,24 +5,33 @@ module Gamefic
5
5
  # A collection of rules for performing commands.
6
6
  #
7
7
  class Playbook
8
+ ActionHook = Struct.new(:verb, :block)
9
+
8
10
  # An array of available syntaxes.
9
11
  #
10
12
  # @return [Array<Gamefic::Syntax>]
11
13
  attr_reader :syntaxes
12
14
 
13
- # An array of defined validators.
15
+ # An array of blocks to execute before actions.
16
+ #
17
+ # @return [Array<Proc>]
18
+ attr_reader :before_actions
19
+
20
+ # An array of blocks to execute after actions.
14
21
  #
15
22
  # @return [Array<Proc>]
16
- attr_reader :validators
23
+ attr_reader :after_actions
17
24
 
18
25
  # @param commands [Hash]
19
26
  # @param syntaxes [Array<Syntax>, Set<Syntax>]
20
- # @param validators [Array]
21
- def initialize commands: {}, syntaxes: [], validators: []
27
+ # @param before_actions [Array]
28
+ # @param after_actions [Array]
29
+ def initialize commands: {}, syntaxes: [], before_actions: [], after_actions: []
22
30
  @commands = commands
23
31
  @syntax_set = syntaxes.to_set
24
32
  sort_syntaxes
25
- @validators = validators
33
+ @before_actions = before_actions
34
+ @after_actions = after_actions
26
35
  end
27
36
 
28
37
  # An array of available actions.
@@ -39,10 +48,25 @@ module Gamefic
39
48
  @commands.keys
40
49
  end
41
50
 
42
- # Add a block that determines whether an action can be executed.
51
+ # Add a proc to be evaluated before a character executes an action.
52
+ # When a verb is specified, the proc will only be evaluated if the
53
+ # action's verb matches it.
54
+ #
55
+ # @param verb [Symbol, nil]
56
+ # @yieldparam [Gamefic::Action]
57
+ def before_action verb = nil, &block
58
+ @before_actions.push ActionHook.new(verb, block)
59
+ end
60
+ alias validate before_action
61
+
62
+ # Add a proc to be evaluated after a character executes an action.
63
+ # When a verb is specified, the proc will only be evaluated if the
64
+ # action's verb matches it.
43
65
  #
44
- def validate &block
45
- @validators.push block
66
+ # @param verb [Symbol, nil]
67
+ # @yieldparam [Gamefic::Action]
68
+ def after_action verb = nil, &block
69
+ @after_actions.push ActionHook.new(verb, block)
46
70
  end
47
71
 
48
72
  # Get an Array of all Actions associated with the specified verb.
@@ -1,6 +1,9 @@
1
1
  module Gamefic
2
2
  module World
3
3
  module Scenes
4
+ include Commands
5
+ include Players
6
+
4
7
  # @return [Class<Gamefic::Scene::Activity>]
5
8
  def default_scene
6
9
  @default_scene ||= Scene::Activity
@@ -29,13 +32,12 @@ module Gamefic
29
32
  # This method is typically called by the Engine that manages game execution.
30
33
  #
31
34
  # @param [Gamefic::Actor]
35
+ # @return [void]
32
36
  def introduce(player)
33
37
  player.playbooks.push playbook unless player.playbooks.include?(playbook)
34
38
  player.cue default_scene
35
39
  players.push player
36
- @introduction.call(player) unless @introduction.nil?
37
- # @todo Find a better way to persist player characters
38
- # Gamefic::Index.stick
40
+ @introduction&.call(player)
39
41
  end
40
42
 
41
43
  # Create a multiple-choice scene.
@@ -53,7 +55,7 @@ module Gamefic
53
55
  # @yieldparam [Gamefic::Scene::MultipleChoice]
54
56
  # @return [Class<Gamefic::Scene::MultipleChoice>]
55
57
  def multiple_choice *choices, &block
56
- s = Scene::MultipleChoice.subclass do |actor, scene|
58
+ s = Scene::MultipleChoice.subclass do |_actor, scene|
57
59
  scene.options.concat choices
58
60
  scene.on_finish &block
59
61
  end
@@ -73,12 +75,12 @@ module Gamefic
73
75
  # end
74
76
  # end
75
77
  #
76
- # @param prompt [String]
78
+ # @param prompt [String, nil]
77
79
  # @yieldparam [Gamefic::Actor]
78
80
  # @yieldparam [Gamefic::Scene::YesOrNo]
79
81
  # @return [Class<Gamefic::Scene::YesOrNo>]
80
82
  def yes_or_no prompt = nil, &block
81
- s = Scene::YesOrNo.subclass do |actor, scene|
83
+ s = Scene::YesOrNo.subclass do |_actor, scene|
82
84
  scene.prompt = prompt
83
85
  scene.on_finish &block
84
86
  end
@@ -98,7 +100,7 @@ module Gamefic
98
100
  # @yieldparam [Gamefic::Scene::Base]
99
101
  # @return [Class<Gamefic::Scene::Base>]
100
102
  def question prompt = 'What is your answer?', &block
101
- s = Scene::Base.subclass do |actor, scene|
103
+ s = Scene::Base.subclass do |_actor, scene|
102
104
  scene.prompt = prompt
103
105
  scene.on_finish &block
104
106
  end
@@ -116,13 +118,13 @@ module Gamefic
116
118
  # actor.prepare default_scene
117
119
  # end
118
120
  #
119
- # @param prompt [String] The text to display when prompting the user to continue.
121
+ # @param prompt [String, nil] The text to display when prompting the user to continue
120
122
  # @yieldparam [Gamefic::Actor]
121
123
  # @return [Class<Gamefic::Scene::Pause>]
122
124
  def pause prompt = nil, &block
123
125
  s = Scene::Pause.subclass do |actor, scene|
124
126
  scene.prompt = prompt unless prompt.nil?
125
- block.call(actor, scene) unless block.nil?
127
+ block&.call(actor, scene)
126
128
  end
127
129
  scene_classes.push s
128
130
  s
@@ -163,7 +165,7 @@ module Gamefic
163
165
  # end
164
166
  # end
165
167
  #
166
- # @param cls [Class] The class of scene to be instantiated.
168
+ # @param cls [Class<Scene::Base>] The class of scene to be instantiated.
167
169
  # @yieldparam [Gamefic::Actor]
168
170
  # @return [Class<Gamefic::Scene::Base>]
169
171
  def custom cls = Scene::Base, &block
@@ -208,10 +210,10 @@ module Gamefic
208
210
  # @return [Class<Gamefic::Scene::MultipleScene>]
209
211
  def multiple_scene map = {}, &block
210
212
  s = Scene::MultipleScene.subclass do |actor, scene|
211
- map.each_pair { |k, v|
213
+ map.each_pair do |k, v|
212
214
  scene.map k, v
213
- }
214
- block.call actor, scene unless block.nil?
215
+ end
216
+ block&.call actor, scene
215
217
  end
216
218
  scene_classes.push s
217
219
  s
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gamefic
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.3
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fred Snyder
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-09 00:00:00.000000000 Z
11
+ date: 2023-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -70,6 +70,7 @@ executables: []
70
70
  extensions: []
71
71
  extra_rdoc_files: []
72
72
  files:
73
+ - ".github/workflows/rspec.yml"
73
74
  - ".gitignore"
74
75
  - ".rspec"
75
76
  - ".rubocop.yml"
@@ -142,7 +143,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
143
  requirements:
143
144
  - - ">="
144
145
  - !ruby/object:Gem::Version
145
- version: 2.1.0
146
+ version: 2.3.0
146
147
  required_rubygems_version: !ruby/object:Gem::Requirement
147
148
  requirements:
148
149
  - - ">="