gamefic 2.4.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +41 -40
- data/.rspec-opal +2 -0
- data/.solargraph.yml +20 -3
- data/CHANGELOG.md +9 -0
- data/Rakefile +11 -1
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gamefic.gemspec +5 -2
- data/lib/gamefic/action.rb +52 -183
- data/lib/gamefic/active/cue.rb +25 -0
- data/lib/gamefic/active/epic.rb +68 -0
- data/lib/gamefic/active/messaging.rb +43 -0
- data/lib/gamefic/active/take.rb +69 -0
- data/lib/gamefic/active.rb +95 -192
- data/lib/gamefic/actor.rb +2 -0
- data/lib/gamefic/block.rb +28 -0
- data/lib/gamefic/command.rb +16 -6
- data/lib/gamefic/core_ext/array.rb +4 -4
- data/lib/gamefic/core_ext/string.rb +10 -5
- data/lib/gamefic/describable.rb +39 -65
- data/lib/gamefic/dispatcher.rb +63 -32
- data/lib/gamefic/entity.rb +44 -19
- data/lib/gamefic/logging.rb +32 -0
- data/lib/gamefic/messenger.rb +66 -0
- data/lib/gamefic/narrative.rb +104 -0
- data/lib/gamefic/node.rb +44 -53
- data/lib/gamefic/plot.rb +60 -93
- data/lib/gamefic/props/default.rb +41 -0
- data/lib/gamefic/props/multiple_choice.rb +65 -0
- data/lib/gamefic/props/pause.rb +11 -0
- data/lib/gamefic/props/yes_or_no.rb +21 -0
- data/lib/gamefic/props.rb +10 -0
- data/lib/gamefic/query/base.rb +45 -126
- data/lib/gamefic/query/general.rb +46 -0
- data/lib/gamefic/query/result.rb +20 -0
- data/lib/gamefic/query/scoped.rb +41 -0
- data/lib/gamefic/query/text.rb +30 -31
- data/lib/gamefic/query.rb +7 -15
- data/lib/gamefic/response.rb +118 -0
- data/lib/gamefic/rulebook/calls.rb +90 -0
- data/lib/gamefic/rulebook/events.rb +79 -0
- data/lib/gamefic/rulebook/hooks.rb +57 -0
- data/lib/gamefic/rulebook/scenes.rb +68 -0
- data/lib/gamefic/rulebook.rb +139 -0
- data/lib/gamefic/scanner.rb +103 -0
- data/lib/gamefic/scene/activity.rb +9 -17
- data/lib/gamefic/scene/conclusion.rb +6 -5
- data/lib/gamefic/scene/default.rb +88 -0
- data/lib/gamefic/scene/multiple_choice.rb +14 -69
- data/lib/gamefic/scene/pause.rb +9 -13
- data/lib/gamefic/scene/yes_or_no.rb +6 -46
- data/lib/gamefic/scene.rb +11 -7
- data/lib/gamefic/scope/base.rb +44 -0
- data/lib/gamefic/scope/children.rb +16 -0
- data/lib/gamefic/scope/family.rb +20 -0
- data/lib/gamefic/scope/myself.rb +13 -0
- data/lib/gamefic/scope/parent.rb +13 -0
- data/lib/gamefic/scope/siblings.rb +14 -0
- data/lib/gamefic/scope.rb +8 -0
- data/lib/gamefic/scriptable/actions.rb +156 -0
- data/lib/gamefic/scriptable/entities.rb +76 -0
- data/lib/gamefic/scriptable/events.rb +65 -0
- data/lib/gamefic/scriptable/proxy.rb +55 -0
- data/lib/gamefic/scriptable/queries.rb +73 -0
- data/lib/gamefic/scriptable/scenes.rb +162 -0
- data/lib/gamefic/scriptable.rb +167 -73
- data/lib/gamefic/snapshot.rb +36 -0
- data/lib/gamefic/stage.rb +51 -0
- data/lib/gamefic/subplot.rb +51 -79
- data/lib/gamefic/syntax/template.rb +67 -0
- data/lib/gamefic/syntax.rb +102 -83
- data/lib/gamefic/vault.rb +50 -0
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic.rb +26 -15
- data/spec-opal/spec_helper.rb +24 -0
- metadata +91 -29
- data/lib/gamefic/element.rb +0 -46
- data/lib/gamefic/keywords.rb +0 -52
- data/lib/gamefic/messaging.rb +0 -43
- data/lib/gamefic/plot/darkroom.rb +0 -120
- data/lib/gamefic/plot/host.rb +0 -42
- data/lib/gamefic/plot/snapshot.rb +0 -27
- data/lib/gamefic/query/children.rb +0 -9
- data/lib/gamefic/query/descendants.rb +0 -15
- data/lib/gamefic/query/external.rb +0 -39
- data/lib/gamefic/query/family.rb +0 -18
- data/lib/gamefic/query/itself.rb +0 -13
- data/lib/gamefic/query/matches.rb +0 -75
- data/lib/gamefic/query/parent.rb +0 -9
- data/lib/gamefic/query/siblings.rb +0 -13
- data/lib/gamefic/query/tree.rb +0 -17
- data/lib/gamefic/scene/base.rb +0 -142
- data/lib/gamefic/scene/multiple_scene.rb +0 -29
- data/lib/gamefic/serialize.rb +0 -196
- data/lib/gamefic/world/callbacks.rb +0 -135
- data/lib/gamefic/world/commands.rb +0 -181
- data/lib/gamefic/world/entities.rb +0 -98
- data/lib/gamefic/world/playbook.rb +0 -233
- data/lib/gamefic/world/players.rb +0 -37
- data/lib/gamefic/world/scenes.rb +0 -228
- data/lib/gamefic/world.rb +0 -18
data/lib/gamefic/active.rb
CHANGED
@@ -1,137 +1,93 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'set'
|
4
|
+
require 'gamefic/active/cue'
|
5
|
+
require 'gamefic/active/epic'
|
6
|
+
require 'gamefic/active/messaging'
|
7
|
+
require 'gamefic/active/take'
|
4
8
|
|
5
9
|
module Gamefic
|
6
|
-
class NotConclusionError < RuntimeError; end
|
7
|
-
|
8
10
|
# The Active module gives entities the ability to perform actions and
|
9
11
|
# participate in scenes. The Actor class, for example, is an Entity
|
10
12
|
# subclass that includes this module.
|
11
13
|
#
|
12
14
|
module Active
|
13
|
-
|
14
|
-
|
15
|
-
# @return [Gamefic::Scene::Base]
|
16
|
-
attr_reader :scene
|
15
|
+
include Logging
|
16
|
+
include Messaging
|
17
17
|
|
18
|
-
# The
|
19
|
-
#
|
18
|
+
# The cue that will be used to create a scene at the beginning of the next
|
19
|
+
# turn.
|
20
20
|
#
|
21
|
-
# @return [
|
22
|
-
attr_reader :
|
23
|
-
|
24
|
-
attr_reader :next_options
|
21
|
+
# @return [Active::Cue, nil]
|
22
|
+
attr_reader :next_cue
|
25
23
|
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
# @return [Symbol, nil]
|
25
|
+
def next_scene
|
26
|
+
next_cue&.scene
|
27
|
+
end
|
30
28
|
|
31
|
-
# The
|
29
|
+
# The rulebooks that will be used to perform commands. Every plot and
|
30
|
+
# subplot has its own rulebook.
|
32
31
|
#
|
33
|
-
# @return [
|
34
|
-
|
32
|
+
# @return [Set<Gamefic::World::Rulebook>]
|
33
|
+
# def rulebooks
|
34
|
+
# @rulebooks ||= Set.new
|
35
|
+
# end
|
35
36
|
|
36
|
-
# The
|
37
|
+
# The narratives in which the entity is participating.
|
37
38
|
#
|
38
|
-
# @return [
|
39
|
-
def
|
40
|
-
@
|
41
|
-
end
|
42
|
-
|
43
|
-
def syntaxes
|
44
|
-
playbooks.flat_map(&:syntaxes)
|
39
|
+
# @return [Epic]
|
40
|
+
def epic
|
41
|
+
@epic ||= Epic.new
|
45
42
|
end
|
46
43
|
|
47
|
-
# An array of
|
44
|
+
# An array of commands waiting to be executed.
|
48
45
|
#
|
49
46
|
# @return [Array<String>]
|
50
47
|
def queue
|
51
48
|
@queue ||= []
|
52
49
|
end
|
53
50
|
|
54
|
-
# A hash of
|
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.
|
55
53
|
#
|
56
|
-
# @return [Hash
|
57
|
-
def state
|
58
|
-
@state ||= {}
|
59
|
-
end
|
60
|
-
|
54
|
+
# @return [Hash]
|
61
55
|
def output
|
62
56
|
@output ||= {}
|
63
57
|
end
|
64
58
|
|
65
|
-
# Send a message to the entity.
|
66
|
-
# This method will automatically wrap the message in HTML paragraphs.
|
67
|
-
# To send a message without paragraph formatting, use #stream instead.
|
68
|
-
#
|
69
|
-
# @param message [String]
|
70
|
-
def tell(message)
|
71
|
-
if buffer_stack > 0
|
72
|
-
append_buffer format(message)
|
73
|
-
else
|
74
|
-
super
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# Send a message to the Character as raw text.
|
79
|
-
# Unlike #tell, this method will not wrap the message in HTML paragraphs.
|
80
|
-
#
|
81
|
-
# @param message [String]
|
82
|
-
def stream(message)
|
83
|
-
if buffer_stack > 0
|
84
|
-
append_buffer message
|
85
|
-
else
|
86
|
-
super
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
59
|
# Perform a command.
|
91
60
|
#
|
92
|
-
# The command's action will be executed immediately regardless of the
|
61
|
+
# The command's action will be executed immediately, regardless of the
|
93
62
|
# entity's state.
|
94
63
|
#
|
95
64
|
# @example Send a command as a string
|
96
65
|
# character.perform "take the key"
|
97
66
|
#
|
98
|
-
# @param command [String
|
99
|
-
# @return [
|
100
|
-
def perform(
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
else
|
105
|
-
dispatchers.push Dispatcher.dispatch(self, command.first.to_s)
|
106
|
-
proceed
|
107
|
-
dispatchers.pop
|
108
|
-
end
|
67
|
+
# @param command [String]
|
68
|
+
# @return [void]
|
69
|
+
def perform(command)
|
70
|
+
dispatchers.push Dispatcher.dispatch(self, command)
|
71
|
+
dispatchers.last.execute
|
72
|
+
dispatchers.pop
|
109
73
|
end
|
110
74
|
|
111
75
|
# Quietly perform a command.
|
112
76
|
# This method executes the command exactly as #perform does, except it
|
113
|
-
# buffers the resulting output instead of sending it to
|
77
|
+
# buffers the resulting output instead of sending it to messages.
|
114
78
|
#
|
115
|
-
# @param command [String
|
79
|
+
# @param command [String]
|
116
80
|
# @return [String] The output that resulted from performing the command.
|
117
|
-
def quietly(
|
118
|
-
|
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
|
81
|
+
def quietly(command)
|
82
|
+
messenger.buffer { perform command }
|
127
83
|
end
|
128
84
|
|
129
85
|
# Perform an action.
|
130
86
|
# This is functionally identical to the `perform` method, except the
|
131
|
-
# action must be declared as a verb with a list of
|
87
|
+
# action must be declared as a verb with a list of arguments. Use
|
132
88
|
# `perform` if you need to parse a string as a command.
|
133
89
|
#
|
134
|
-
# The command will be executed immediately regardless of the entity's
|
90
|
+
# The command will be executed immediately, regardless of the entity's
|
135
91
|
# state.
|
136
92
|
#
|
137
93
|
# @example
|
@@ -139,11 +95,10 @@ module Gamefic
|
|
139
95
|
#
|
140
96
|
# @param verb [Symbol]
|
141
97
|
# @param params [Array]
|
142
|
-
# @params quietly [Boolean]
|
143
98
|
# @return [Gamefic::Action]
|
144
|
-
def execute(verb, *params
|
99
|
+
def execute(verb, *params)
|
145
100
|
dispatchers.push Dispatcher.dispatch_from_params(self, verb, params)
|
146
|
-
|
101
|
+
dispatchers.last.execute
|
147
102
|
dispatchers.pop
|
148
103
|
end
|
149
104
|
|
@@ -170,138 +125,86 @@ module Gamefic
|
|
170
125
|
# end
|
171
126
|
# end
|
172
127
|
#
|
173
|
-
# @
|
174
|
-
|
175
|
-
|
176
|
-
return if dispatchers.empty?
|
177
|
-
a = dispatchers.last.next
|
178
|
-
return if a.nil?
|
179
|
-
prepare_buffer quietly
|
180
|
-
a.execute
|
181
|
-
flush_buffer quietly
|
128
|
+
# @return [void]
|
129
|
+
def proceed
|
130
|
+
dispatchers.last&.proceed&.execute
|
182
131
|
end
|
183
132
|
|
184
|
-
#
|
185
|
-
# Use #prepare if you want to declare a scene to be started at the
|
186
|
-
# beginning of the next turn.
|
133
|
+
# Cue a scene to start in the next turn.
|
187
134
|
#
|
188
|
-
# @
|
189
|
-
# @param data [Hash] Additional scene data
|
190
|
-
def cue new_scene, **data
|
191
|
-
@next_scene = nil
|
192
|
-
if new_scene.nil?
|
193
|
-
@scene = nil
|
194
|
-
else
|
195
|
-
@scene = new_scene.new(self, **data)
|
196
|
-
@scene.start
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
# Prepare a scene to be started for this character at the beginning of the
|
201
|
-
# next turn. As opposed to #cue, a prepared scene will not start until the
|
202
|
-
# current scene finishes.
|
135
|
+
# @raise [ArgumentError] if the scene is not valid
|
203
136
|
#
|
204
|
-
# @param
|
205
|
-
# @
|
206
|
-
|
207
|
-
|
208
|
-
@
|
209
|
-
end
|
210
|
-
|
211
|
-
# Return true if the character is expected to be in the specified scene on
|
212
|
-
# the next turn.
|
213
|
-
#
|
214
|
-
# @return [Boolean]
|
215
|
-
def will_cue? scene
|
216
|
-
(@scene.class == scene and @next_scene.nil?) || @next_scene == scene
|
217
|
-
end
|
137
|
+
# @param scene [Symbol]
|
138
|
+
# @param context [Hash] Extra data to pass to the scene's props
|
139
|
+
# @return [Cue]
|
140
|
+
def cue scene, **context
|
141
|
+
return @next_cue if @next_cue&.scene == scene && @next_cue&.context == context
|
218
142
|
|
219
|
-
|
220
|
-
# NotConclusionError if the scene is not a Scene::Conclusion.
|
221
|
-
#
|
222
|
-
# @param new_scene [Class<Scene::Base>]
|
223
|
-
# @oaram data [Hash] Additional scene data
|
224
|
-
def conclude new_scene, **data
|
225
|
-
raise NotConclusionError unless new_scene <= Scene::Conclusion
|
226
|
-
cue new_scene, **data
|
227
|
-
end
|
143
|
+
logger.debug "Overwriting existing cue `#{@next_cue.scene}` with `#{scene}`" if @next_cue
|
228
144
|
|
229
|
-
|
230
|
-
#
|
231
|
-
# @return [Boolean]
|
232
|
-
def concluded?
|
233
|
-
!scene.nil? && scene.kind_of?(Scene::Conclusion)
|
145
|
+
@next_cue = Cue.new(scene, **context)
|
234
146
|
end
|
147
|
+
alias prepare cue
|
235
148
|
|
236
|
-
def
|
237
|
-
|
149
|
+
def start_take
|
150
|
+
ensure_cue
|
151
|
+
@last_cue = @next_cue
|
152
|
+
cue :default_scene
|
153
|
+
@props = Take.start(self, @last_cue)
|
238
154
|
end
|
239
155
|
|
240
|
-
def
|
241
|
-
|
242
|
-
end
|
156
|
+
def finish_take
|
157
|
+
return unless @last_cue
|
243
158
|
|
244
|
-
|
245
|
-
#
|
246
|
-
def entered scene
|
247
|
-
klass = (scene.is_a?(Gamefic::Scene::Base) ? scene.class : scene)
|
248
|
-
entered_scenes.add klass
|
159
|
+
Take.finish(self, @last_cue, @props)
|
249
160
|
end
|
250
161
|
|
251
|
-
#
|
162
|
+
# Restart the scene from the most recent cue.
|
252
163
|
#
|
253
|
-
# @return [
|
254
|
-
def
|
255
|
-
|
256
|
-
entered_scenes.include?(klass)
|
257
|
-
end
|
258
|
-
|
259
|
-
private
|
164
|
+
# @return [Cue, nil]
|
165
|
+
def recue
|
166
|
+
logger.warn "No scene to recue" unless @last_cue
|
260
167
|
|
261
|
-
|
262
|
-
if quietly
|
263
|
-
if buffer_stack == 0
|
264
|
-
@buffer = ""
|
265
|
-
end
|
266
|
-
set_buffer_stack(buffer_stack + 1)
|
267
|
-
end
|
168
|
+
@next_cue = @last_cue
|
268
169
|
end
|
269
170
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
171
|
+
# Cue a conclusion. This method works like #cue, except it will raise an
|
172
|
+
# error if the scene is not a conclusion.
|
173
|
+
#
|
174
|
+
# @raise [ArgumentError] if the requested scene is not a conclusion
|
175
|
+
#
|
176
|
+
# @param new_scene [Symbol]
|
177
|
+
# @oaram context [Hash] Additional scene data
|
178
|
+
def conclude scene, **context
|
179
|
+
cue scene, **context
|
180
|
+
available = epic.select_scene(scene)
|
181
|
+
raise ArgumentError, "`#{scene}` is not a conclusion" unless available.conclusion?
|
276
182
|
|
277
|
-
|
278
|
-
def entered_scenes
|
279
|
-
@entered_scenes ||= Set.new
|
183
|
+
@next_cue
|
280
184
|
end
|
281
185
|
|
282
|
-
|
283
|
-
|
186
|
+
# True if the actor is ready to leave the game.
|
187
|
+
#
|
188
|
+
def concluding?
|
189
|
+
epic.empty? || (@last_cue && epic.conclusion?(@last_cue.scene))
|
284
190
|
end
|
285
191
|
|
286
|
-
def
|
287
|
-
|
192
|
+
def accessible?
|
193
|
+
false
|
288
194
|
end
|
289
195
|
|
290
|
-
|
291
|
-
def buffer
|
292
|
-
@buffer ||= ''
|
293
|
-
end
|
196
|
+
private
|
294
197
|
|
295
|
-
|
296
|
-
|
198
|
+
# @return [Array<Dispatcher>]
|
199
|
+
def dispatchers
|
200
|
+
@dispatchers ||= []
|
297
201
|
end
|
298
202
|
|
299
|
-
def
|
300
|
-
|
301
|
-
end
|
203
|
+
def ensure_cue
|
204
|
+
return if next_cue
|
302
205
|
|
303
|
-
|
304
|
-
|
206
|
+
logger.debug "Using default scene for actor without cue"
|
207
|
+
cue :default_scene
|
305
208
|
end
|
306
209
|
end
|
307
210
|
end
|
data/lib/gamefic/actor.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
# A code container for seeds and scripts.
|
5
|
+
#
|
6
|
+
class Block
|
7
|
+
# @return [Symbol]
|
8
|
+
attr_reader :type
|
9
|
+
|
10
|
+
# @return [Proc]
|
11
|
+
attr_reader :code
|
12
|
+
|
13
|
+
# @param type [Symbol]
|
14
|
+
# @param code [Proc]
|
15
|
+
def initialize type, code
|
16
|
+
@type = type
|
17
|
+
@code = code
|
18
|
+
end
|
19
|
+
|
20
|
+
def script?
|
21
|
+
type == :script
|
22
|
+
end
|
23
|
+
|
24
|
+
def seed?
|
25
|
+
type == :seed
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/gamefic/command.rb
CHANGED
@@ -1,15 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Gamefic
|
2
|
-
# A
|
3
|
-
#
|
4
|
+
# A decomposition of a text-based command into its verb and arguments.
|
5
|
+
#
|
6
|
+
# Commands are typically derived from tokenization against syntaxes.
|
4
7
|
#
|
5
8
|
class Command
|
6
|
-
# A Symbol representing the command's verb or verbal phrase.
|
7
|
-
#
|
8
9
|
# @return [Symbol]
|
9
10
|
attr_reader :verb
|
10
11
|
|
11
|
-
# An Array of arguments to be mapped to an Action's Queries.
|
12
|
-
#
|
13
12
|
# @return [Array<String>]
|
14
13
|
attr_reader :arguments
|
15
14
|
|
@@ -17,5 +16,16 @@ module Gamefic
|
|
17
16
|
@verb = verb
|
18
17
|
@arguments = arguments
|
19
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
|
20
30
|
end
|
21
31
|
end
|
@@ -14,7 +14,7 @@ class Array
|
|
14
14
|
#
|
15
15
|
# @return [Array]
|
16
16
|
def that_are(*cls)
|
17
|
-
result =
|
17
|
+
result = dup
|
18
18
|
cls.each do |c|
|
19
19
|
_keep result, c, true
|
20
20
|
end
|
@@ -26,7 +26,7 @@ class Array
|
|
26
26
|
#
|
27
27
|
# @return [Array]
|
28
28
|
def that_are_not(*cls)
|
29
|
-
result =
|
29
|
+
result = dup
|
30
30
|
cls.each do |c|
|
31
31
|
_keep result, c, false
|
32
32
|
end
|
@@ -72,8 +72,8 @@ class Array
|
|
72
72
|
case cls
|
73
73
|
when Class, Module
|
74
74
|
arr.keep_if { |i| i.is_a?(cls) == bool }
|
75
|
-
when
|
76
|
-
arr.keep_if { |i|
|
75
|
+
when Proc
|
76
|
+
arr.keep_if { |i| !!cls.call(i) == bool }
|
77
77
|
else
|
78
78
|
arr.keep_if { |i| (i == cls) == bool }
|
79
79
|
end
|
@@ -1,19 +1,24 @@
|
|
1
|
-
|
2
|
-
include Gamefic::Keywords
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
3
|
+
class String
|
4
4
|
# Capitalize the first letter without changing the rest of the string.
|
5
5
|
# (String#capitalize makes the rest of the string lower-case.)
|
6
6
|
#
|
7
7
|
# @return [String] The capitalized text
|
8
8
|
def capitalize_first
|
9
|
-
"#{self[0, 1].upcase}#{self[1,
|
9
|
+
"#{self[0, 1].upcase}#{self[1, length]}"
|
10
10
|
end
|
11
11
|
alias cap_first capitalize_first
|
12
12
|
|
13
13
|
# Get an array of words split by any whitespace.
|
14
14
|
#
|
15
15
|
# @return [Array]
|
16
|
-
def
|
17
|
-
|
16
|
+
def keywords
|
17
|
+
gsub(/[\s-]+/, ' ').strip.downcase.split - %w[a an the]
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String]
|
21
|
+
def normalize
|
22
|
+
keywords.join(' ')
|
18
23
|
end
|
19
24
|
end
|