inform-runtime 1.0.4

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +623 -0
  3. data/README.md +185 -0
  4. data/Rakefile +65 -0
  5. data/config/database.yml +37 -0
  6. data/exe/inform.rb +6 -0
  7. data/game/config.yml +5 -0
  8. data/game/example.inf +76 -0
  9. data/game/example.rb +90 -0
  10. data/game/forms/example_form.rb +2 -0
  11. data/game/grammar/game_grammar.inf.rb +11 -0
  12. data/game/languages/english.rb +2 -0
  13. data/game/models/example_model.rb +2 -0
  14. data/game/modules/example_module.rb +9 -0
  15. data/game/rules/example_state.rb +2 -0
  16. data/game/scripts/example_script.rb +2 -0
  17. data/game/topics/example_topic.rb +2 -0
  18. data/game/verbs/game_verbs.rb +15 -0
  19. data/game/verbs/metaverbs.rb +2028 -0
  20. data/lib/runtime/articles.rb +138 -0
  21. data/lib/runtime/builtins.rb +359 -0
  22. data/lib/runtime/color.rb +145 -0
  23. data/lib/runtime/command.rb +470 -0
  24. data/lib/runtime/config.rb +48 -0
  25. data/lib/runtime/context.rb +78 -0
  26. data/lib/runtime/daemon.rb +266 -0
  27. data/lib/runtime/database.rb +500 -0
  28. data/lib/runtime/events.rb +771 -0
  29. data/lib/runtime/experimental/handler_dsl.rb +175 -0
  30. data/lib/runtime/game.rb +74 -0
  31. data/lib/runtime/game_loader.rb +132 -0
  32. data/lib/runtime/grammar_parser.rb +553 -0
  33. data/lib/runtime/helpers.rb +177 -0
  34. data/lib/runtime/history.rb +45 -0
  35. data/lib/runtime/inflector.rb +195 -0
  36. data/lib/runtime/io.rb +174 -0
  37. data/lib/runtime/kernel.rb +450 -0
  38. data/lib/runtime/library.rb +59 -0
  39. data/lib/runtime/library_loader.rb +135 -0
  40. data/lib/runtime/link.rb +158 -0
  41. data/lib/runtime/logging.rb +197 -0
  42. data/lib/runtime/mixins.rb +570 -0
  43. data/lib/runtime/module.rb +202 -0
  44. data/lib/runtime/object.rb +761 -0
  45. data/lib/runtime/options.rb +104 -0
  46. data/lib/runtime/persistence.rb +292 -0
  47. data/lib/runtime/plurals.rb +60 -0
  48. data/lib/runtime/prototype.rb +307 -0
  49. data/lib/runtime/publication.rb +92 -0
  50. data/lib/runtime/runtime.rb +321 -0
  51. data/lib/runtime/session.rb +202 -0
  52. data/lib/runtime/stdlib.rb +604 -0
  53. data/lib/runtime/subscription.rb +47 -0
  54. data/lib/runtime/tag.rb +287 -0
  55. data/lib/runtime/tree.rb +204 -0
  56. data/lib/runtime/version.rb +24 -0
  57. data/lib/runtime/world_tree.rb +69 -0
  58. data/lib/runtime.rb +35 -0
  59. metadata +199 -0
