gamefic 3.0.0 → 3.2.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: 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