gamefic 4.1.1 → 4.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: 0e888ff6feb58de6f23f2727cc23fed710935f1876ea393ee84c19c2252166c8
4
- data.tar.gz: 314a944a945ffb5c51cdc37b842452653408c5a902722b91585cb036b82ee8b7
3
+ metadata.gz: 923f92309b7fcac0e2ed6aa4e691e2feea39521f15f19c7b7a560ddb7583b365
4
+ data.tar.gz: 858516d0d245300605612517d833e858b84b8a40473f6e9bf7d8670c5589cd77
5
5
  SHA512:
6
- metadata.gz: aa90ce26213fbeec901719fcb1d099c14f627bb5c1ac94eefaa8c883c06f1d48bf6a269c4f259d67d6b63d35d6855a67cf5e3b0a22f3be23b3dde955afd47a3d
7
- data.tar.gz: e237330cd06ca31b3b53ae00b2d774aa9f5b1ab6a307efcc179a07c77928d0e409f3c5237ec94739583c1b835cb8600d81c7c0b2b33ceaa4ced2f3b331f8a081
6
+ metadata.gz: d3d3aa37745380138eb41ed6bee89231e96b385a1c5b8fffc42eb90bb23e9fdcb5d9ea0466fe7f5e1b2d9d459609ec002c3f80d6d6ab324658cbdea4fce1aee2
7
+ data.tar.gz: 16a1c623c7fc6173f306f43ca080b668b72fdef3700469a6b421abcf88df04c405137f1458727754fce9706107502d7bc0f1dd4cf6c91115784e14de38a6af5e
@@ -22,7 +22,7 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  strategy:
24
24
  matrix:
25
- ruby-version: ['2.7', '3.0', '3.1', '3.3']
25
+ ruby-version: ['2.7', '3.0', '3.1', '3.3', '3.4']
26
26
 
27
27
  steps:
28
28
  - uses: actions/checkout@v3
data/.rubocop.yml CHANGED
@@ -1,4 +1,4 @@
1
- Metrics/LineLength:
1
+ Layout/LineLength:
2
2
  Description: 'Limit lines to 80 characters.'
3
3
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
4
4
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 4.2.0 - November 27, 2025
2
+ - Syntaxes support variable (piped) words
3
+ - Props::MultipleChoice#index_of
4
+
5
+ ## 4.1.2 - May 25, 2025
6
+ - Seed unreferenced entities
7
+ - Ensure seed uniqueness
8
+ - Add base64 dependency
9
+
1
10
  ## 4.1.1 - March 2, 2025
2
11
  - Warn for duplicate chapters
3
12
  - YARD documentation
data/Rakefile CHANGED
@@ -15,7 +15,8 @@ task :default => :spec
15
15
 
16
16
  Opal::RSpec::RakeTask.new(:opal) do |_, config|
17
17
  Opal.append_path File.join(__dir__, 'lib')
18
+ Opal.append_path File.join(__dir__, 'spec', 'fixtures', 'modular')
18
19
  config.default_path = 'spec'
19
20
  config.pattern = 'spec/**/*_spec.rb'
20
- config.requires = ['opal_helper']
21
+ config.requires = ['opal_helper', 'modular_test_script', 'modular_test_plot']
21
22
  end
data/gamefic.gemspec CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
21
21
 
22
22
  s.required_ruby_version = '>= 2.7.0'
23
23
 
24
+ s.add_runtime_dependency 'base64', '~> 0.1'
24
25
  s.add_runtime_dependency 'yard-solargraph', '~> 0.1'
25
26
 
26
27
  s.add_development_dependency 'opal', '~> 1.7'
@@ -66,6 +66,8 @@ module Gamefic
66
66
  narrative_set.flat_map(&:after_commands)
67
67
  end
68
68
 
69
+ # @sg-ignore Type checker has trouble reconciling return type of `Set#each`
70
+ # with unresolved `generic<R>` of `Enumerable#each`
69
71
  def each(&block)
