gamefic 2.2.3 → 2.4.0

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