gamefic 3.0.0 → 3.2.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: 6647dc3319080277826fecd19b8466cfbe82a42f4f4c68aeb89fa74bd0de4319
4
- data.tar.gz: 0dc0680cb42b981d6e93c12d03e95b4ffcce0a98acb0678595723ab2d5ae7db9
3
+ metadata.gz: ac4f50caef9bd101bf90447e3ab289b05a29470e8f722ad391f7f0c9d8c4a921
4
+ data.tar.gz: 926b7d40dc87036e3759a1a464d3db3c82187da4f829731d0d82b053ac5e52a4
5
5
  SHA512:
6
- metadata.gz: 1ec19185367b63b0ebe352e620af403e0aef00bdd7afac7135801551568ef5c35c923b45092cb4d781cd8458bba71776b042efe9837a0e9d2f144141b6b8d606
7
- data.tar.gz: 300d2c30b05993ee4689f14ce8daeeec3585bfdf1cd7636ec6a931bfc568e8227a97771fdf6d20ec859080f3792b914a29ba1fc6a7246c9f9ef9eb563d81920c
6
+ metadata.gz: 6a4b426b4703a29717dbcc59bfee2e3f5d3b1b2ceeb6121741f52111e5fe2d8165e0946f8c3a085f1a3c4094edab11e2966f751dbc3cfc1541809ebc3a9ad970
7
+ data.tar.gz: ba3f3e82bd772ae392ffb23b056b62de2c1b5bebb0e810fea81bd59449d3e64c8c848f748803ac36f155ad3a2891b0e42d76ca48e4e74239a776670f19fdcc0e
data/CHANGELOG.md CHANGED
@@ -1,4 +1,17 @@
1
- ## 3.0.0
1
+ ## 3.2.0 - April 9, 2024
2
+ - Bug fix for marshal of structs in Opal
3
+ - Add last_input and last_prompt at start of take
4
+
5
+ ## 3.1.0 - April 8, 2024
6
+ - Dispatcher prioritizes strict token matches
7
+ - Scanner builds commands
8
+ - Tokenize expressions and execute commands
9
+ - Delete concluded subplots last in Plot#ready
10
+ - Fix plot conclusion check after subplots conclude
11
+ - Correct contexts for conclude and output blocks
12
+ - Reinstate Active#last_input
13
+
14
+ ## 3.0.0 - January 27, 2024
2
15
  - Instantiate subplots from snapshots
3
16
  - Split Action into Response and Action
4
17
  - Logging
@@ -13,8 +13,10 @@ module Gamefic
13
13
  # action's performance.
14
14
  #
15
15
  class Hook
16
+ # @param [Array<Symbol>]
16
17
  attr_reader :verbs
17
18
 
19
+ # @param [Proc]
18
20
  attr_reader :block
19
21
 
20
22
  def initialize *verbs, &block
@@ -45,11 +47,13 @@ module Gamefic
45
47
  @response = response
46
48
  end
47
49
 
50
+ # @return [self]
48
51
  def execute
49
- return if cancelled?
52
+ return self if cancelled? || executed?
50
53
 
51
54
  @executed = true
52
55
  response.execute actor, *arguments
56
+ self
53
57
  end
54
58
 
55
59
  # True if the response has been executed. False typically means that the
@@ -75,6 +79,10 @@ module Gamefic
75
79
  response.verb
76
80
  end
77
81
 
82
+ def narrative
83
+ response.narrative
84
+ end
85
+
78
86
  def meta?
79
87
  response.meta?
80
88
  end
@@ -47,9 +47,14 @@ module Gamefic
47
47
  narratives.one?
48
48
  end
49
49
 
50
- # @param name [Symbol]
51
- def conclusion? name
52
- select_scene(name).conclusion?
50
+ def syntaxes
51
+ rulebooks.flat_map(&:syntaxes)
52
+ end
53
+
54
+ def responses_for(*verbs)
55
+ rulebooks.to_a
56
+ .reverse
57
+ .flat_map { |rb| rb.responses_for(*verbs) }
53
58
  end
54
59
 