70
72
  narrative_set.each(&block)
71
73
  end
@@ -22,8 +22,10 @@ module Gamefic
22
22
  end
23
23
  end
24
24
 
25
+ # @return [Narrative]
25
26
  attr_reader :narrative
26
27
 
28
+ # @return [Proc]
27
29
  attr_reader :code
28
30
 
29
31
  # @param narrative [Narrative]
@@ -44,6 +44,7 @@ module Gamefic
44
44
 
45
45
  # Subclasses can override this method to handle additional configuration.
46
46
  #
47
+ # @return [void]
47
48
  def configure; end
48
49
 
49
50
  class << self
@@ -142,10 +142,10 @@ module Gamefic
142
142
  #
143
143
  # @param text [String]
144
144
  def description=(text)
145
- @description = (text if text != (format(Describable.default_description, name: definitely, Name: definitely.capitalize_first)))
145
+ @description = (text if text != format(Describable.default_description, name: definitely, Name: definitely.capitalize_first))
146
146
  end
147
147
 
148
- def synonyms= text
148
+ def synonyms=(text)
149
149
  @synonyms = text
150
150
  end
151
151
 
@@ -62,7 +62,7 @@ module Gamefic
62
62
  #
63
63
  # In the base Narrative class, this method runs all applicable player
64
64
  # conclude blocks and the narrative's own conclude blocks.
65
- #
65
+ #
66
66
  # @return [void]
67
67
  def turn
68
68
  players.select(&:concluding?).each { |plyr| player_conclude_blocks.each { |blk| blk[plyr] } }
@@ -77,6 +77,7 @@ module Gamefic
77
77
  # @param snapshot [String]
78
78
  # @return [self]
79
79
  def self.restore(snapshot)
80
+ # @sg-ignore
80
81
  Marshal.load(snapshot)
81
82
  end
82
83
 
@@ -14,16 +14,16 @@ module Gamefic
14
14
 
15
15
  # Cast a player character in the plot.
16
16
  #
17
- # @param character [Actor]
18
- # @return [Actor]
17
+ # @param character [Actor, Active]
18
+ # @return [Actor, Active]
19
19
  def cast(character = plot.introduce)
20
20
  plot.cast character
21
21
  end
22
22
 
23
23
  # Uncast a player character from the plot.
24
24
  #
25
- # @param character [Actor]
26
- # @return [Actor]
25
+ # @param character [Actor, Active]
26
+ # @return [Actor, Active]
27
27
  def uncast(character)
28
28
  plot.uncast character
29
29
  end
data/lib/gamefic/order.rb CHANGED
@@ -20,9 +20,9 @@ module Gamefic
20
20
  def to_actions
21
21
  Action.sort(
22
22
  actor.narratives
23
- .responses_for(verb)
24
- .map { |response| match_arguments(response) }
25
- .compact
23
+ .responses_for(verb)
24
+ .map { |response| match_arguments(response) }
25
+ .compact
26
26
  )
27
27
  end
28
28
 
@@ -5,9 +5,6 @@ module Gamefic
5
5
  # Props for MultipleChoice scenes.
6
6
  #
7
7
  class MultipleChoice < Default
8
- # A message to send the player for an invalid choice. A formatting
9
- # token named %<input>s can be used to inject the user input.
10
- #
11
8
  # @return [String]
12
9
  attr_writer :invalid_message
13
10
 
@@ -18,6 +15,13 @@ module Gamefic
18
15
  @options ||= []
19
16
  end
20
17
 
18
+ # A message to send the player for an invalid choice. A formatting
19
+ # token named `%<input>s` can be used to inject the user input.
20
+ #
21
+ # @example
22
+ # props.invalid_message = '"%<input>s" is not a valid choice.'
23
+ #
24
+ # @return [String]
21
25
  def invalid_message
22
26
  @invalid_message ||= '"%<input>s" is not a valid choice.'
23
27
  end
@@ -28,7 +32,7 @@ module Gamefic
28
32
  def index
