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,321 @@
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_relative 'config'
22
+ require_relative 'game'
23
+ require_relative 'session'
24
+ require_relative 'io'
25
+ require_relative 'context'
26
+ require_relative 'library'
27
+ require_relative 'options'
28
+ require_relative 'publication'
29
+
30
+ # The Inform module
31
+ module Inform
32
+ # The RuntimeConstants module
33
+ module RuntimeConstants
34
+ UndefinedMainPattern = %r{undefined method 'Main' for an instance of Object}.freeze
35
+ Registry = Class.new(Hash)
36
+ LibraryRegistry = Registry.new
37
+ end
38
+
39
+ # The RuntimeLibrary module
40
+ module RuntimeLibrary
41
+ def play
42
+ log.debug "Using runtime-managed InformLibrary play method override"
43
+ inform_library = Inform::Runtime.libraries[Inform::Runtime]
44
+ inform_library.play
45
+ inform_library
46
+ end
47
+ end
48
+
49
+ # The RuntimeClassMethods module
50
+ module RuntimeClassMethods
51
+ def libraries
52
+ Inform::Runtime::LibraryRegistry
53
+ end
54
+
55
+ def default_config
56
+ @default_config ||= Inform::Config::DEFAULTS
57
+ end
58
+
59
+ def default_game_dir_path
60
+ File.join(project_dir_path, Inform::Runtime.default_config[:game_dir_name])
61
+ end
62
+
63
+ def support_dir_path
64
+ Inform::SUPPORT_DIR_PATH
65
+ end
66
+
67
+ def inform_dir_path
68
+ Inform::INFORM_DIR_PATH
69
+ end
70
+
71
+ def lib_dir_path
72
+ Inform::LIB_DIR_PATH
73
+ end
74
+
75
+ def project_dir_path
76
+ Inform::PROJECT_DIR_PATH
77
+ end
78
+
79
+ def database_saves_dir_path
80
+ File.join(Inform::Runtime.project_dir_path, '.' + Inform::Game.config[:database_name] + '_saves')
81
+ end
82
+
83
+ def default_environment
84
+ Inform::Runtime.default_config[:environment]
85
+ end
86
+
87
+ def invocation_properties
88
+ return Inform::Runtime.instance.invocation_properties unless Inform::Runtime.instance.nil?
89
+ Inform::Runtime.default_config.fetch(:properties, '').split.map(&:to_sym)
90
+ end
91
+
92
+ def invocation_context
93
+ return Inform::Runtime.instance.invocation_context unless Inform::Runtime.instance.nil?
94
+ Struct.new(*Inform::Runtime.invocation_properties)
95
+ end
96
+
97
+ def language_name
98
+ Inform::Runtime.default_config[:language]
99
+ end
100
+
101
+ def main_gem_spec_executable
102
+ require 'rubygems'
103
+ current_working_dir = File.expand_path(__dir__)
104
+ gem_spec = Gem.loaded_specs.values.find do |s|
105
+ current_working_dir.start_with?(s.full_gem_path)
106
+ end
107
+ (gem_spec&.executables || []).first
108
+ end
109
+
110
+ def player?(obj)
111
+ return false unless defined?(Inform::IO::Session) && Inform::IO::Session.respond_to?(:players)
112
+ Inform::IO::Session.players.include?(obj)
113
+ end
114
+
115
+ def init(options = Inform::Runtime.default_config)
116
+ Inform::Runtime.instance(options)
117
+ end
118
+
119
+ def instance(options = Inform::Runtime.default_config)
120
+ return @instance unless @instance.nil?
121
+ @instance_mutex.synchronize do
122
+ @instance ||= new(options)
123
+ end
124
+ @instance
125
+ end
126
+ end
127
+ # module RuntimeClassMethods
128
+
129
+ # The RuntimeInstanceMethods module
130
+ module RuntimeInstanceMethods
131
+ include RuntimeConstants
132
+
133
+ attr_accessor :main_object
134
+
135
+ def config_file_path
136
+ @config_file_path ||= File.join(game_path, @options[:game_config_file_name])
137
+ end
138
+
139
+ def game_dir_name
140
+ @game_dir_name ||= @options[:game_dir_name]
141
+ end
142
+
143
+ def game_path
144
+ @game_path ||= File.expand_path(@options[:game_path])
145
+ end
146
+
147
+ def game_components
148
+ @game_components ||= @options.fetch(:game_components, '').split.map(&:to_sym)
149
+ end
150
+
151
+ def grammar_module_path
152
+ @grammar_module_path ||= File.join(game_path, @options[:game_grammar_module_name])
153
+ end
154
+
155
+ def invocation_properties
156
+ @invocation_properties ||= @options.fetch(:properties, '').split.map(&:to_sym)
157
+ end
158
+
159
+ def invocation_context
160
+ @invocation_context ||= Struct.new(*invocation_properties)
161
+ end
162
+
163
+ def invoke_main_method
164
+ ::Object.new.send(:Main)
165
+ rescue NameError => e
166
+ if UndefinedMainPattern.match?(e.message)
167
+ log.error "Main() definition is missing"
168
+ else
169
+ log.error e.message
170
+ e.backtrace.each { |t| log.error t } # TODO: Remove
171
+ end
172
+ end
173
+
174
+ # TODO: Ensuring the location of the player object requires
175
+ # that this method be invoked before the move @player, @location
176
+ # operation in the InformLibrary#play method. This means that
177
+ # ensuring the location must take place either in the game-
178
+ # defined Initialise() method, or else it can happen in a
179
+ # provided LibraryExtension method, but in that case it would
180
+ # be at risk of being overridden by a game-defined Initialise()
181
+ # method.
182
+ # TODO: Try to figure out what the best approach here is. I
183
+ # think that if a multi-player game is to supply an entrypoint
184
+ # module, it will have to handle the location ensurance itself.
185
+ def ensure_location
186
+ return if inform_library.nil?
187
+ return if inform_library.location
188
+ player_obj = inform_library.player
189
+ location = player_obj.location
190
+ location ||= player_obj.spawn_point if player_obj.respond_to?(:spawn_point)
191
+ inform_library.location = location
192
+ end
193
+
194
+ def inform_library(key = Inform::Runtime)
195
+ Inform::Runtime.libraries[key]
196
+ end
197
+
198
+ def manage_inform_library(key = Inform::Runtime)
199
+ Inform::Runtime.libraries[key] ||= begin
200
+ inform_library = InformLibrary.new
201
+ if self.respond_to?(:specialized_player)
202
+ inform_library.selfobj = specialized_player
203
+ end
204
+ inform_library.subscribe(inform_library.selfobj)
205
+ inform_library
206
+ end
207
+ end
208
+
209
+ def apply_preferences
210
+ return if inform_library.nil?
211
+ player_obj = inform_library.player
212
+ preferences = player_obj.&:preferences
213
+ return if preferences.nil? || !preferences.respond_to?(:properties)
214
+ preferences.properties.each_pair do |key, value|
215
+ variable = :"@#{key}"
216
+ Parser.initial_state.delete(variable)
217
+ inform_library.instance_variable_set(variable, value)
218
+ end
219
+ end
220
+
221
+ def manage_privileges
222
+ Inform::Object.include(Inform::Privileges)
223
+ return unless @options[:admin]
224
+ give inform_library.player, :admin
225
+ end
226
+
227
+ def read_eval_print_loop
228
+ loop do
229
+ prompt
230
+ inform_library.send(inform_library.inform(read))
231
+ end
232
+ rescue Interrupt => e
233
+ $stdout.puts "\n#{e.class.name}"
234
+ end
235
+
236
+ def play_game
237
+ log.debug "#{self}#play_game"
238
+ invoke_main_method
239
+ # ensure_location
240
+ apply_preferences
241
+ manage_privileges
242
+ read_eval_print_loop
243
+ end
244
+
245
+ def quit
246
+ $stdout.print "[Hit enter to exit.]"
247
+ $stdin.getc
248
+ # Curses.getch
249
+ # Curses.close_screen
250
+ exit
251
+ end
252
+
253
+ def to_s
254
+ "#<#{self.class.name}:#{object_id}>"
255
+ end
256
+
257
+ def inspect
258
+ to_s
259
+ end
260
+ end
261
+ # module RuntimeInstanceMethods
262
+
263
+ # The Inform::Privileges module to implement privilege
264
+ module Privileges
265
+ def admin?
266
+ self.has?(:admin)
267
+ end
268
+
269
+ def builder?
270
+ admin? || self.has?(:builder)
271
+ end
272
+ end
273
+
274
+ # The Runtime class loads a game and plays it in a REPL session.
275
+ class Runtime
276
+ include Inform::RuntimeConstants
277
+ include Inform::RuntimeInstanceMethods
278
+ include Inform::Game::Loader
279
+ include Inform::Library::Loader
280
+ include Inform::IO
281
+ include Inform::Publisher
282
+
283
+ class << self
284
+ include Inform::RuntimeClassMethods
285
+ include Inform::Library::ClassMethods
286
+ end
287
+
288
+ @default_config = nil
289
+ @instance = nil
290
+ @instance_mutex = Mutex.new
291
+
292
+ private_class_method :new
293
+
294
+ def initialize(options = Inform::Runtime.default_config)
295
+ @options = options
296
+ Inform::Game.init(@options)
297
+ Inform::Context.set(invocation_context)
298
+ Inform.initialize_persistence_layer
299
+ end
300
+ end
301
+ # class Runtime
302
+
303
+ # The App module
304
+ module App
305
+ include Inform::Options
306
+
307
+ def main(args = parse_arguments)
308
+ Logging.log_level = args[:log_level]
309
+ runtime = Inform::Runtime.init(args)
310
+ runtime.load_game
311
+ runtime.play_game
312
+ end
313
+ end
314
+ end
315
+ # module Inform
316
+
317
+ if defined?(ZCODE_ONLY)
318
+ # Simulate compiling with Z-code only compiler
319
+ WORDSIZE = (2**((0.size * 8) - 2) - 1)
320
+ end
321
+ # defined?(ZCODE_ONLY)
@@ -0,0 +1,202 @@
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
+ # The SessionManagementMethods module
24
+ module SessionManagementMethods
25
+ Registry = Class.new(Hash)
26
+ SessionsByChannel = Registry.new
27
+
28
+ def add_promiscuous_states(*states)
29
+ Promiscuous.merge states.flatten
30
+ end
31
+
32
+ def sessions_by_channel
33
+ SessionsByChannel
34
+ end
35
+
36
+ def register(channel, session)
37
+ sessions_by_channel[channel] = session
38
+ end
39
+
40
+ def unregister(channel)
41
+ sessions_by_channel.delete(channel)
42
+ end
43
+
44
+ def [](channel)
45
+ sessions_by_channel[channel]
46
+ end
47
+
48
+ def []=(channel, session)
49
+ sessions_by_channel[channel] = session
50
+ end
51
+
52
+ def include?(channel)
53
+ sessions_by_channel.include?(channel)
54
+ end
55
+
56
+ def get(channel)
57
+ sessions_by_channel[channel] ||= Session.new(channel)
58
+ end
59
+ end
60
+ # module SessionManagementMethods
61
+
62
+ # The SessionStateManagementMethods module
63
+ module SessionStateManagementMethods
64
+ def [](key)
65
+ @session_data[key.to_sym]
66
+ end
67
+
68
+ def []=(key, value)
69
+ @session_data[key.to_sym] = value
70
+ end
71
+
72
+ def include?(key)
73
+ keys.include?(key)
74
+ end
75
+
76
+ def keys
77
+ @session_data.keys
78
+ end
79
+
80
+ def values
81
+ @session_data.values
82
+ end
83
+
84
+ def delete(key)
85
+ @session_data.delete(key)
86
+ end
87
+ end
88
+
89
+ module Inform
90
+ module IO
91
+ # The Session class
92
+ # TODO: Refactor method implementations into modules
93
+ class Session
94
+ # These states accept any input, including no input
95
+ Promiscuous = Set.new
96
+ include SessionStateManagementMethods
97
+
98
+ class << self
99
+ include SessionManagementMethods
100
+ end
101
+ attr_accessor :channel, :state, :status
102
+ attr_reader :last_good_state, :last_activity, :previous, :inbound, :outbound, :settings
103
+
104
+ def initialize(channel = nil)
105
+ init_channel_session(channel) unless channel.nil?
106
+ @status = nil
107
+ @last_good_state = @state = default if respond_to? :default
108
+ @session_data = {}
109
+ @settings = {}
110
+ end
111
+
112
+ def init_channel_session(channel)
113
+ Session[channel] = self
114
+ @channel = channel
115
+ @inbound = Inbound.new(self)
116
+ @outbound = Outbound.new(self)
117
+ @last_activity = Time.now
118
+ end
119
+
120
+ def receive(message)
121
+ return if message.nil? # It is okay if the message parameter is an empty string
122
+ return disconnected if @channel.nil?
123
+ return disconnected unless @channel.isActive() && @channel.isOpen()
124
+ process(sanitize(message))
125
+ end
126
+
127
+ def update(machine = self)
128
+ safely_progress(machine)
129
+ after_update if self.respond_to?(:after_update)
130
+ @last_good_state = @state unless @state == :confused
131
+ end
132
+
133
+ def after_update
134
+ # Override for application implementations
135
+ end
136
+
137
+ def sanitize(message)
138
+ message.to_s.strip.scan(/[[:print:]]/).join
139
+ end
140
+
141
+ def session
142
+ self
143
+ end
144
+
145
+ ExitCommandPattern = /^(exit|quit|q)$/i.freeze
146
+
147
+ def exit_command?(message)
148
+ !Promiscuous.include?(@state) && ExitCommandPattern.match?(message)
149
+ end
150
+
151
+ def process(message)
152
+ return disconnect if exit_command?(message)
153
+ @buffer = message
154
+ @last_activity = Time.now
155
+ @status = :active
156
+ if valid_state?
157
+ update
158
+ else
159
+ @inbound.publish @buffer
160
+ end
161
+
162
+ respond
163
+ end
164
+
165
+ def respond
166
+ flush_io
167
+ end
168
+
169
+ def safely_progress(machine = self)
170
+ @state = machine.send(@state)
171
+ raise 'Bad state machine implementation: nil state' if @state.nil?
172
+ rescue StandardError => e
173
+ log.error "Error updating state: #{e.message}", e
174
+ log.warn "Reverting to last known good state #{@last_good_state}"
175
+ @state = @last_good_state
176
+ end
177
+
178
+ def valid_state?(machine = self)
179
+ case @state
180
+ when String, Symbol
181
+ machine.respond_to? @state
182
+ else
183
+ false
184
+ end
185
+ end
186
+
187
+ def confused
188
+ println "The session is in an unknown state: #{@state}"
189
+ println "The last known good session state was: #{@last_good_state}"
190
+ println "Reverting the session to its last known good state, for better or worse."
191
+ prompt
192
+ @last_good_state
193
+ end
194
+ end
195
+ # class Session
196
+
197
+ # The Connection class
198
+ class Connection; end
199
+ end
200
+ # module IO
201
+ end
202
+ # module Inform