story-teller 1.1.3
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 +7 -0
- data/LICENSE +623 -0
- data/README.md +188 -0
- data/Rakefile +58 -0
- data/config/database.yml +37 -0
- data/exe/inform.rb +6 -0
- data/game/config.yml +5 -0
- data/game/example.inf +90 -0
- data/game/example.rb +105 -0
- data/game/forms/example_form.rb +2 -0
- data/game/grammar/admin.inf.rb +185 -0
- data/game/grammar/builder.inf.rb +310 -0
- data/game/grammar/game_grammar.inf.rb +6 -0
- data/game/grammar/meta.inf.rb +41 -0
- data/game/languages/english.rb +571 -0
- data/game/models/example_model.rb +2 -0
- data/game/modules/example_module.rb +9 -0
- data/game/modules/parser_extensions.rb +264 -0
- data/game/rules/example_state.rb +2 -0
- data/game/scripts/example_script.rb +2 -0
- data/game/topics/example_topic.rb +2 -0
- data/game/verbs/game_verbs.rb +35 -0
- data/game/verbs/metaverbs.rb +2066 -0
- data/lib/story_teller/application.rb +82 -0
- data/lib/story_teller/cli.rb +35 -0
- data/lib/story_teller/color.rb +144 -0
- data/lib/story_teller/config.rb +61 -0
- data/lib/story_teller/curses_adapter.rb +30 -0
- data/lib/story_teller/database.rb +527 -0
- data/lib/story_teller/game/loader.rb +276 -0
- data/lib/story_teller/game.rb +22 -0
- data/lib/story_teller/inform/models.rb +42 -0
- data/lib/story_teller/inform/relational/link.rb +239 -0
- data/lib/story_teller/inform/relational/module.rb +203 -0
- data/lib/story_teller/inform/relational/object.rb +546 -0
- data/lib/story_teller/inform/relational/tag.rb +152 -0
- data/lib/story_teller/options.rb +151 -0
- data/lib/story_teller/persistence.rb +340 -0
- data/lib/story_teller/player_character.rb +99 -0
- data/lib/story_teller/privileges.rb +55 -0
- data/lib/story_teller/runtime.rb +381 -0
- data/lib/story_teller/snapshots.rb +412 -0
- data/lib/story_teller/terminal.rb +58 -0
- data/lib/story_teller/version.rb +24 -0
- data/lib/story_teller_cli.rb +34 -0
- metadata +158 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# lib/story_teller/snapshots.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 but freely usable (see license)
|
|
6
|
+
#
|
|
7
|
+
# This file is part of the StoryTeller.
|
|
8
|
+
#
|
|
9
|
+
# The StoryTeller is free software: you can redistribute it and/or
|
|
10
|
+
# modify it under the terms of the GNU General Public License as published
|
|
11
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# The StoryTeller is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with the StoryTeller. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
|
|
22
|
+
require 'json'
|
|
23
|
+
require 'zlib'
|
|
24
|
+
require 'time'
|
|
25
|
+
require 'date'
|
|
26
|
+
require 'base64'
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
# Helps Postgres JSON/JSONB accept Hash/Array inserts in many setups.
|
|
30
|
+
require 'sequel/extensions/pg_json'
|
|
31
|
+
rescue LoadError
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The StoryTeller module
|
|
36
|
+
module StoryTeller
|
|
37
|
+
# The Snapshots module
|
|
38
|
+
module Snapshots
|
|
39
|
+
GzipMagicBytes = [0x1F, 0x8B].freeze
|
|
40
|
+
|
|
41
|
+
# Adjust exclusions as needed
|
|
42
|
+
TableExclusions = %i[
|
|
43
|
+
schema_migrations
|
|
44
|
+
ar_internal_metadata
|
|
45
|
+
sequel_migrations
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
# Snapshot JSON schema:
|
|
49
|
+
# {
|
|
50
|
+
# version: 1,
|
|
51
|
+
# created_at: "ISO8601",
|
|
52
|
+
# tables: [
|
|
53
|
+
# {
|
|
54
|
+
# name: "object",
|
|
55
|
+
# primary_key: "id",
|
|
56
|
+
# self_foreign_key: "parent_id", # optional
|
|
57
|
+
# rows: [ { col: val, ... }, ... ]
|
|
58
|
+
# },
|
|
59
|
+
# ...
|
|
60
|
+
# ]
|
|
61
|
+
# }
|
|
62
|
+
|
|
63
|
+
module_function
|
|
64
|
+
|
|
65
|
+
def gzipped?(filename)
|
|
66
|
+
File.binread(filename, 2).bytes == GzipMagicBytes
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def read_snapshot_file(filename)
|
|
70
|
+
data = if gzipped?(filename)
|
|
71
|
+
Zlib::GzipReader.open(filename, &:read)
|
|
72
|
+
else
|
|
73
|
+
File.read(filename)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
JSON.parse(data, symbolize_names: true)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def world_tables(db)
|
|
80
|
+
db.tables.reject { |t| TableExclusions.include?(t) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def ordered_world_tables(db, only_tables: nil)
|
|
84
|
+
tables = only_tables ? only_tables.map(&:to_sym) : world_tables(db)
|
|
85
|
+
deps = table_dependencies(db, tables)
|
|
86
|
+
topo_sort_tables(tables, deps)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# rubocop: disable Metrics/AbcSize
|
|
90
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
91
|
+
# rubocop: disable Metrics/MethodLength
|
|
92
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
93
|
+
def table_dependencies(db, tables)
|
|
94
|
+
table_set = tables.to_h { |t| [t, true] }
|
|
95
|
+
deps = {}
|
|
96
|
+
tables.each { |t| deps[t] = {} }
|
|
97
|
+
|
|
98
|
+
tables.each do |table|
|
|
99
|
+
fks = safe_foreign_key_list(db, table)
|
|
100
|
+
fks.each do |fk|
|
|
101
|
+
parent = fk[:table]&.to_sym
|
|
102
|
+
next unless parent
|
|
103
|
+
next unless table_set[parent]
|
|
104
|
+
next if parent == table # self-FK handled at row ordering level
|
|
105
|
+
|
|
106
|
+
deps[table][parent] = true
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
deps
|
|
111
|
+
end
|
|
112
|
+
# rubocop: enable Metrics/AbcSize
|
|
113
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
114
|
+
# rubocop: enable Metrics/MethodLength
|
|
115
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
116
|
+
|
|
117
|
+
# rubocop: disable Metrics/AbcSize
|
|
118
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
119
|
+
# rubocop: disable Metrics/MethodLength
|
|
120
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
121
|
+
def topo_sort_tables(tables, deps)
|
|
122
|
+
remaining = tables.to_h { |t| [t, true] }
|
|
123
|
+
incoming = deps.transform_values(&:dup)
|
|
124
|
+
|
|
125
|
+
ordered = []
|
|
126
|
+
loop do
|
|
127
|
+
roots = remaining.keys.select { |t| incoming[t].empty? }.sort_by(&:to_s)
|
|
128
|
+
break if roots.empty?
|
|
129
|
+
|
|
130
|
+
roots.each do |root|
|
|
131
|
+
ordered << root
|
|
132
|
+
remaining.delete(root)
|
|
133
|
+
incoming.delete(root)
|
|
134
|
+
incoming.each_value { |parents| parents.delete(root) }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return ordered if remaining.empty?
|
|
139
|
+
|
|
140
|
+
cycle = remaining.keys.sort_by(&:to_s)
|
|
141
|
+
raise "Foreign key table dependency cycle detected among: #{cycle.join(', ')}"
|
|
142
|
+
end
|
|
143
|
+
# rubocop: enable Metrics/AbcSize
|
|
144
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
145
|
+
# rubocop: enable Metrics/MethodLength
|
|
146
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
147
|
+
|
|
148
|
+
def safe_foreign_key_list(db, table)
|
|
149
|
+
db.foreign_key_list(table)
|
|
150
|
+
rescue Sequel::NotImplemented
|
|
151
|
+
[]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def primary_key_for_table(db, table)
|
|
155
|
+
pk = db.primary_key(table)
|
|
156
|
+
return pk if pk.is_a?(Symbol)
|
|
157
|
+
|
|
158
|
+
# Fallback: detect first PK column from schema
|
|
159
|
+
schema = db.schema(table)
|
|
160
|
+
pk_cols = schema.map { |name, info| info[:primary_key] ? name : nil }.compact
|
|
161
|
+
pk_cols.length == 1 ? pk_cols.first : nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self_fk_column(db, table)
|
|
165
|
+
fks = safe_foreign_key_list(db, table)
|
|
166
|
+
self_fk = fks.find { |fk| fk[:table].to_sym == table.to_sym }
|
|
167
|
+
return nil unless self_fk
|
|
168
|
+
|
|
169
|
+
cols = Array(self_fk[:columns])
|
|
170
|
+
cols.length == 1 ? cols.first : nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# rubocop: disable Metrics/MethodLength
|
|
174
|
+
def normalize_value(value)
|
|
175
|
+
case value
|
|
176
|
+
when Time
|
|
177
|
+
value.utc.iso8601
|
|
178
|
+
when DateTime
|
|
179
|
+
value.to_time.utc.iso8601
|
|
180
|
+
when Date
|
|
181
|
+
value.iso8601
|
|
182
|
+
when BigDecimal
|
|
183
|
+
value.to_s("F")
|
|
184
|
+
when Sequel::SQL::Blob
|
|
185
|
+
{ __blob__: Base64.strict_encode64(value) }
|
|
186
|
+
else
|
|
187
|
+
value
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
# rubocop: enable Metrics/MethodLength
|
|
191
|
+
|
|
192
|
+
def normalize_row(row)
|
|
193
|
+
row.transform_values { |v| normalize_value(v) }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
197
|
+
# rubocop: disable Metrics/MethodLength
|
|
198
|
+
def parse_value(value, type)
|
|
199
|
+
return nil if value.nil?
|
|
200
|
+
|
|
201
|
+
if value.is_a?(Hash) && value.key?(:__blob__)
|
|
202
|
+
return Sequel::SQL::Blob.new(Base64.decode64(value[:__blob__]))
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
case type
|
|
206
|
+
when :datetime, :timestamp
|
|
207
|
+
value.is_a?(String) ? Time.iso8601(value) : value
|
|
208
|
+
when :date
|
|
209
|
+
value.is_a?(String) ? Date.iso8601(value) : value
|
|
210
|
+
else
|
|
211
|
+
value
|
|
212
|
+
end
|
|
213
|
+
rescue ArgumentError
|
|
214
|
+
# If parsing fails, keep original value to avoid hard failure.
|
|
215
|
+
value
|
|
216
|
+
end
|
|
217
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
218
|
+
# rubocop: enable Metrics/MethodLength
|
|
219
|
+
|
|
220
|
+
def denormalize_row(db, table, row)
|
|
221
|
+
schema = db.schema(table)
|
|
222
|
+
types = schema.to_h.transform_values { |info| info[:type] }
|
|
223
|
+
|
|
224
|
+
out = {}
|
|
225
|
+
row.each do |k, v|
|
|
226
|
+
type = types[k.to_sym]
|
|
227
|
+
out[k.to_sym] = parse_value(v, type)
|
|
228
|
+
end
|
|
229
|
+
out
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# rubocop: disable Metrics/AbcSize
|
|
233
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
234
|
+
# rubocop: disable Metrics/MethodLength
|
|
235
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
236
|
+
def order_rows_by_self_fk(rows, pk_col, parent_col)
|
|
237
|
+
return rows if rows.empty?
|
|
238
|
+
return rows unless pk_col && parent_col
|
|
239
|
+
|
|
240
|
+
by_id = {}
|
|
241
|
+
rows.each { |r| by_id[r[pk_col]] = r }
|
|
242
|
+
|
|
243
|
+
in_degree = Hash.new(0)
|
|
244
|
+
children = Hash.new { |h, k| h[k] = [] }
|
|
245
|
+
|
|
246
|
+
rows.each do |r|
|
|
247
|
+
id = r[pk_col]
|
|
248
|
+
parent_id = r[parent_col]
|
|
249
|
+
next if parent_id.nil?
|
|
250
|
+
next unless by_id.key?(parent_id)
|
|
251
|
+
|
|
252
|
+
in_degree[id] += 1
|
|
253
|
+
children[parent_id] << id
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
queue = rows.select { |r| in_degree[r[pk_col]].zero? }.map { |r| r[pk_col] }.sort
|
|
257
|
+
|
|
258
|
+
ordered = []
|
|
259
|
+
until queue.empty?
|
|
260
|
+
id = queue.shift
|
|
261
|
+
row = by_id[id]
|
|
262
|
+
ordered << row if row
|
|
263
|
+
|
|
264
|
+
children[id].sort.each do |child_id|
|
|
265
|
+
in_degree[child_id] -= 1
|
|
266
|
+
queue << child_id if in_degree[child_id].zero?
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# If cycle or missing nodes, append remaining deterministically.
|
|
271
|
+
remaining_ids = by_id.keys - ordered.map { |r| r[pk_col] }
|
|
272
|
+
remaining_ids.sort.each { |id| ordered << by_id[id] }
|
|
273
|
+
|
|
274
|
+
ordered
|
|
275
|
+
end
|
|
276
|
+
# rubocop: enable Metrics/AbcSize
|
|
277
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
278
|
+
# rubocop: enable Metrics/MethodLength
|
|
279
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
280
|
+
|
|
281
|
+
# rubocop: disable Metrics/AbcSize
|
|
282
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
283
|
+
# rubocop: disable Metrics/MethodLength
|
|
284
|
+
def export_to_file(filename)
|
|
285
|
+
db = Sequel::Model.db
|
|
286
|
+
tables = ordered_world_tables(db)
|
|
287
|
+
|
|
288
|
+
tables_payload = tables.map do |table|
|
|
289
|
+
pk = primary_key_for_table(db, table)
|
|
290
|
+
s_fk = self_fk_column(db, table)
|
|
291
|
+
|
|
292
|
+
rows = db[table].all
|
|
293
|
+
# Stable base order, then parent-first for self-FK if applicable.
|
|
294
|
+
rows = rows.sort_by { |r| r[pk] || 0 } if pk
|
|
295
|
+
rows = order_rows_by_self_fk(rows, pk, s_fk)
|
|
296
|
+
rows = rows.map { |r| normalize_row(r) }
|
|
297
|
+
|
|
298
|
+
{
|
|
299
|
+
name: table.to_s,
|
|
300
|
+
primary_key: pk&.to_s,
|
|
301
|
+
self_foreign_key: s_fk&.to_s,
|
|
302
|
+
rows: rows
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
data = {
|
|
307
|
+
version: 1,
|
|
308
|
+
created_at: Time.now.utc.iso8601,
|
|
309
|
+
tables: tables_payload
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
File.open(filename, 'wb') do |file|
|
|
313
|
+
Zlib::GzipWriter.wrap(file) { |gz| gz.write(JSON.generate(data)) }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
filename
|
|
317
|
+
end
|
|
318
|
+
# rubocop: enable Metrics/AbcSize
|
|
319
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
320
|
+
# rubocop: enable Metrics/MethodLength
|
|
321
|
+
|
|
322
|
+
# rubocop: disable Metrics/MethodLength
|
|
323
|
+
def truncate_tables(db, tables)
|
|
324
|
+
if db.database_type == :postgres
|
|
325
|
+
existing = tables.select { |t| db.table_exists?(t) }
|
|
326
|
+
return if existing.empty?
|
|
327
|
+
|
|
328
|
+
table_list = existing.map { |t| db.literal(t) }.join(", ")
|
|
329
|
+
db.run("TRUNCATE TABLE #{table_list} RESTART IDENTITY CASCADE")
|
|
330
|
+
return
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# For other DBs, truncate/delete children first (reverse dependency order).
|
|
334
|
+
tables.reverse_each do |table|
|
|
335
|
+
next unless db.table_exists?(table)
|
|
336
|
+
db[table].truncate
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
# rubocop: enable Metrics/MethodLength
|
|
340
|
+
|
|
341
|
+
# rubocop: disable Metrics/AbcSize
|
|
342
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
343
|
+
# rubocop: disable Metrics/MethodLength
|
|
344
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
345
|
+
def import_from_file(filename)
|
|
346
|
+
db = Sequel::Model.db
|
|
347
|
+
data = read_snapshot_file(filename)
|
|
348
|
+
|
|
349
|
+
unless data[:version] == 1
|
|
350
|
+
raise "Unsupported snapshot version: #{data[:version].inspect}"
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
snapshot_tables = Array(data[:tables])
|
|
354
|
+
table_rows_by_name = snapshot_tables.to_h do |t|
|
|
355
|
+
[t[:name].to_s, Array(t[:rows])]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
table_syms = snapshot_tables.map { |t| t[:name].to_sym }
|
|
359
|
+
ordered = ordered_world_tables(db, only_tables: table_syms)
|
|
360
|
+
|
|
361
|
+
db.transaction do
|
|
362
|
+
truncate_tables(db, ordered)
|
|
363
|
+
|
|
364
|
+
ordered.each do |table|
|
|
365
|
+
rows = table_rows_by_name[table.to_s] || []
|
|
366
|
+
next if rows.empty?
|
|
367
|
+
next unless db.table_exists?(table)
|
|
368
|
+
|
|
369
|
+
denorm = rows.map { |r| denormalize_row(db, table, r) }
|
|
370
|
+
db[table].multi_insert(denorm) unless denorm.empty?
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
reseed_sequences(db, ordered)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
filename
|
|
377
|
+
end
|
|
378
|
+
# rubocop: enable Metrics/AbcSize
|
|
379
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
380
|
+
# rubocop: enable Metrics/MethodLength
|
|
381
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
382
|
+
|
|
383
|
+
SelectSerialSequence = 'SELECT pg_get_serial_sequence(?, ?) AS seq'.freeze
|
|
384
|
+
SelectSetvalTemplate = 'SELECT setval(%<sequence>s, %<max_id>s, true)'.freeze
|
|
385
|
+
|
|
386
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
387
|
+
# rubocop: disable Metrics/MethodLength
|
|
388
|
+
def reseed_sequences(db, tables)
|
|
389
|
+
return unless db.database_type == :postgres
|
|
390
|
+
|
|
391
|
+
tables.each do |table|
|
|
392
|
+
next unless db.table_exists?(table)
|
|
393
|
+
|
|
394
|
+
pk = primary_key_for_table(db, table)
|
|
395
|
+
next unless pk
|
|
396
|
+
|
|
397
|
+
max_id = db[table].max(pk)
|
|
398
|
+
next unless max_id
|
|
399
|
+
|
|
400
|
+
seq_row = db.fetch(SelectSerialSequence, table.to_s, pk.to_s).first
|
|
401
|
+
seq_name = seq_row && seq_row[:seq]
|
|
402
|
+
next unless seq_name
|
|
403
|
+
|
|
404
|
+
db.run(format(SelectSetvalTemplate, sequence: db.literal(seq_name), max_id: max_id))
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
408
|
+
# rubocop: enable Metrics/MethodLength
|
|
409
|
+
end
|
|
410
|
+
# module Snapshots
|
|
411
|
+
end
|
|
412
|
+
# module StoryTeller
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# lib/story_teller/terminal.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 but freely usable (see license)
|
|
6
|
+
#
|
|
7
|
+
# This file is part of the StoryTeller.
|
|
8
|
+
#
|
|
9
|
+
# The StoryTeller is free software: you can redistribute it and/or
|
|
10
|
+
# modify it under the terms of the GNU General Public License as published
|
|
11
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# The StoryTeller is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with the StoryTeller. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
|
|
22
|
+
require 'io/console'
|
|
23
|
+
|
|
24
|
+
# module StoryTeller
|
|
25
|
+
module StoryTeller
|
|
26
|
+
# module Terminal
|
|
27
|
+
module Terminal
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
MINIMUM_WORD_WRAP = 1
|
|
31
|
+
|
|
32
|
+
def word_wrap(output = $stdout)
|
|
33
|
+
terminal_columns = columns(output)
|
|
34
|
+
return default_word_wrap if terminal_columns.nil? || terminal_columns.to_i <= 0
|
|
35
|
+
|
|
36
|
+
[terminal_columns.to_i - 1, MINIMUM_WORD_WRAP].max.tap do |n|
|
|
37
|
+
log.debug "word_wrap: #{n}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def default_word_wrap
|
|
42
|
+
StoryTeller::Config.defaults[:default_word_wrap]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def columns(output = $stdout)
|
|
46
|
+
return output.winsize[1] if output.respond_to?(:winsize) && output.tty?
|
|
47
|
+
|
|
48
|
+
console = ::IO.console
|
|
49
|
+
return console.winsize[1] if console.respond_to?(:winsize)
|
|
50
|
+
|
|
51
|
+
nil
|
|
52
|
+
rescue IOError, SystemCallError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
# module Terminal
|
|
57
|
+
end
|
|
58
|
+
# module StoryTeller
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# lib/story_teller/version.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2026 but freely usable (see license)
|
|
6
|
+
#
|
|
7
|
+
# This file is part of the StoryTeller.
|
|
8
|
+
#
|
|
9
|
+
# The StoryTeller is free software: you can redistribute it and/or
|
|
10
|
+
# modify it under the terms of the GNU General Public License as published
|
|
11
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# The StoryTeller is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with the StoryTeller. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
|
|
22
|
+
module StoryTellerCli
|
|
23
|
+
VERSION = '1.1.3'.freeze
|
|
24
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# lib/story_teller_cli.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 but freely usable (see license)
|
|
6
|
+
#
|
|
7
|
+
# This file is part of the StoryTeller.
|
|
8
|
+
#
|
|
9
|
+
# The StoryTeller is free software: you can redistribute it and/or
|
|
10
|
+
# modify it under the terms of the GNU General Public License as published
|
|
11
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# The StoryTeller is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with the StoryTeller. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
|
|
22
|
+
require 'story_teller'
|
|
23
|
+
|
|
24
|
+
require_relative 'story_teller/application'
|
|
25
|
+
require_relative 'story_teller/config'
|
|
26
|
+
require_relative 'story_teller/options'
|
|
27
|
+
require_relative 'story_teller/privileges'
|
|
28
|
+
require_relative 'story_teller/runtime'
|
|
29
|
+
require_relative 'story_teller/game'
|
|
30
|
+
require_relative 'story_teller/persistence'
|
|
31
|
+
require_relative 'story_teller/inform/models'
|
|
32
|
+
require_relative 'story_teller/snapshots'
|
|
33
|
+
require_relative 'story_teller/terminal'
|
|
34
|
+
require_relative 'story_teller/cli'
|
metadata
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: story-teller
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nels Nelson
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: curses
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 1.4.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 1.4.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: inform-runtime
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.5.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.5.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: nokogiri
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 1.13.1
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 1.13.1
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: pg
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 1.6.2
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 1.6.2
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: sequel
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 5.58.0
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 5.58.0
|
|
82
|
+
description: The Story Teller is a command-line interface software runtime program
|
|
83
|
+
for playing interactive-fiction games which use the Inform 6 Ruby Port.
|
|
84
|
+
email: nels@nelsnelson.org
|
|
85
|
+
executables:
|
|
86
|
+
- inform.rb
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- LICENSE
|
|
91
|
+
- README.md
|
|
92
|
+
- Rakefile
|
|
93
|
+
- config/database.yml
|
|
94
|
+
- exe/inform.rb
|
|
95
|
+
- game/config.yml
|
|
96
|
+
- game/example.inf
|
|
97
|
+
- game/example.rb
|
|
98
|
+
- game/forms/example_form.rb
|
|
99
|
+
- game/grammar/admin.inf.rb
|
|
100
|
+
- game/grammar/builder.inf.rb
|
|
101
|
+
- game/grammar/game_grammar.inf.rb
|
|
102
|
+
- game/grammar/meta.inf.rb
|
|
103
|
+
- game/languages/english.rb
|
|
104
|
+
- game/models/example_model.rb
|
|
105
|
+
- game/modules/example_module.rb
|
|
106
|
+
- game/modules/parser_extensions.rb
|
|
107
|
+
- game/rules/example_state.rb
|
|
108
|
+
- game/scripts/example_script.rb
|
|
109
|
+
- game/topics/example_topic.rb
|
|
110
|
+
- game/verbs/game_verbs.rb
|
|
111
|
+
- game/verbs/metaverbs.rb
|
|
112
|
+
- lib/story_teller/application.rb
|
|
113
|
+
- lib/story_teller/cli.rb
|
|
114
|
+
- lib/story_teller/color.rb
|
|
115
|
+
- lib/story_teller/config.rb
|
|
116
|
+
- lib/story_teller/curses_adapter.rb
|
|
117
|
+
- lib/story_teller/database.rb
|
|
118
|
+
- lib/story_teller/game.rb
|
|
119
|
+
- lib/story_teller/game/loader.rb
|
|
120
|
+
- lib/story_teller/inform/models.rb
|
|
121
|
+
- lib/story_teller/inform/relational/link.rb
|
|
122
|
+
- lib/story_teller/inform/relational/module.rb
|
|
123
|
+
- lib/story_teller/inform/relational/object.rb
|
|
124
|
+
- lib/story_teller/inform/relational/tag.rb
|
|
125
|
+
- lib/story_teller/options.rb
|
|
126
|
+
- lib/story_teller/persistence.rb
|
|
127
|
+
- lib/story_teller/player_character.rb
|
|
128
|
+
- lib/story_teller/privileges.rb
|
|
129
|
+
- lib/story_teller/runtime.rb
|
|
130
|
+
- lib/story_teller/snapshots.rb
|
|
131
|
+
- lib/story_teller/terminal.rb
|
|
132
|
+
- lib/story_teller/version.rb
|
|
133
|
+
- lib/story_teller_cli.rb
|
|
134
|
+
homepage: https://rubygems.org/gems/story-teller
|
|
135
|
+
licenses:
|
|
136
|
+
- GPL-3.0-or-later
|
|
137
|
+
metadata:
|
|
138
|
+
source_code_uri: https://gitlab.com/nelsnelson/story-teller
|
|
139
|
+
bug_tracker_uri: https://gitlab.com/nelsnelson/story-teller/issues
|
|
140
|
+
rubygems_mfa_required: 'true'
|
|
141
|
+
rdoc_options: []
|
|
142
|
+
require_paths:
|
|
143
|
+
- lib
|
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
145
|
+
requirements:
|
|
146
|
+
- - ">="
|
|
147
|
+
- !ruby/object:Gem::Version
|
|
148
|
+
version: 4.0.0
|
|
149
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
150
|
+
requirements:
|
|
151
|
+
- - ">="
|
|
152
|
+
- !ruby/object:Gem::Version
|
|
153
|
+
version: '0'
|
|
154
|
+
requirements: []
|
|
155
|
+
rubygems_version: 4.0.11
|
|
156
|
+
specification_version: 4
|
|
157
|
+
summary: Story Teller for playing Inform 6 Ruby games
|
|
158
|
+
test_files: []
|