@ak--47/dungeon-master 1.0.0 → 1.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.
Files changed (104) hide show
  1. package/README.md +7 -7
  2. package/dungeons/technical/ad-spend-schema.json +128 -0
  3. package/dungeons/technical/ad-spend.js +116 -0
  4. package/dungeons/technical/anonymous-users-schema.json +92 -0
  5. package/dungeons/technical/anonymous-users.js +102 -0
  6. package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
  7. package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +12 -5
  8. package/dungeons/technical/experiments-schema.json +203 -0
  9. package/dungeons/technical/experiments.js +168 -0
  10. package/dungeons/technical/foobar-schema.json +362 -0
  11. package/dungeons/{foobar.js → technical/foobar.js} +3 -3
  12. package/dungeons/technical/group-analytics-schema.json +241 -0
  13. package/dungeons/technical/group-analytics.js +151 -0
  14. package/dungeons/technical/mirror-strategies-schema.json +84 -0
  15. package/dungeons/technical/mirror-strategies.js +126 -0
  16. package/dungeons/technical/nested-objects-schema.json +145 -0
  17. package/dungeons/technical/nested-objects.js +207 -0
  18. package/dungeons/technical/retention-cadence-schema.json +37 -0
  19. package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +2 -3
  20. package/dungeons/technical/sanity-schema.json +185 -0
  21. package/dungeons/{sanity.js → technical/sanity.js} +4 -2
  22. package/dungeons/technical/scale-test-schema.json +70 -0
  23. package/dungeons/technical/scale-test.js +90 -0
  24. package/dungeons/technical/scd-schema.json +467 -0
  25. package/dungeons/{scd.js → technical/scd.js} +20 -10
  26. package/dungeons/technical/simple-schema.json +362 -0
  27. package/dungeons/{simple.js → technical/simple.js} +7 -7
  28. package/dungeons/technical/simplest-schema.json +724 -0
  29. package/dungeons/{simplest.js → technical/simplest.js} +7 -7
  30. package/dungeons/technical/text-generation-schema.json +1062 -0
  31. package/dungeons/{text-generation.js → technical/text-generation.js} +24 -11
  32. package/dungeons/vertical/community-schema.json +579 -0
  33. package/dungeons/vertical/community.js +794 -0
  34. package/dungeons/vertical/devtools-schema.json +601 -0
  35. package/dungeons/vertical/devtools.js +843 -0
  36. package/dungeons/vertical/ecommerce-schema.json +604 -0
  37. package/dungeons/{ecommerce.js → vertical/ecommerce.js} +43 -5
  38. package/dungeons/vertical/education-schema.json +5686 -0
  39. package/dungeons/{education.js → vertical/education.js} +70 -39
  40. package/dungeons/vertical/fintech-schema.json +630 -0
  41. package/dungeons/{fintech.js → vertical/fintech.js} +107 -56
  42. package/dungeons/vertical/fitness-schema.json +530 -0
  43. package/dungeons/vertical/fitness.js +808 -0
  44. package/dungeons/vertical/food-delivery-schema.json +36728 -0
  45. package/dungeons/{food.js → vertical/food-delivery.js} +67 -26
  46. package/dungeons/vertical/gaming-schema.json +438 -0
  47. package/dungeons/{gaming.js → vertical/gaming.js} +29 -16
  48. package/dungeons/vertical/healthcare-schema.json +549 -0
  49. package/dungeons/vertical/healthcare.js +775 -0
  50. package/dungeons/vertical/insurance-application-schema.json +485 -0
  51. package/dungeons/{insurance-application.js → vertical/insurance-application.js} +41 -31
  52. package/dungeons/vertical/logistics-schema.json +574 -0
  53. package/dungeons/vertical/logistics.js +799 -0
  54. package/dungeons/vertical/marketplace-schema.json +533 -0
  55. package/dungeons/vertical/marketplace.js +850 -0
  56. package/dungeons/vertical/media-schema.json +4749 -0
  57. package/dungeons/{media.js → vertical/media.js} +103 -19
  58. package/dungeons/vertical/rpg-schema.json +2491 -0
  59. package/dungeons/{rpg.js → vertical/rpg.js} +87 -60
  60. package/dungeons/vertical/sass-schema.json +3128 -0
  61. package/dungeons/{sass.js → vertical/sass.js} +65 -26
  62. package/dungeons/vertical/social-schema.json +620 -0
  63. package/dungeons/{social.js → vertical/social.js} +76 -17
  64. package/dungeons/vertical/travel-schema.json +580 -0
  65. package/dungeons/vertical/travel.js +742 -0
  66. package/index.js +20 -16
  67. package/lib/core/config-validator.js +286 -1
  68. package/lib/core/context.js +16 -5
  69. package/lib/core/storage.js +2 -0
  70. package/lib/generators/adspend.js +3 -3
  71. package/lib/generators/events.js +141 -14
  72. package/lib/generators/funnels.js +67 -7
  73. package/lib/generators/text.js +2 -2
  74. package/lib/orchestrators/user-loop.js +444 -22
  75. package/lib/templates/soup-presets.js +2 -2
  76. package/lib/utils/utils.js +4 -5
  77. package/package.json +4 -3
  78. package/scripts/dungeon-to-json.mjs +163 -0
  79. package/scripts/extract-dungeon-schema.mjs +343 -0
  80. package/scripts/json-to-dungeon.mjs +200 -0
  81. package/scripts/run-dungeon.mjs +76 -0
  82. package/scripts/run-many.mjs +118 -0
  83. package/scripts/verify-runner.mjs +42 -0
  84. package/types.d.ts +316 -1
  85. package/dungeons/array-of-object-lookup-schema.json +0 -327
  86. package/dungeons/ecommerce-schema.json +0 -462
  87. package/dungeons/education-schema.json +0 -2409
  88. package/dungeons/fintech-schema.json +0 -14034
  89. package/dungeons/foobar-schema.json +0 -403
  90. package/dungeons/food-delivery-schema.json +0 -192
  91. package/dungeons/food-delivery.js +0 -602
  92. package/dungeons/food-schema.json +0 -1152
  93. package/dungeons/gaming-schema.json +0 -1270
  94. package/dungeons/insurance-application-schema.json +0 -204
  95. package/dungeons/media-schema.json +0 -906
  96. package/dungeons/retention-cadence-schema.json +0 -78
  97. package/dungeons/rpg-schema.json +0 -4526
  98. package/dungeons/sanity-schema.json +0 -255
  99. package/dungeons/sass-schema.json +0 -1291
  100. package/dungeons/scd-schema.json +0 -919
  101. package/dungeons/simple-schema.json +0 -608
  102. package/dungeons/simplest-schema.json +0 -1418
  103. package/dungeons/social-schema.json +0 -1118
  104. package/dungeons/text-generation-schema.json +0 -3096
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Converts a JavaScript dungeon file to JSON that can be loaded into the UI
5
+ * Usage: node scripts/dungeon-to-json.js <input.js> [output-name]
6
+ */
7
+
8
+ import { writeFileSync } from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ // Get command line arguments
15
+ const args = process.argv.slice(2);
16
+
17
+ if (args.length === 0) {
18
+ console.error('❌ Error: Please provide an input JavaScript file');
19
+ console.log('Usage: node scripts/dungeon-to-json.js <input.js> [output-name]');
20
+ console.log('Example: node scripts/dungeon-to-json.js ./dungeons/my-dungeon.js my-dungeon');
21
+ process.exit(1);
22
+ }
23
+
24
+ const inputPath = path.resolve(args[0]);
25
+ const outputName = args[1] || path.basename(inputPath, '.js');
26
+
27
+ try {
28
+ // Import the JavaScript module
29
+ console.log(`📖 Loading ${inputPath}...`);
30
+ const module = await import(`file://${inputPath}`);
31
+ const config = module.default;
32
+
33
+ if (!config) {
34
+ throw new Error('No default export found in the module');
35
+ }
36
+
37
+ // Extract hooks if they exist
38
+ const hooksFunction = config.hook;
39
+ const hooksString = hooksFunction ? hooksFunction.toString() : null;
40
+
41
+ // Create a clean config without the hook function
42
+ const cleanConfig = { ...config };
43
+ delete cleanConfig.hook;
44
+
45
+ // Convert to JSON-serializable format
46
+ console.log('🔄 Converting to JSON format...');
47
+ const schema = convertToJSON(cleanConfig);
48
+
49
+ // Create the dungeon state object (same format as UI saves)
50
+ const dungeonState = {
51
+ schema,
52
+ hooks: hooksString,
53
+ timestamp: new Date().toISOString(),
54
+ version: '4.0'
55
+ };
56
+
57
+ // Write to JSON file
58
+ const outputPath = path.join(path.dirname(inputPath), `${outputName}.json`);
59
+ writeFileSync(outputPath, JSON.stringify(dungeonState, null, 2), 'utf-8');
60
+
61
+ console.log(`✅ Successfully created ${outputPath}`);
62
+ console.log(`\n💡 You can now load this file in the dungeon-master UI:`);
63
+ console.log(` Click "Load" and select ${outputName}.json`);
64
+
65
+ } catch (error) {
66
+ console.error('❌ Error:', error.message);
67
+ console.error(error.stack);
68
+ process.exit(1);
69
+ }
70
+
71
+ /**
72
+ * Convert JavaScript config to JSON-serializable format
73
+ * Detects functions and converts them to object representation
74
+ */
75
+ function convertToJSON(value) {
76
+ // Null/undefined
77
+ if (value === null || value === undefined) {
78
+ return null;
79
+ }
80
+
81
+ // Primitives
82
+ if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
83
+ return value;
84
+ }
85
+
86
+ // Functions - convert to object representation
87
+ if (typeof value === 'function') {
88
+ return convertFunctionToObject(value);
89
+ }
90
+
91
+ // Arrays
92
+ if (Array.isArray(value)) {
93
+ return value.map(item => convertToJSON(item));
94
+ }
95
+
96
+ // Objects
97
+ if (typeof value === 'object') {
98
+ const result = {};
99
+ for (const [key, val] of Object.entries(value)) {
100
+ result[key] = convertToJSON(val);
101
+ }
102
+ return result;
103
+ }
104
+
105
+ // Fallback
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Convert a function to its object representation
111
+ * Tries to detect the function type and extract parameters
112
+ */
113
+ function convertFunctionToObject(func) {
114
+ const funcString = func.toString();
115
+
116
+ // Arrow function
117
+ if (funcString.startsWith('(') || funcString.startsWith('_') || funcString.includes('=>')) {
118
+ return {
119
+ functionName: 'arrow',
120
+ body: funcString
121
+ };
122
+ }
123
+
124
+ // Bound chance methods (e.g., chance.name.bind(chance))
125
+ if (funcString.includes('.bind(')) {
126
+ const match = funcString.match(/chance\.(\w+)\.bind/);
127
+ if (match) {
128
+ return {
129
+ functionName: `chance.${match[1]}`,
130
+ args: []
131
+ };
132
+ }
133
+ }
134
+
135
+ // Try to detect common utility functions
136
+ // This is a best-effort approach - some complex functions might not be detected
137
+ const commonFunctions = [
138
+ 'weighNumRange',
139
+ 'weighArray',
140
+ 'weighChoices',
141
+ 'pickAWinner',
142
+ 'date',
143
+ 'integer',
144
+ 'uid',
145
+ 'comma'
146
+ ];
147
+
148
+ for (const fnName of commonFunctions) {
149
+ if (funcString.includes(fnName)) {
150
+ // Extract args (this is simplified - real parsing would be more complex)
151
+ return {
152
+ functionName: fnName,
153
+ args: [] // Args would need to be extracted, but that's complex
154
+ };
155
+ }
156
+ }
157
+
158
+ // Generic function - just store as arrow function
159
+ return {
160
+ functionName: 'arrow',
161
+ body: funcString
162
+ };
163
+ }
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Extracts a complete schema from a JavaScript dungeon file.
5
+ *
6
+ * Output contains everything about the dungeon EXCEPT:
7
+ * - hook function (runtime logic, not schema)
8
+ * - credentials (token, serviceAccount, serviceSecret, projectId)
9
+ * - execution params (seed, format, gzip, verbose, concurrency, batchSize, writeToDisk, region)
10
+ *
11
+ * Property values are simplified:
12
+ * - Plain arrays → deduplicated
13
+ * - weighNumRange(min,max) arrays → { "$range": [min, max] }
14
+ * - Functions (pickAWinner, weighChoices, etc.) → sampled and deduplicated
15
+ * - High-cardinality / complex values → type hint
16
+ *
17
+ * Usage:
18
+ * node scripts/extract-dungeon-schema.mjs <input.js> [output.json]
19
+ * node scripts/extract-dungeon-schema.mjs dungeons/vertical/*.js # batch mode
20
+ */
21
+
22
+ import { writeFileSync } from 'fs';
23
+ import path from 'path';
24
+ import { fileURLToPath } from 'url';
25
+ import { initChance } from '../lib/utils/utils.js';
26
+
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
+ const args = process.argv.slice(2);
29
+
30
+ if (args.length === 0) {
31
+ console.error('Usage: node scripts/extract-dungeon-schema.mjs <dungeon.js> [output.json]');
32
+ console.error(' node scripts/extract-dungeon-schema.mjs dungeons/vertical/*.js');
33
+ process.exit(1);
34
+ }
35
+
36
+ // Batch mode: multiple files
37
+ if (args.length > 2 || (args.length > 1 && args[1].endsWith('.js'))) {
38
+ for (const file of args) {
39
+ if (!file.endsWith('.js') && !file.endsWith('.mjs')) continue;
40
+ try {
41
+ await processOne(file);
42
+ } catch (e) {
43
+ console.error(` ❌ ${path.basename(file)}: ${e.message}`);
44
+ }
45
+ }
46
+ } else {
47
+ await processOne(args[0], args[1]);
48
+ }
49
+
50
+ async function processOne(inputFile, outputFile) {
51
+ const inputPath = path.resolve(inputFile);
52
+ const baseName = path.basename(inputPath, '.js');
53
+ const outputPath = outputFile
54
+ ? path.resolve(outputFile)
55
+ : path.join(path.dirname(inputPath), `${baseName}-schema.json`);
56
+
57
+ // Seed RNG for deterministic sampling of function-based properties
58
+ initChance('schema-extraction');
59
+
60
+ const mod = await import(`file://${inputPath}`);
61
+ const config = mod.default;
62
+ if (!config) throw new Error('No default export');
63
+
64
+ const schema = {};
65
+
66
+ // ── Events ──────────────────────────────────────────────────────
67
+ if (config.events?.length) {
68
+ schema.events = config.events.map(evt => {
69
+ const entry = { event: evt.event };
70
+ if (evt.weight && evt.weight !== 1) entry.weight = evt.weight;
71
+ if (evt.isFirstEvent) entry.isFirstEvent = true;
72
+ if (evt.isChurnEvent) entry.isChurnEvent = true;
73
+ if (evt.returnLikelihood !== undefined) entry.returnLikelihood = evt.returnLikelihood;
74
+ if (evt.isStrictEvent) entry.isStrictEvent = true;
75
+ if (evt.isSessionStartEvent) entry.isSessionStartEvent = true;
76
+ if (evt.properties && Object.keys(evt.properties).length) {
77
+ const props = extractProps(evt.properties);
78
+ if (Object.keys(props).length) entry.properties = props;
79
+ }
80
+ return entry;
81
+ });
82
+ }
83
+
84
+ // ── Funnels ─────────────────────────────────────────────────────
85
+ if (config.funnels?.length) {
86
+ schema.funnels = config.funnels.map(f => {
87
+ const entry = { sequence: f.sequence };
88
+ if (f.name) entry.name = f.name;
89
+ if (f.isFirstFunnel) entry.isFirstFunnel = true;
90
+ if (f.conversionRate !== undefined) entry.conversionRate = f.conversionRate;
91
+ if (f.timeToConvert !== undefined) entry.timeToConvert = f.timeToConvert;
92
+ if (f.order && f.order !== 'sequential') entry.order = f.order;
93
+ if (f.weight && f.weight !== 1) entry.weight = f.weight;
94
+ if (f.requireRepeats) entry.requireRepeats = true;
95
+ if (f.experiment) entry.experiment = true;
96
+ if (f.bindPropsIndex !== undefined) entry.bindPropsIndex = f.bindPropsIndex;
97
+ if (f.conditions) entry.conditions = f.conditions;
98
+ if (f.props && Object.keys(f.props).length) {
99
+ const fp = extractProps(f.props);
100
+ if (Object.keys(fp).length) entry.props = fp;
101
+ }
102
+ return entry;
103
+ });
104
+ }
105
+
106
+ // ── Super props ─────────────────────────────────────────────────
107
+ if (config.superProps && Object.keys(config.superProps).length) {
108
+ const sp = extractProps(config.superProps);
109
+ if (Object.keys(sp).length) schema.superProps = sp;
110
+ }
111
+
112
+ // ── User props ──────────────────────────────────────────────────
113
+ if (config.userProps && Object.keys(config.userProps).length) {
114
+ const up = extractProps(config.userProps);
115
+ if (Object.keys(up).length) schema.userProps = up;
116
+ }
117
+
118
+ // ── Group keys ──────────────────────────────────────────────────
119
+ if (config.groupKeys?.length) {
120
+ schema.groupKeys = config.groupKeys.map(gk => {
121
+ if (Array.isArray(gk)) {
122
+ const entry = { key: gk[0], count: gk[1] };
123
+ if (gk[2]) entry.affectsEvents = gk[2];
124
+ return entry;
125
+ }
126
+ return gk;
127
+ });
128
+ }
129
+
130
+ // ── Group props ─────────────────────────────────────────────────
131
+ if (config.groupProps && Object.keys(config.groupProps).length) {
132
+ schema.groupProps = {};
133
+ for (const [groupKey, props] of Object.entries(config.groupProps)) {
134
+ schema.groupProps[groupKey] = extractProps(props);
135
+ }
136
+ }
137
+
138
+ // ── SCD props ───────────────────────────────────────────────────
139
+ if (config.scdProps && Object.keys(config.scdProps).length) {
140
+ schema.scdProps = {};
141
+ for (const [key, scd] of Object.entries(config.scdProps)) {
142
+ const entry = {};
143
+ entry.type = scd.type || "user";
144
+ if (scd.frequency) entry.frequency = scd.frequency;
145
+ if (scd.timing) entry.timing = scd.timing;
146
+ if (scd.max !== undefined) entry.max = scd.max;
147
+ if (scd.values) {
148
+ const simplified = simplifyValue(scd.values);
149
+ if (simplified) entry.values = simplified;
150
+ }
151
+ schema.scdProps[key] = entry;
152
+ }
153
+ }
154
+
155
+ writeFileSync(outputPath, JSON.stringify(schema, null, 2) + '\n', 'utf-8');
156
+ console.log(` ✅ ${baseName}-schema.json`);
157
+ }
158
+
159
+ // ── Property extraction ──────────────────────────────────────────────
160
+
161
+ /**
162
+ * Extracts a simplified property map from a dungeon properties object.
163
+ * Returns { key: simplifiedValue } for each property.
164
+ * Keys are always preserved — complex values get a type hint instead of being omitted.
165
+ */
166
+ function extractProps(propsObj) {
167
+ const result = {};
168
+ for (const [key, value] of Object.entries(propsObj)) {
169
+ const simplified = simplifyValue(value);
170
+ if (simplified !== null) {
171
+ result[key] = simplified;
172
+ } else {
173
+ // Always keep the key — use a type hint for unresolvable values
174
+ result[key] = inferTypeHint(value);
175
+ }
176
+ }
177
+ return result;
178
+ }
179
+
180
+ /**
181
+ * Infer a type hint for a value we can't fully resolve.
182
+ * Tries calling the function first; falls back to string analysis.
183
+ */
184
+ function inferTypeHint(value) {
185
+ if (typeof value === 'function') {
186
+ // Try calling to detect actual output type
187
+ try {
188
+ const result = value();
189
+ if (typeof result === 'function') {
190
+ try {
191
+ const inner = result();
192
+ if (Array.isArray(inner)) {
193
+ // Drill through nested arrays to find the first object
194
+ let sample = inner;
195
+ while (Array.isArray(sample) && sample.length > 0 && Array.isArray(sample[0])) {
196
+ sample = sample[0];
197
+ }
198
+ if (sample.length > 0 && typeof sample[0] === 'object' && sample[0] !== null && !Array.isArray(sample[0])) {
199
+ return { "$type": "object[]", "$keys": Object.keys(sample[0]) };
200
+ }
201
+ return { "$type": "array" };
202
+ }
203
+ return { "$type": typeof inner };
204
+ } catch { return { "$type": "function" }; }
205
+ }
206
+ if (typeof result === 'string') return { "$type": "string" };
207
+ if (typeof result === 'number') return { "$type": "number" };
208
+ if (typeof result === 'boolean') return { "$type": "boolean" };
209
+ if (Array.isArray(result)) return { "$type": "array" };
210
+ return { "$type": typeof result };
211
+ } catch { /* can't call — fall through to string analysis */ }
212
+
213
+ // String analysis fallback
214
+ const fnStr = value.toString();
215
+ if (fnStr.includes('guid') || fnStr.includes('uuid')) return { "$type": "string" };
216
+ if (fnStr.includes('integer')) return { "$type": "number" };
217
+ return { "$type": "unknown" };
218
+ }
219
+ if (Array.isArray(value)) {
220
+ if (value.length > 0 && typeof value[0] === 'object') return { "$type": "object[]" };
221
+ return { "$type": "array" };
222
+ }
223
+ return { "$type": typeof value };
224
+ }
225
+
226
+ /**
227
+ * Simplify a property value to its schema representation.
228
+ * Returns null if the value is too complex or high-cardinality to represent.
229
+ */
230
+ function simplifyValue(value) {
231
+ // Primitives
232
+ if (typeof value === 'string' || typeof value === 'boolean') return [value];
233
+ if (typeof value === 'number') return [value];
234
+
235
+ // Arrays (plain values or weighNumRange output)
236
+ if (Array.isArray(value)) {
237
+ return simplifyArray(value);
238
+ }
239
+
240
+ // Functions: pickAWinner, weighChoices, chance bindings, custom generators
241
+ if (typeof value === 'function') {
242
+ return simplifyFunction(value);
243
+ }
244
+
245
+ return null;
246
+ }
247
+
248
+ /**
249
+ * Simplify an array value. Detects numeric ranges vs categorical arrays.
250
+ */
251
+ function simplifyArray(arr) {
252
+ if (arr.length === 0) return null;
253
+
254
+ // Check element types
255
+ const types = new Set(arr.map(v => typeof v));
256
+
257
+ // All numbers — likely weighNumRange output if large, or small explicit list
258
+ if (types.size === 1 && types.has('number')) {
259
+ if (arr.length > 10) {
260
+ // Numeric range (weighNumRange produces 50 elements)
261
+ const min = Math.min(...arr);
262
+ const max = Math.max(...arr);
263
+ // Round to clean integers if values are close to integers
264
+ const roundedMin = Number.isInteger(min) ? min : Math.round(min * 100) / 100;
265
+ const roundedMax = Number.isInteger(max) ? max : Math.round(max * 100) / 100;
266
+ return { "$range": [roundedMin, roundedMax] };
267
+ }
268
+ // Small explicit numeric list — deduplicate
269
+ return [...new Set(arr)];
270
+ }
271
+
272
+ // All strings — deduplicate
273
+ if (types.size === 1 && types.has('string')) {
274
+ return [...new Set(arr)];
275
+ }
276
+
277
+ // All booleans — deduplicate
278
+ if (types.size === 1 && types.has('boolean')) {
279
+ return [...new Set(arr)];
280
+ }
281
+
282
+ // Mixed primitives (e.g. [true, false, 0.75, 1.0]) — deduplicate
283
+ if ([...types].every(t => t === 'string' || t === 'number' || t === 'boolean')) {
284
+ return [...new Set(arr)];
285
+ }
286
+
287
+ // Contains objects/arrays (nested items like makeProducts)
288
+ if (arr.some(v => typeof v === 'object' && v !== null && !Array.isArray(v))) {
289
+ // Extract keys from first object to show the shape
290
+ const firstObj = arr.find(v => typeof v === 'object' && v !== null);
291
+ if (firstObj) {
292
+ const keys = Object.keys(firstObj);
293
+ return { "$type": "object[]", "$keys": keys };
294
+ }
295
+ }
296
+ return null;
297
+ }
298
+
299
+ /**
300
+ * Simplify a function-based value by sampling it.
301
+ * Tries calling first; string analysis only for bound methods.
302
+ */
303
+ function simplifyFunction(fn) {
304
+ try {
305
+ // Try calling the function first
306
+ const result = fn();
307
+
308
+ // Double-wrapped function (e.g. makeProducts returns fn → fn → array)
309
+ if (typeof result === 'function') {
310
+ try {
311
+ const inner = result();
312
+ if (Array.isArray(inner)) {
313
+ if (inner.some(v => typeof v === 'object' && v !== null)) return null;
314
+ return simplifyArray(inner);
315
+ }
316
+ } catch { /* inner call failed */ }
317
+ return null;
318
+ }
319
+
320
+ // Function returned an array (pickAWinner, weighChoices)
321
+ if (Array.isArray(result)) {
322
+ return simplifyArray(result);
323
+ }
324
+
325
+ // Function returned a primitive (chance.animal, chance.profession, etc.)
326
+ if (typeof result === 'string') {
327
+ // Sample a few times to see if it's low-cardinality
328
+ const samples = new Set([result]);
329
+ for (let i = 0; i < 30; i++) {
330
+ samples.add(fn());
331
+ }
332
+ // If < 15 unique values in 30 samples, it's low cardinality — include
333
+ if (samples.size < 15) {
334
+ return [...samples].sort();
335
+ }
336
+ return null; // high cardinality
337
+ }
338
+
339
+ return null;
340
+ } catch {
341
+ return null; // function can't be called standalone — skip
342
+ }
343
+ }
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Converts a frontend JSON dungeon file to a proper JavaScript dungeon file
5
+ * Usage: node scripts/json-to-dungeon.js <input.json> [output-name]
6
+ */
7
+
8
+ import { readFileSync, writeFileSync } from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ // Get command line arguments
15
+ const args = process.argv.slice(2);
16
+
17
+ if (args.length === 0) {
18
+ console.error('❌ Error: Please provide an input JSON file');
19
+ console.log('Usage: node scripts/json-to-dungeon.js <input.json> [output-name]');
20
+ console.log('Example: node scripts/json-to-dungeon.js /path/to/dungeon.json my-dungeon');
21
+ process.exit(1);
22
+ }
23
+
24
+ const inputPath = path.resolve(args[0]);
25
+ const outputName = args[1] || path.basename(inputPath, '.json');
26
+
27
+ try {
28
+ // Read the JSON file
29
+ console.log(`📖 Reading ${inputPath}...`);
30
+ const jsonContent = readFileSync(inputPath, 'utf-8');
31
+ const dungeonData = JSON.parse(jsonContent);
32
+
33
+ // Extract schema and hooks
34
+ const schema = dungeonData.schema || dungeonData;
35
+ const hooksString = dungeonData.hooks || null;
36
+
37
+ // Convert to JavaScript
38
+ console.log('🔄 Converting to JavaScript module...');
39
+ const jsContent = convertToJavaScript(schema, hooksString, outputName);
40
+
41
+ // Write to same directory as input file
42
+ const outputPath = path.join(path.dirname(inputPath), `${outputName}.js`);
43
+ writeFileSync(outputPath, jsContent, 'utf-8');
44
+
45
+ console.log(`✅ Successfully created ${outputPath}`);
46
+ console.log(`\n💡 You can now import and use this dungeon:`);
47
+ console.log(` import config from '${path.relative(process.cwd(), outputPath)}';`);
48
+
49
+ } catch (error) {
50
+ console.error('❌ Error:', error.message);
51
+ process.exit(1);
52
+ }
53
+
54
+ /**
55
+ * Convert JSON schema to JavaScript module string
56
+ */
57
+ function convertToJavaScript(schema, hooksString, name) {
58
+ const lines = [];
59
+
60
+ // File header
61
+ lines.push(`/**`);
62
+ lines.push(` * Generated dungeon: ${name}`);
63
+ lines.push(` * Created: ${new Date().toISOString()}`);
64
+ lines.push(` * Source: Converted from frontend JSON`);
65
+ lines.push(` */`);
66
+ lines.push('');
67
+
68
+ // Imports
69
+ lines.push(`import Chance from 'chance';`);
70
+ lines.push(`let chance = new Chance();`);
71
+ lines.push(`import dayjs from "dayjs";`);
72
+ lines.push(`import utc from "dayjs/plugin/utc.js";`);
73
+ lines.push(`dayjs.extend(utc);`);
74
+ lines.push(`import { uid, comma } from 'ak-tools';`);
75
+ lines.push(`import {
76
+ pickAWinner, weighNumRange, date, integer, weighChoices,
77
+ choose, range, exhaust, maybe, takeSome, randomElement, randomInt,
78
+ weighArray
79
+ } from "../lib/utils/utils.js";`);
80
+ lines.push(`import { createTextGenerator, generateBatch } from "../lib/generators/text.js";`);
81
+ lines.push('');
82
+
83
+ // Config object
84
+ lines.push(`/** @type {import('../types').Dungeon} */`);
85
+ lines.push(`const config = ${convertValueToJS(schema, 1)};`);
86
+ lines.push('');
87
+
88
+ // Hook function (if exists)
89
+ if (hooksString) {
90
+ lines.push(`// Apply the hook function`);
91
+ lines.push(`config.hook = ${hooksString};`);
92
+ lines.push('');
93
+ }
94
+
95
+ // Export
96
+ lines.push(`export default config;`);
97
+ lines.push('');
98
+
99
+ return lines.join('\n');
100
+ }
101
+
102
+ /**
103
+ * Convert a JavaScript value to its source code representation
104
+ */
105
+ function convertValueToJS(value, indentLevel = 0) {
106
+ const indent = '\t'.repeat(indentLevel);
107
+ const nextIndent = '\t'.repeat(indentLevel + 1);
108
+
109
+ // Null
110
+ if (value === null || value === undefined) {
111
+ return 'null';
112
+ }
113
+
114
+ // Boolean
115
+ if (typeof value === 'boolean') {
116
+ return String(value);
117
+ }
118
+
119
+ // Number
120
+ if (typeof value === 'number') {
121
+ return String(value);
122
+ }
123
+
124
+ // String
125
+ if (typeof value === 'string') {
126
+ // Handle multi-line strings
127
+ if (value.includes('\n')) {
128
+ return '`' + value.replace(/`/g, '\\`') + '`';
129
+ }
130
+ // Use double quotes for regular strings
131
+ return '"' + value.replace(/"/g, '\\"') + '"';
132
+ }
133
+
134
+ // Function (stored as object with functionName and args)
135
+ if (value && typeof value === 'object' && value.functionName) {
136
+ const funcName = value.functionName;
137
+ const args = value.args || [];
138
+ const argsStr = args.map(arg => convertValueToJS(arg, 0)).join(', ');
139
+
140
+ // Handle arrow functions
141
+ if (funcName === 'arrow') {
142
+ return value.body || '() => {}';
143
+ }
144
+
145
+ // Handle chance bindings
146
+ if (funcName.startsWith('chance.')) {
147
+ return `${funcName}.bind(chance)`;
148
+ }
149
+
150
+ // Regular function calls
151
+ return `${funcName}(${argsStr})`;
152
+ }
153
+
154
+ // Array
155
+ if (Array.isArray(value)) {
156
+ if (value.length === 0) {
157
+ return '[]';
158
+ }
159
+
160
+ // Check if it's a simple array (all primitives)
161
+ const allPrimitives = value.every(v =>
162
+ v === null ||
163
+ typeof v === 'boolean' ||
164
+ typeof v === 'number' ||
165
+ typeof v === 'string'
166
+ );
167
+
168
+ if (allPrimitives && value.length <= 5) {
169
+ // Inline simple arrays
170
+ const items = value.map(v => convertValueToJS(v, 0));
171
+ return `[${items.join(', ')}]`;
172
+ }
173
+
174
+ // Multi-line arrays
175
+ const items = value.map(v => `${nextIndent}${convertValueToJS(v, indentLevel + 1)}`);
176
+ return `[\n${items.join(',\n')}\n${indent}]`;
177
+ }
178
+
179
+ // Object
180
+ if (typeof value === 'object') {
181
+ const keys = Object.keys(value);
182
+
183
+ if (keys.length === 0) {
184
+ return '{}';
185
+ }
186
+
187
+ const entries = keys.map(key => {
188
+ const val = convertValueToJS(value[key], indentLevel + 1);
189
+ // Use quotes for keys with special characters or spaces
190
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(key) || /^\d/.test(key);
191
+ const keyStr = needsQuotes ? `"${key}"` : key;
192
+ return `${nextIndent}${keyStr}: ${val}`;
193
+ });
194
+
195
+ return `{\n${entries.join(',\n')}\n${indent}}`;
196
+ }
197
+
198
+ // Fallback
199
+ return 'null';
200
+ }