@@ -0,0 +1,553 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # Copyright Nels Nelson 2008-2023 but freely usable (see license)
5
+ #
6
+ # This file is part of the Inform Runtime.
7
+ #
8
+ # The Inform Runtime is free software: you can redistribute it and/or
9
+ # modify it under the terms of the GNU General Public License as published
10
+ # by the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # The Inform Runtime is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with the Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
20
+
21
+ require 'set'
22
+
23
+ # In-file Grammar support implementation follows.
24
+ #
25
+ # Usage example:
26
+ #
27
+ # Inform::Grammar <<~EOT
28
+ # Verb meta 'example'
29
+ # * -> Example;
30
+ # Verb 'smile'
31
+ # * -> Smile;
32
+ # EOT
33
+ module Inform
34
+ def Grammar(src)
35
+ source_location = caller_locations(1, 1).first
36
+ mod_name = File.basename(source_location.path, '.*').gsub(/\W+/, '_').to_sym
37
+ Inform::Grammar::Parser.new.parse_grammar(mod_name, src.each_line,
38
+ origin_path: source_location.path, origin_line: source_location.lineno)
39
+ self
40
+ end
41
+ module_function :Grammar
42
+ end
43
+
44
+ # The Inform module
45
+ module Inform
46
+ Dictionary = Set.new
47
+
48
+ ### Verbs
49
+ class Verb < Set
50
+ attr_reader :meta, :emote
51
+ attr_accessor :grammars, :source
52
+
53
+ def initialize(verbs, source, meta = nil, emote = nil)
54
+ super()
55
+ merge(verbs)
56
+ @meta = meta unless meta.nil?
57
+ @emote = emote unless emote.nil?
58
+ @grammars = []
59
+ @source = source
60
+ end
61
+
62
+ def ==(other)
63
+ !other.nil? && include?(other.to_s) &&
64
+ meta? == other.meta? && grammars == other.grammars && super
65
+ end
66
+
67
+ def <=>(other)
68
+ !other.nil? && include?(other.to_s) &&
69
+ meta? == other.meta? && grammars == other.grammars && super
70
+ end
71
+
72
+ def eql?(other)
73
+ !other.nil? && include?(other.to_s) &&
74
+ meta? == other.meta? && grammars == other.grammars && super
75
+ end
76
+
77
+ def verb
78
+ first
79
+ end
80
+
81
+ def meta?
82
+ !meta.nil?
83
+ end
84
+
85
+ def emote?
86
+ !emote.nil?
87
+ end
88
+
89
+ SpaceString = ' '.freeze
90
+ VerbString = 'Verb'.freeze
91
+ MetaString = 'meta'.freeze
92
+ EmoteString = 'emote'.freeze
93
+ Newline = "\n".freeze
94
+
95
+ def to_s
96
+ VerbString + SpaceString +
97
+ if meta?
98
+ MetaString + SpaceString
99
+ elsif emote?
100
+ EmoteString + SpaceString
101
+ else
102
+ ''
103
+ end +
104
+ map { |verb| "'#{verb}'" }.join(SpaceString) + Newline +
105
+ grammars.map(&:to_s).join(Newline)
106
+ end
107
+ alias to_str to_s
108
+
109
+ def hash
110
+ [super, meta, emote, grammars].hash
111
+ end
112
+
113
+ #### Grammars
114
+ class Grammar < Array
115
+ MultipleEnquotedPrepositionPattern = %r{^'(.*)'/'(.*)'+$}.freeze
116
+ SingleEnquotedPrepositionPattern = %r{^'(.*)'$}.freeze
117
+ CustomScopePattern = %r{^(.*)=(.*)$}.freeze
118
+ attr_reader :source, :action, :expected_tokens
119
+
120
+ def initialize(grammar, source, action, reverse = nil)
121
+ super()
122
+ concat(symbolize_grammar(grammar)) unless grammar.empty?
123
+ push(:end)
124
+ @action = action
125
+ @expected_tokens = grammar.map { |a| a =~ CustomScopePattern ? a.to_s.split('=').first.to_sym : a }
126
+ @reverse = reverse.nil? ? false : reverse
127
+ @source = source
128
+ end
129
+
130
+ def empty?
131
+ length == (include?(:end) ? 1 : 0)
132
+ end
133
+
134
+ SpaceString = ' '.freeze
135
+ Indention = SpaceString * 4
136
+ AsteriskString = '*'.freeze
137
+ ReverseString = 'reverse'.freeze
138
+ ArrowString = '->'.freeze
139
+ GrammarTokenPadding = 40
140
+
141
+ def to_s
142
+ Indention + AsteriskString + SpaceString +
143
+ tokens_sans_end.ljust(GrammarTokenPadding) +
144
+ ArrowString + SpaceString + action +
145
+ if reverse?
146
+ SpaceString + ReverseString
147
+ else
148
+ ''
149
+ end
150
+ end
151
+ alias to_str to_s
152
+
153
+ def parameters
154
+ @parameters ||= []
155
+ end
156
+
157
+ def reversed_parameters
158
+ @reversed_parameters ||= [parameters[1], parameters[0]] + parameters[2..]
159
+ end
160
+
161
+ def expected_parameters
162
+ reverse? ? reversed_parameters : parameters
163
+ end
164
+
165
+ def reverse?
166
+ @reverse
167
+ end
168
+ alias reversed? reverse?
169
+
170
+ private
171
+
172
+ def tokens_sans_end
173
+ self[0..-2].join(SpaceString)
174
+ end
175
+
176
+ def symbolize_grammar(grammar)
177
+ grammar.map { |token| symbolize_token(token) }
178
+ end
179
+
180
+ def when_matching(regexp, str, &block)
181
+ match_data = regexp.match(str)
182
+ return false if match_data.nil?
183
+ block.call(match_data.captures) if block_given?
184
+ true
185
+ end
186
+
187
+ def prepositions?(token)
188
+ when_matching(MultipleEnquotedPrepositionPattern, token) do |matches|
189
+ Inform::Dictionary.merge(matches.map(&:to_sym))
190
+ end
191
+ end
192
+
193
+ def preposition?(token)
194
+ when_matching(SingleEnquotedPrepositionPattern, token) do |matches|
195
+ Inform::Dictionary << matches.first.to_sym
196
+ end
197
+ end
198
+
199
+ def custom_scope?(token)
200
+ when_matching(CustomScopePattern, token) do |matches|
201
+ parameters << matches.first.to_sym
202
+ end
203
+ end
204
+
205
+ def symbolize_token(token)
206
+ return token if prepositions?(token) || preposition?(token) || custom_scope?(token)
207
+ parameters << token.to_sym
208
+ token.to_sym
209
+ end
210
+ end
211
+ # class Grammar
212
+ end
213
+ # class Verb
214
+ end
215
+ # module Inform
216
+
217
+ # The Inform module to implement additional Grammar parsing
218
+ module Inform
219
+ # The DuplicateVerb error class
220
+ class DuplicateVerb < StandardError
221
+ UnknownString = 'unknown'.freeze
222
+
223
+ attr_writer :file, :line
224
+
225
+ def initialize(verb)
226
+ super()
227
+ @verb = verb
228
+ @file = UnknownString
229
+ @line = UnknownString
230
+ end
231
+
232
+ def to_s
233
+ "(#{@file}):#{@line}: verb error, verb '#{@verb}', already defined"
234
+ end
235
+ end
236
+
237
+ # The UnexpectedToken error class
238
+ class UnexpectedToken < StandardError
239
+ UnknownString = 'unknown'.freeze
240
+ attr_writer :file, :line
241
+
242
+ def initialize(expected, found)
243
+ super()
244
+ @expected = expected
245
+ @found = found
246
+ @file = UnknownString
247
+ @line = UnknownString
248
+ end
249
+
250
+ def to_s
251
+ "(#{@file}):#{@line}: token error, unexpected '#{@found}', expecting '#{@expected}'"
252
+ end
253
+ end
254
+
255
+ def self.load_grammar(module_name)
256
+ Inform::Grammar.load_by_name(module_name)
257
+ end
258
+
259
+ def self.load_grammar_by_path(module_path)
260
+ Inform::Grammar.load_by_path(module_path)
261
+ end
262
+
263
+ def self.unload_grammars!
264
+ log.debug "Unloading all grammars"
265
+ Inform::Grammar::Verbs.clear
266
+ end
267
+
268
+ def self.prime_dictionary
269
+ Inform::Object.select_map(:name).compact.each do |n|
270
+ Inform::Dictionary.merge(n.split(/[,\s]+/).grep_v(/[^a-z]/i).map(&:downcase).map(&:to_sym))
271
+ end
272
+ end
273
+
274
+ ### Grammar
275
+ module Grammar
276
+ # The Inform::Grammar::Index class
277
+ class Index < Hash
278
+ AdminPattern = %r{admin}.freeze
279
+ BuilderPattern = %r{builder}.freeze
280
+ Profanity = %w[shit damn fuck sod].freeze
281
+ ValidModes = %i[admin builder player].freeze
282
+ ModePatterns = ValidModes.each_with_object({}) { |key, memo| memo[key] = Regexp.new(key.to_s) }
283
+
284
+ def lookup(key, actor = nil, grammar_index = self)
285
+ return nil if key.nil? || !grammar_index.include?(key)
286
+ verb = grammar_index[key].dup
287
+ return verb if actor.nil?
288
+ return nil if guarded?(verb, actor)
289
+ filter_by_permission(verb, actor)
290
+ verb
291
+ end
292
+
293
+ def all(actor = nil, grammar_index = self)
294
+ return grammar_index.map(&:keys).flatten.sort if actor.nil?
295
+ mode = role(actor)
296
+ init_all_verbs_by_mode(mode, grammar_index, actor) unless Inform::Grammar.partitions.include?(mode)
297
+ Inform::Grammar.partitions[mode]
298
+ end
299
+
300
+ def by_mode(mode = nil, grammar_index = self)
301
+ mode = validate(mode)
302
+ init_verbs_filtered_by_mode(mode, grammar_index) unless Inform::Grammar.selections.include?(mode)
303
+ Inform::Grammar.selections[mode]
304
+ end
305
+
306
+ private
307
+
308
+ def init_all_verbs_by_mode(mode, grammar_index, actor, set = verbs_set_instance)
309
+ verbs = grammar_index.each_with_object(set) do |(key, verb), memo|
310
+ memo.add(key) unless Profanity.include?(key) || guarded?(verb, actor)
311
+ end
312
+ Inform::Grammar.partitions[mode] = verbs.to_a
313
+ end
314
+
315
+ def init_verbs_filtered_by_mode(mode, grammar_index, set = verbs_set_instance)
316
+ verbs = grammar_index.each_with_object(set) do |(key, verb), memo|
317
+ memo.add(key) if ModePatterns[mode].match?(verb.source)
318
+ end
319
+ Inform::Grammar.selections[mode] = verbs.to_a
320
+ end
321
+
322
+ def verbs_set_instance
323
+ defined?(Java) ? java.util.TreeSet.new : Set.new
324
+ end
325
+
326
+ def filter_by_permission(verb, actor)
327
+ verb.grammars.delete_if do |grammar|
328
+ guarded?(grammar, actor)
329
+ end
330
+ end
331
+
332
+ def guarded?(resource, obj)
333
+ (AdminPattern.match?(resource.source) && !obj.admin?) ||
334
+ (BuilderPattern.match?(resource.source) && !obj.builder?)
335
+ end
336
+
337
+ def role(obj)
338
+ return :builder if obj.builder?
339
+ return :admin if obj.admin?
340
+ :player
341
+ end
342
+
343
+ def validate(mode)
344
+ return mode.to_sym if ValidModes.include?(mode.to_sym)
345
+ :player
346
+ end
347
+ end
348
+
349
+ Verbs = Inform::Grammar::Index.new
350
+
351
+ def self.partitions
352
+ @partitions ||= defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : {}
353
+ end
354
+
355
+ def self.selections
356
+ @selections ||= defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : {}
357
+ end
358
+
359
+ VALID_EXTENSIONS = ['h.inf.rb', '.inf.rb', '.rb', ''].freeze
360
+
361
+ def self.absolute_basename(file_path)
362
+ File.basename(file_path, File.extname(file_path)).split(/\./).first
363
+ end
364
+
365
+ SUPPORT_DIR_PATH = File.expand_path(__dir__) unless defined?(SUPPORT_DIR_PATH)
366
+ def self.get_path_for_best_match(str)
367
+ VALID_EXTENSIONS.each do |ext|
368
+ grammar_guess = str + ext
369
+ guess = File.expand_path(
370
+ File.join(
371
+ Inform::Library.inform_gem_lib_path,
372
+ Inform::Runtime.inform_dir_path,
373
+ grammar_guess))
374
+ return guess if File.exist?(guess)
375
+ end
376
+ nil
377
+ end
378
+
379
+ def self.load_by_name(module_name)
380
+ module_path = get_path_for_best_match(module_name)
381
+ load_by_path(module_path, module_name)
382
+ end
383
+
384
+ def self.load_by_path(module_path, module_name = nil)
385
+ raise ArgumentError, 'Parameter may not be nil: module_path' if module_path.nil?
386
+ raise LoadError, 'No such file: ' + module_path unless File.exist?(module_path)
387
+ module_name ||= absolute_basename(module_path)
388
+ log.debug "Loading grammar file: #{module_path}"
389
+ Inform::Grammar::Parser.new.parse_grammar(
390
+ module_name.to_sym, File.foreach(module_path), origin_path: module_path)
391
+ end
392
+
393
+ # The Inform::Grammar::Parser class will parse inform-style
394
+ # grammar files
395
+ # rubocop: disable Metrics/ClassLength
396
+ class Parser
397
+ CommentedOrBlankLine = %r{^\s*[^\#\n]}.freeze
398
+ TerminalLinePattern = %r{;$}.freeze
399
+ TokenPattern = %r{'[^\s']+'}.freeze
400
+ ArrowPattern = %r{->}.freeze
401
+ def initialize
402
+ # Base state for block comment toggling
403
+ @block_comment = false
404
+ end
405
+
406
+ # rubocop: disable Metrics/MethodLength
407
+ def parse_grammar(grammar_module, lines, origin_path: nil, origin_line: 0)
408
+ line_index = Integer(origin_line || 0)
409
+ lines.each do |line|
410
+ line_index += 1
411
+ next unless /^\s*[^\#\n]/.match?(line)
412
+ begin
413
+ parse(grammar_module, line)
414
+ rescue DuplicateVerb => e
415
+ e.file = File.basename(origin_path)
416
+ e.line = line_index
417
+ log.warn e
418
+ rescue UnexpectedToken => e
419
+ e.file = File.basename(origin_path)
420
+ e.line = line_index
421
+ raise e
422
+ end
423
+ end
424
+ true
425
+ end
426
+ # rubocop: enable Metrics/MethodLength
427
+
428
+ # rubocop: disable Metrics/AbcSize
429
+ # rubocop: disable Metrics/CyclomaticComplexity
430
+ # rubocop: disable Metrics/MethodLength
431
+ # rubocop: disable Metrics/PerceivedComplexity
432
+ # rubocop: disable Style/RaiseArgs
433
+ def parse(source, line)
434
+ return if block_comment?(line)
435
+ terminal = /;$/.match(line)
436
+ # terminal = TerminalLinePattern.match?(line)
437
+ line.gsub!(/\#.*/, '')
438
+ line.gsub!(/;$/, '')
439
+ tokens = line.split
440
+ return if tokens.empty?
441
+ token = tokens.first
442
+ @grammars ||= []
443
+ case token
444
+ when /^Verb/
445
+ raise UnexpectedToken.new('*', 'Verb') unless @grammars.empty?
446
+ tokens.shift
447
+ meta = tokens.delete 'meta'
448
+ emote = tokens.delete 'emote'
449
+ @first = false
450
+ @last = true
451
+ synonym = tokens.delete '='
452
+ replace = tokens.delete 'replace' # rubocop: disable Lint/UselessAssignment
453
+ tokens.delete_if { |a| !/'[^\s']+'/.match?(a) }
454
+ # tokens.delete_if { |a| !TokenPattern.match?(a) }
455
+ keys = tokens.map { |a| a[1..-2] }
456
+ key = synonym ? keys.last : keys.first
457
+ log.debug "Loading verb: #{key}"
458
+ @verb = Inform::Grammar::Verbs.lookup(key)
459
+ if @verb
460
+ raise DuplicateVerb.new(key) unless synonym
461
+ @verb.merge keys
462
+ else
463
+ @verb = Verb.new(keys, source, meta, emote)
464
+ end
465
+ keys.each do |k|
466
+ Inform::Grammar::Verbs[k] = @verb
467
+ end
468
+ when /^Extend/
469
+ raise UnexpectedToken.new('*', 'Extend') unless @grammars.empty?
470
+ tokens.shift
471
+ meta = tokens.delete('meta')
472
+ emote = tokens.delete('emote')
473
+ only = tokens.delete('only')
474
+ @first = tokens.delete('first')
475
+ @last = tokens.delete('last')
476
+ replace = tokens.delete('replace')
477
+ tokens.delete_if { |verb_token| !/'[^\s']+'/.match?(verb_token) }
478
+ # tokens.delete_if { |token| !TokenPattern.match?(token) }
479
+ keys = tokens.map { |verb_token| verb_token[1..-2] }
480
+ key = keys.first
481
+ existing_verb = Inform::Grammar::Verbs.lookup(key)
482
+ if existing_verb.nil?
483
+ @verb = Verb.new(keys, source, meta, emote)
484
+ elsif only.nil?
485
+ existing_verb.merge(keys)
486
+ @verb = existing_verb
487
+ unless replace.nil?
488
+ @verb.source = source
489
+ @verb.grammars.clear
490
+ end
491
+ else
492
+ keys.each do |existing_key|
493
+ Inform::Grammar::Verbs.delete(existing_key)
494
+ existing_verb.delete(existing_key)
495
+ end
496
+ @verb = Verb.new(keys, source, meta, emote)
497
+ @verb.grammars = existing_verb.grammars if replace.nil?
498
+ keys.each do |new_key|
499
+ Inform::Grammar::Verbs[new_key] = @verb
500
+ end
501
+ end
502
+ Inform::Grammar::Verbs[key] = @verb
503
+ when /\*/
504
+ tokens, action = line.split(ArrowPattern)
505
+ tokens = tokens.split
506
+ tokens.shift
507
+ reverse = false; reverse = true if /reverse/.match?(action)
508
+ action = action.gsub(/;/, '').split.first.to_sym
509
+ grammar = Verb::Grammar.new(tokens, source, action, reverse)
510
+ @grammars.push(grammar)
511
+ else
512
+ unless @verb.nil?
513
+ new_keys = tokens.map { |verb_token| verb_token[1..-2] }
514
+ @verb.merge(new_keys) # Set#merge modifies in-place
515
+ new_keys.each do |new_key|
516
+ Inform::Grammar::Verbs[new_key] = @verb
517
+ end
518
+ end
519
+ end
520
+
521
+ return if terminal.nil?
522
+
523
+ if @first.nil?
524
+ @verb.grammars.concat(@grammars)
525
+ else
526
+ @verb.grammars.unshift(*@grammars)
527
+ end
528
+ @grammars.clear
529
+ end
530
+ # rubocop: enable Metrics/AbcSize
531
+ # rubocop: enable Metrics/CyclomaticComplexity
532
+ # rubocop: enable Metrics/MethodLength
533
+ # rubocop: enable Metrics/PerceivedComplexity
534
+ # rubocop: enable Style/RaiseArgs
535
+ # def parse
536
+
537
+ def block_comment?(line)
538
+ if line[/^=begin/]
539
+ @block_comment = true
540
+ elsif line[/^=end/]
541
+ @block_comment = false
542
+ true
543
+ else
544
+ @block_comment
545
+ end
546
+ end
547
+ end
548
+ # rubocop: enable Metrics/ClassLength
549
+ # class Parser
550
+ end
551
+ # module Grammar
552
+ end
553
+ # module Inform