29
33
  return nil unless input
30
34
 
31
- @index ||= index_by_number || index_by_text
35
+ @index ||= index_of(input)
32
36
  end
33
37
 
34
38
  # The one-based index of the selected option.
@@ -53,15 +57,36 @@ module Gamefic
53
57
  !!index
54
58
  end
55
59
 
60
+ # Get the index of an option using input criteria, e.g., a one-based
61
+ # number or the text of the option. The return value is the option's
62
+ # zero-based index or nil.
63
+ #
64
+ # @example
65
+ # props = Gamefic::Props::MultipleChoice.new
66
+ # props.options.push 'First choice', 'Second choice'
67
+ #
68
+ # props.index_of(1) # => 0
69
+ # props.index_of('Second choice') # => 1
70
+ #
71
+ # @param option [String, Integer]
72
+ # @return [Integer, nil]
73
+ def index_of(option)
74
+ index_by_number(option) || index_by_text(option)
75
+ end
76
+
56
77
  private
57
78
 
58
- def index_by_number
59
- return input.to_i - 1 if input.match(/^\d+$/) && options[input.to_i - 1]
79
+ # @param [String, Integer]
80
+ # @return [Integer, nil]
81
+ def index_by_number(input)
82
+ return input.to_i - 1 if input.to_s.match(/^\d+$/) && options[input.to_i - 1]
60
83
 
61
84
  nil
62
85
  end
63
86
 
64
- def index_by_text
87
+ # @param [String]
88
+ # @return [Integer, nil]
89
+ def index_by_text(input)
65
90
  options.find_index { |opt| opt.casecmp?(input) }
66
91
  end
67
92
  end
@@ -7,7 +7,7 @@ module Gamefic
7
7
  class MultiplePartial < MultipleChoice
8
8
  private
9
9
 
10
- def index_by_text
10
+ def index_by_text(input)
11
11
  matches = options.map.with_index { |text, idx| next idx if text.downcase.start_with?(input.downcase) }.compact
12
12
  matches.first if matches.one?
13
13
  end
@@ -77,7 +77,7 @@ module Gamefic
77
77
  # gets sorted in descending order of their responses' overall precision,
78
78
  # so the action with the highest precision gets attempted first.
79
79
  #
80
- # @return [Integer]
80
+ # @return [::Integer]
81
81
  def precision
82
82
  @precision ||= calculate_precision
83
83
  end
@@ -18,7 +18,7 @@ module Gamefic
18
18
  # @param selection [Array<Entity>]
19
19
  # @param token [String]
20
20
  # @param use [Array<Class<Scanner::Base>>]
21
- # @return [Result]
21
+ # @return [Result, nil]
22
22
  def self.scan(selection, token, use = processors)
23
23
  result = nil
24
24
  use.each do |processor|
@@ -12,8 +12,8 @@ module Gamefic
12
12
  attr_reader :actor, :narrative, :props, :context
13
13
 
14
14
  # @param actor [Actor]
15
- # @param narrative [Narrative]
16
- # @param props [Props::Default]
15
+ # @param narrative [Narrative, nil]
16
+ # @param props [Props::Default, nil]
17
17
  def initialize(actor, narrative = nil, props = nil, **context)
18
18
  @actor = actor
19
19
  @narrative = narrative
@@ -11,11 +11,13 @@ module Gamefic
11
11
  # @return [Scene::Conclusion]
12
12
  attr_reader :default_conclusion
13
13
 
14
+ # @param [Scene::Base]
14
15
  def select_default_scene(klass)
15
16
  scene_classes.add klass
16
17
  @default_scene = klass
17
18
  end
18
19
 
20
+ # @param [Scene::Conclusion]
19
21
  def select_default_conclusion(klass)
20
22
  scene_classes.add klass
21
23
  @default_conclusion = klass
@@ -61,7 +63,8 @@ module Gamefic
61
63
  #
62
64
  # @yieldparam [Gamefic::Actor]