55
60
  # @param name [Symbol]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  module Active
3
5
  # A module for active entities that provides a default Messenger with
@@ -16,7 +16,7 @@ module Gamefic
16
16
 
17
17
  # @param actor [Active]
18
18
  # @param cue [Active::Cue]
19
- # @param props [Props::Default]
19
+ # @param props [Props::Default, nil]
20
20
  def initialize actor, cue, props = nil
21
21
  @actor = actor
22
22
  @cue = cue
@@ -31,12 +31,11 @@ module Gamefic
31
31
 
32
32
  # @return [Props::Default]
33
33
  def start
34
- actor.output[:scene] = scene.to_hash
35
34
  scene.run_start_blocks actor, props
36
35
  scene.start actor, props
37
36
  # @todo See if this can be handled better
38
- actor.epic.rulebooks.each { |rlbk| rlbk.run_player_output_blocks actor, actor.output }
39
- actor.output.merge!({
37
+ actor.epic.rulebooks.each { |rlbk| rlbk.run_player_output_blocks actor, props.output }
38
+ props.output.merge!({
40
39
  messages: actor.messages,
41
40
  queue: actor.queue
42
41
  })
@@ -47,7 +46,6 @@ module Gamefic
47
46
  def finish
48
47
  actor.flush
49
48
  scene.finish(actor, props)
50
- actor.output.replace(last_prompt: props.prompt, last_input: props.input)
51
49
  scene.run_finish_blocks actor, props
52
50
  end
53
51
 
@@ -21,6 +21,9 @@ module Gamefic
21
21
  # @return [Active::Cue, nil]
22
22
  attr_reader :next_cue
23
23
 
24
+ # @return [String, nil]
25
+ attr_reader :last_input
26
+
24
27
  # @return [Symbol, nil]
25
28
  def next_scene
26
29
  next_cue&.scene
@@ -48,12 +51,22 @@ module Gamefic
48
51
  @queue ||= []
49
52
  end
50
53
 
51
- # A hash of data that will be sent to the user. The output is typically
52
- # sent after a scene has started and before the user is prompted for input.
54
+ # Data that will be sent to the user. The output is typically sent after a
55
+ # scene has started and before the user is prompted for input.
56
+ #
57
+ # The output object attached to the actor is always frozen. Authors should
58
+ # use on_player_output blocks to modify output to be sent to the user.
53
59
  #
54
- # @return [Hash]
60
+ # @return [Props::Output]
55
61
  def output
56
- @output ||= {}
62
+ @output ||= Props::Output.new.freeze
63
+ end
64
+
65
+ # The output from the previous turn.
66
+ #
67
+ # @return [Props::Output]
68
+ def last_output
69
+ @last_output ||= output
57
70
  end
58
71
 
59
72
  # Perform a command.
@@ -65,11 +78,10 @@ module Gamefic
65
78
  # character.perform "take the key"
66
79
  #
67
80
  # @param command [String]
68
- # @return [void]
81
+ # @return [Action, nil]
69
82
  def perform(command)
70
83
  dispatchers.push Dispatcher.dispatch(self, command)
71
- dispatchers.last.execute
72
- dispatchers.pop
84
+ dispatchers.last.execute.tap { dispatchers.pop }
73
85
  end
74
86
 
75
87
  # Quietly perform a command.
@@ -95,11 +107,10 @@ module Gamefic
95
107
  #
96
108
  # @param verb [Symbol]
97
109
  # @param params [Array]
98
- # @return [Gamefic::Action]
110
+ # @return [Action, nil]
99
111
  def execute(verb, *params)
100
112
  dispatchers.push Dispatcher.dispatch_from_params(self, verb, params)
101
- dispatchers.last.execute
102
- dispatchers.pop
113
+ dispatchers.last.execute.tap { dispatchers.pop }
103
114
  end
104
115
 
105
116
  # Proceed to the next Action in the current stack.
@@ -125,9 +136,9 @@ module Gamefic
125
136
  # end
126
137
  # end
127
138
  #
128
- # @return [void]
139
+ # @return [Action, nil]
129
140
  def proceed
130
- dispatchers.last&.proceed&.execute
141
+ dispatchers.last&.proceed
131
142
  end
132
143
 
133
144
  # Cue a scene to start in the next turn.
@@ -146,17 +157,24 @@ module Gamefic
146
157
  end
147
158
  alias prepare cue
148
159
 
160
+ # @return [void]
149
161
  def start_take
150
162
  ensure_cue
151
163
  @last_cue = @next_cue
152
164
  cue :default_scene
153
165
  @props = Take.start(self, @last_cue)
166
+ @last_output = self.output
167
+ @props.output[:last_prompt] = @last_output.prompt
168
+ @props.output[:last_input] = @last_input
169
+ @output = @props.output.dup.freeze
154
170
  end
155
171
 
172
+ # @return [void]
156
173
  def finish_take
157
174
  return unless @last_cue
158
175
 
159
176
  Take.finish(self, @last_cue, @props)
177
+ @last_input = @props.input
160
178
  end
161
179
 
162
180
  # Restart the scene from the most recent cue.
@@ -175,6 +193,7 @@ module Gamefic
175
193
  #
176
194
  # @param new_scene [Symbol]
177
195
  # @oaram context [Hash] Additional scene data
196
+ # @return [Cue]
178
197
  def conclude scene, **context
179
198
  cue scene, **context
180
199
  available = epic.select_scene(scene)
@@ -186,7 +205,7 @@ module Gamefic
186
205
  # True if the actor is ready to leave the game.
187
206
  #
188
207
  def concluding?
189
- epic.empty? || (@last_cue && epic.conclusion?(@last_cue.scene))
208
+ epic.empty? || @props&.scene&.fetch(:type) == 'Conclusion'
190
209
  end
191
210
 
192
211
  def accessible?
@@ -1,31 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gamefic
4
- # A decomposition of a text-based command into its verb and arguments.
5
- #
6
- # Commands are typically derived from tokenization against syntaxes.
4
+ # A concrete representation of an input as a verb and an array of arguments.
7
5
  #
8
6
  class Command
9
7
  # @return [Symbol]
10
8
  attr_reader :verb
11
9
 
12
- # @return [Array<String>]
10
+ # @return [Array<Array<Entity>, Entity, String>]
13
11
  attr_reader :arguments
14
12
 
13
+ # @param verb [Symbol]
14
+ # @param arguments [Array<Array<Entity>, Entity, String>]
15
15
  def initialize verb, arguments
16
16
  @verb = verb
17
17
  @arguments = arguments
18
18
  end
19
-
20
- # Compare two syntaxes for the purpose of ordering them by relevance while
21
- # dispatching.
22
- #
23
- def compare other
24
- if verb == other.verb
25
- other.arguments.compact.length <=> arguments.compact.length
26
- else
27
- (other.verb ? 1 : 0) <=> (verb ? 1 : 0)
28
- end
29
- end
30
19
  end
31
20
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ # A function module for creating commands from expressions.
5
+ #
6
+ module Composer
7
+ # Create a command from the first expression that matches a response.
8
+ #
9
+ # @param actor [Actor]
10
+ # @param expressions [Array<Expression>]
11
+ # @return [Command]
12
+ def self.compose actor, expressions
13
+ %i[strict fuzzy].each do |method|
14
+ result = match_expressions_to_response actor, expressions, method
15
+ return result if result
16
+ end
17
+ Command.new(nil, [])
18
+ end
19
+
20
+ class << self
21
+ private
22
+
23
+ def match_expressions_to_response actor, expressions, method
24
+ expressions.each do |expression|
25
+ result = match_response_arguments actor, expression, method
26
+ return result if result
27
+ end
28
+ nil
29
+ end
30
+
31
+ def match_response_arguments actor, expression, method
32
+ actor.epic.responses_for(expression.verb).each do |response|
33
+ next unless response.queries.length >= expression.tokens.length
34
+
35
+ result = match_query_arguments(actor, expression, response, method)
36
+ return result if result
37
+ end
38
+ nil
39
+ end
40
+
41
+ def match_query_arguments actor, expression, response, method
42
+ remainder = response.verb ? '' : expression.verb.to_s
43
+ arguments = []
44
+ response.queries.each_with_index do |query, idx|
45
+ result = Scanner.send(method, query.select(actor), "#{remainder} #{expression.tokens[idx]}".strip)
46
+ break unless validate_result_from_query(result, query)
47
+
48
+ if query.ambiguous?
49
+ arguments.push result.matched
50
+ else
51
+ arguments.push result.matched.first
52
+ end
53
+ remainder = result.remainder
54
+ end
55
+
56
+ return nil if arguments.length != response.queries.length || remainder != ''
57
+
58
+ Command.new(response.verb, arguments)
59
+ end
60
+
61
+ # @param result [Scanner::Result]
62
+ # @param query [Query::Base]
63
+ def validate_result_from_query result, query
64
+ return false if result.matched.empty?
65
+
66
+ result.matched.length == 1 || query.ambiguous?
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,63 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gamefic
4
- # The action selector for character commands.
4
+ # The action executor for character commands.
5
5
  #
6
6
  class Dispatcher
7
7
  # @param actor [Actor]
8
- # @param commands [Array<Command>]
9
- # @param responses [Array<Response>]
10
- def initialize actor, commands = [], responses = []
8
+ # @param command [Command]
9
+ def initialize actor, command
11
10
  @actor = actor
12
- @commands = commands
13
- @responses = responses
11
+ @command = command
14
12
  @executed = false
13
+ @finalized = false
15
14
  end
16
15
 
17
16
  # Run the dispatcher.
18
17
  #
18
+ # @return [Action, nil]
19
19
  def execute
20
20
  return if @executed
21
21
 
22
- action = proceed
22
+ @executed = true
23
+ action = next_action
23
24
  return unless action
24
25
 
25
- @executed = action.arguments
26
26
  run_before_action_hooks action
27
27
  return if action.cancelled?
28
28
 
29
29
  action.execute
30
30
  run_after_action_hooks action
31
+ action
31
32
  end
32
33
 
33
- # Get the next executable action.
34
+ # Execute the next available action.
35
+ #
36
+ # Actors should run #execute first.
34
37
  #
35
38
  # @return [Action, nil]
36
39
  def proceed
37
- while (response = responses.shift)
38
- commands.each do |cmd|
39
- action = response.attempt(actor, cmd)
40
- next unless action && arguments_match?(action.arguments)
40
+ return unless @executed
41
41
 
42
- return action
43
- end
44
- end
45
- nil # Without this, return value in Opal is undefined
42
+ next_action&.execute
46
43
  end
47
44
 
48
45
  # @param actor [Active]
49
46
  # @param input [String]
50
47
  # @return [Dispatcher]
51
48
  def self.dispatch actor, input
52
- commands = Syntax.tokenize(input, actor.epic.rulebooks.flat_map(&:syntaxes))
53
- verbs = commands.map(&:verb).uniq
54
- responses = actor.epic
55
- .rulebooks
56
- .to_a
57
- .reverse
58
- .flat_map { |pb| pb.responses_for(*verbs) }
59
- .reject(&:hidden?)
60
- new(actor, commands, responses)
49
+ expressions = Syntax.tokenize(input, actor.epic.syntaxes)
50
+ command = Composer.compose(actor, expressions)
51
+ new(actor, command)
61
52
  end
62
53
 
63
54
  # @param actor [Active]
@@ -66,12 +57,7 @@ module Gamefic
66
57
  # @return [Dispatcher]
67
58
  def self.dispatch_from_params actor, verb, params
68
59
  command = Command.new(verb, params)
69
- responses = actor.epic
70
- .rulebooks
71
- .to_a
72
- .reverse
73
- .flat_map { |pb| pb.responses_for(verb) }
74
- new(actor, [command], responses)
60
+ new(actor, command)
75
61
  end
76
62
 
77
63
  protected
@@ -79,27 +65,48 @@ module Gamefic
79
65
  # @return [Actor]
80
66
  attr_reader :actor
81
67
 
82
- # @return [Array<Command>]
83
- attr_reader :commands
68
+ # @return [Command]
69
+ attr_reader :command
84
70
 
85
71
  # @return [Array<Response>]
86
- attr_reader :responses
72
+ def responses
73
+ @responses ||= actor.epic.responses_for(command.verb)
74
+ end
87
75
 
88
76
  private
89
77
 
90
- # After the first action gets selected, subsequent actions need to use the
91
- # same arguments.
92
- #
93
- def arguments_match? arguments
94
- !@executed || arguments == @executed
78
+ # @return [Action, nil]
79
+ def next_action
80
+ while (response = responses.shift)
81
+ next if response.queries.length < @command.arguments.length
82
+
83
+ return Action.new(actor, @command.arguments, response) if response.accept?(actor, @command)
84
+ end
85
+ finalize
95
86
  end
96
87
 
88
+ # @return [void]
97
89
  def run_before_action_hooks action
98
90
  actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_before_actions action }
99
91
  end
100
92
 
93
+ # @return [void]
101
94
  def run_after_action_hooks action
102
95
  actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_after_actions action }