63
65
  # @yieldparam [Props::Default]
64
- # @return [Symbol]
66
+ # @yieldreceiver [Object<self>]
67
+ # @return [void]
65
68
  def introduction(&start)
66
69
  introductions.push start
67
70
  end
@@ -88,7 +91,7 @@ module Gamefic
88
91
  # @yieldreceiver [Class<Scene::MultipleChoice>]
89
92
  # @return [Class<Scene::MultipleChoice>]
90
93
  def multiple_choice(name = nil, &block)
91
- block Class.new(Scene::MultipleChoice, &block), name
94
+ self.block Class.new(Scene::MultipleChoice, &block), name
92
95
  end
93
96
 
94
97
  # Create a yes-or-no scene.
@@ -113,7 +116,7 @@ module Gamefic
113
116
  # @param name [Symbol, nil]
114
117
  # @return [Class<Scene::YesOrNo>]
115
118
  def yes_or_no(name = nil, &block)
116
- block Class.new(Scene::YesOrNo, &block), name
119
+ self.block Class.new(Scene::YesOrNo, &block), name
117
120
  end
118
121
 
119
122
  # Create an active choice scene.
@@ -121,7 +124,7 @@ module Gamefic
121
124
  # @param name [Symbol, nil]
122
125
  # @return [Class<Scene::ActiveChoice>]
123
126
  def active_choice(name = nil, &block)
124
- block Class.new(Scene::ActiveChoice, &block), name
127
+ self.block Class.new(Scene::ActiveChoice, &block), name
125
128
  end
126
129
 
127
130
  # Create a pause.
@@ -131,9 +134,10 @@ module Gamefic
131
134
  # @param name [Symbol, nil]
132
135
  # @yieldparam [Actor]
133
136
  # @yieldparam [Props::Default]
137
+ # @yieldreceiver [Object<self>]
134
138
  # @return [Class<Scene::Pause>]
135
139
  def pause(name = nil, &block)
136
- block(Class.new(Scene::Pause) do
140
+ self.block(Class.new(Scene::Pause) do
137
141
  on_start(&block)
138
142
  end, name)
139
143
  end
@@ -150,9 +154,10 @@ module Gamefic
150
154
  # @param name [Symbol, nil]
151
155
  # @yieldparam [Actor]
152
156
  # @yieldparam [Props::Default]
157
+ # @yieldreceiver [Object<self>]
153
158
  # @return [Class<Scene::Conclusion>]
154
159
  def conclusion(name = nil, &block)
155
- block(Class.new(Scene::Conclusion) do
160
+ self.block(Class.new(Scene::Conclusion) do
156
161
  on_start(&block)
157
162
  end, name)
158
163
  end
@@ -62,7 +62,7 @@ module Gamefic
62
62
  # pick('the red box')
63
63
  #
64
64
  # @param args [Array]
65
- # @return [Proxy]
65
+ # @return [Proxy::Pick]
66
66
  def pick *args
67
67
  Proxy::Pick.new(*args)
68
68
  end
@@ -17,8 +17,15 @@ module Gamefic
17
17
  # @param command [String] The format of the original command
18
18
  # @param translation [String] The format of the translated command
19
19
  # @return [Syntax] the Syntax object
20
- def interpret command, translation
21
- syntaxes.push Syntax.new(command, translation)
20
+ def interpret(command, translation)
21
+ parts = Syntax.split(command)
22
+ additions = if parts.first.include?('|')
23
+ parts.first.split('|').map { |verb| Syntax.new("#{verb} #{verb[1..].join(' ')}", translation) }
24
+ else
25
+ [Syntax.new(command, translation)]
26
+ end
27
+ syntaxes.concat additions
28
+ additions
22
29
  end
23
30
 
24
31
  def syntaxes
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  module Scripting
3
5
  # Scripting hook methods are instance methods that return callbacks for
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  module Scripting
3
5
  module Responses
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  module Scripting
3
5
  module Scenes
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  module Scripting
3
5
  module Seeds
4
6
  # @return [Array<Proc>]
5
7
  def seeds
6
- self.class.seeds
8
+ (included_scripts.flat_map(&:seeds) + self.class.seeds).uniq
7
9
  end
8
10
  end
9
11
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Gamefic
2
4
  module Scripting
3
5
  module Syntaxes
@@ -35,7 +35,7 @@ module Gamefic
35
35
  .map { |blk| Binding.new(self, blk) }
36
36
  end
37
37
 
38
- def self.included other
38
+ def self.included(other)
39
39
  super
40
40
  other.extend Scriptable
41
41
  end
@@ -82,9 +82,10 @@ module Gamefic
82
82
  #
83
83
  # @param text [String]
84
84
  # @return [Boolean]
85
- def accept?(text)
85
+ def match?(text)
86
86
  !!text.match(regexp)
87
87
  end
88
+ alias accept? match?
88
89
 
89
90
  # Get a signature that identifies the form of the Syntax.
90
91
  # Signatures are used to compare Syntaxes to each other.
@@ -118,6 +119,48 @@ module Gamefic
118
119
  string.start_with?(':') ? nil : string.to_sym
119
120
  end
120
121
 
122
+ # Split a syntax template by words and expressions.
123
+ #
124
+ # @param template [String]
125
+ # @return [Array<String>]
126
+ def self.split(template)
127
+ parens = 0
128
+ result = template.normalize.chars.each.with_object(['']) do |char, result|
129
+ parens = parse_token(char, parens, result)
130
+ end
131
+ raise "Unbalanced parentheses in syntax '#{template}'" unless parens.zero?
132
+
133
+ result.pop if result.last.empty?
134
+
135
+ result
136
+ end
137
+
138
+ class << self
139
+ private
140
+
141
+ # @param char [String]
142
+ # @param parens [Integer]
143
+ # @param result [Array<String>]
144
+ def parse_token(char, parens, result)
145
+ case char
146
+ when /[()]/
147
+ parens += (char == '(' ? 1 : -1)
148
+ when /\s/
149
+ return parens if result.last.empty?
150
+
151
+ if parens.positive?
152
+ result.push(result.pop + char)
153
+ else
154
+ result.push ''
155
+ end
156
+ else
157
+ result.push(result.pop + char)
158
+ end
159
+
160
+ parens
161
+ end
162
+ end
163
+
121
164
  private
122
165
 
123
166
  # @return [String]
@@ -142,9 +185,9 @@ module Gamefic
142
185
 
143
186
  # @return [Array<String>]
144
187
  def make_tokens
145
- template.keywords.map.with_index do |word, idx|
188
+ Syntax.split(template).map.with_index do |word, idx|
189
+ next "(?:\\b#{word.gsub('|', '|\\b')})" if word.include?('|')
146
190
  next word unless word.match?(PARAM_REGEXP)
147
-
148
191
  next nil if idx.positive? && template.keywords[idx - 1].match?(PARAM_REGEXP)
149
192
 
150
193
  '([\w\W\s\S]*?)'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gamefic
4
- VERSION = '4.1.1'
4
+ VERSION = '4.2.0'
5
5
  end
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gamefic
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.1
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fred Snyder
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-03-02 00:00:00.000000000 Z
10
+ date: 2025-11-27 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: yard-solargraph
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -226,7 +239,6 @@ homepage: https://gamefic.com
226
239
  licenses:
227
240
  - MIT
228
241
  metadata: {}
229
- post_install_message:
230
242
  rdoc_options: []
231
243
  require_paths:
232
244
  - lib
@@ -241,8 +253,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
241
253
  - !ruby/object:Gem::Version
242
254
  version: '0'
243
255
  requirements: []
244
- rubygems_version: 3.3.7
245
- signing_key:
256
+ rubygems_version: 3.6.7
246
257
  specification_version: 4
247
258
  summary: Gamefic
248
259
  test_files: []