103
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
+ end
104
111
  end
105
112
  end
@@ -9,7 +9,6 @@ module Gamefic
9
9
  class Entity
10
10
  include Describable
11
11
  include Node
12
- # include Messaging
13
12
 
14
13
  def initialize **args
15
14
  klass = self.class
@@ -57,14 +56,13 @@ module Gamefic
57
56
  end
58
57
 
59
58
  class << self
60
- # Set or update the default values for new instances.
59
+ # Set or update the default attributes for new instances.
61
60
  #
62
- # @param attrs [Hash] The attributes to be merged into the defaults.
63
- def set_default attrs = {}
61
+ def set_default **attrs
64
62
  default_attributes.merge! attrs
65
63
  end
66
64
 
67
- # A hash of default values for attributes when creating an instance.
65
+ # A hash of default attributes when creating an instance.
68
66
  #
69
67
  # @return [Hash]
70
68
  def default_attributes
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ # A tokenization of an input from available syntaxes.
5
+ #
6
+ class Expression
7
+ # @return [Symbol]
8
+ attr_reader :verb
9
+
10
+ # @return [Array<String>]
11
+ attr_reader :tokens
12
+
13
+ # @param verb [Symbol, nil]
14
+ # @param tokens [Array<String>]
15
+ def initialize verb, tokens
16
+ @verb = verb
17
+ @tokens = tokens
18
+ end
19
+
20
+ # Compare two syntaxes for the purpose of ordering them by relevance while
21
+ # dispatching.
22
+ #
23
+ def compare other
24
+ if verb == other.verb
25
+ other.tokens.compact.length <=> tokens.compact.length
26
+ else
27
+ (other.verb ? 1 : 0) <=> (verb ? 1 : 0)
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/gamefic/plot.rb CHANGED
@@ -9,8 +9,8 @@ module Gamefic
9
9
  super
10
10
  subplots.each(&:ready)
11
11
  players.each(&:start_take)
12
- subplots.delete_if(&:concluding?)
13
12
  players.select(&:concluding?).each { |plyr| rulebook.run_player_conclude_blocks plyr }
13
+ subplots.delete_if(&:concluding?)
14
14
  end
15
15
 
16
16
  def update
@@ -25,17 +25,23 @@ module Gamefic
25
25
  attr_reader :context
26
26
  alias data context
27
27
 
28
- # @param scene [Scene, nil]
28
+ # @return [Hash]
29
+ attr_reader :scene
30
+
31
+ # @param scene [Scene]
29
32
  # @param context [Hash]
30
- def initialize name, type, **context
31
- @scene_name = name
32
- @scene_type = type
33
+ def initialize scene, **context
34
+ @scene = { name: scene.name, type: scene.type }
33
35
  @context = context
34
36
  end
35
37
 
36
38
  def prompt
37
39
  @prompt ||= '>'
38
40
  end
41
+
42
+ def output
43
+ @output ||= Props::Output.new
44
+ end
39
45
  end
40
46
  end
41
47
  end