@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.
- package/README.md +7 -7
- package/dungeons/technical/ad-spend-schema.json +128 -0
- package/dungeons/technical/ad-spend.js +116 -0
- package/dungeons/technical/anonymous-users-schema.json +92 -0
- package/dungeons/technical/anonymous-users.js +102 -0
- package/dungeons/technical/array-of-object-lookup-schema.json +191 -0
- package/dungeons/{array-of-object-lookup.js → technical/array-of-object-lookup.js} +12 -5
- package/dungeons/technical/experiments-schema.json +203 -0
- package/dungeons/technical/experiments.js +168 -0
- package/dungeons/technical/foobar-schema.json +362 -0
- package/dungeons/{foobar.js → technical/foobar.js} +3 -3
- package/dungeons/technical/group-analytics-schema.json +241 -0
- package/dungeons/technical/group-analytics.js +151 -0
- package/dungeons/technical/mirror-strategies-schema.json +84 -0
- package/dungeons/technical/mirror-strategies.js +126 -0
- package/dungeons/technical/nested-objects-schema.json +145 -0
- package/dungeons/technical/nested-objects.js +207 -0
- package/dungeons/technical/retention-cadence-schema.json +37 -0
- package/dungeons/{retention-cadence.js → technical/retention-cadence.js} +2 -3
- package/dungeons/technical/sanity-schema.json +185 -0
- package/dungeons/{sanity.js → technical/sanity.js} +4 -2
- package/dungeons/technical/scale-test-schema.json +70 -0
- package/dungeons/technical/scale-test.js +90 -0
- package/dungeons/technical/scd-schema.json +467 -0
- package/dungeons/{scd.js → technical/scd.js} +20 -10
- package/dungeons/technical/simple-schema.json +362 -0
- package/dungeons/{simple.js → technical/simple.js} +7 -7
- package/dungeons/technical/simplest-schema.json +724 -0
- package/dungeons/{simplest.js → technical/simplest.js} +7 -7
- package/dungeons/technical/text-generation-schema.json +1062 -0
- package/dungeons/{text-generation.js → technical/text-generation.js} +24 -11
- package/dungeons/vertical/community-schema.json +579 -0
- package/dungeons/vertical/community.js +794 -0
- package/dungeons/vertical/devtools-schema.json +601 -0
- package/dungeons/vertical/devtools.js +843 -0
- package/dungeons/vertical/ecommerce-schema.json +604 -0
- package/dungeons/{ecommerce.js → vertical/ecommerce.js} +43 -5
- package/dungeons/vertical/education-schema.json +5686 -0
- package/dungeons/{education.js → vertical/education.js} +70 -39
- package/dungeons/vertical/fintech-schema.json +630 -0
- package/dungeons/{fintech.js → vertical/fintech.js} +107 -56
- package/dungeons/vertical/fitness-schema.json +530 -0
- package/dungeons/vertical/fitness.js +808 -0
- package/dungeons/vertical/food-delivery-schema.json +36728 -0
- package/dungeons/{food.js → vertical/food-delivery.js} +67 -26
- package/dungeons/vertical/gaming-schema.json +438 -0
- package/dungeons/{gaming.js → vertical/gaming.js} +29 -16
- package/dungeons/vertical/healthcare-schema.json +549 -0
- package/dungeons/vertical/healthcare.js +775 -0
- package/dungeons/vertical/insurance-application-schema.json +485 -0
- package/dungeons/{insurance-application.js → vertical/insurance-application.js} +41 -31
- package/dungeons/vertical/logistics-schema.json +574 -0
- package/dungeons/vertical/logistics.js +799 -0
- package/dungeons/vertical/marketplace-schema.json +533 -0
- package/dungeons/vertical/marketplace.js +850 -0
- package/dungeons/vertical/media-schema.json +4749 -0
- package/dungeons/{media.js → vertical/media.js} +103 -19
- package/dungeons/vertical/rpg-schema.json +2491 -0
- package/dungeons/{rpg.js → vertical/rpg.js} +87 -60
- package/dungeons/vertical/sass-schema.json +3128 -0
- package/dungeons/{sass.js → vertical/sass.js} +65 -26
- package/dungeons/vertical/social-schema.json +620 -0
- package/dungeons/{social.js → vertical/social.js} +76 -17
- package/dungeons/vertical/travel-schema.json +580 -0
- package/dungeons/vertical/travel.js +742 -0
- package/index.js +20 -16
- package/lib/core/config-validator.js +286 -1
- package/lib/core/context.js +16 -5
- package/lib/core/storage.js +2 -0
- package/lib/generators/adspend.js +3 -3
- package/lib/generators/events.js +141 -14
- package/lib/generators/funnels.js +67 -7
- package/lib/generators/text.js +2 -2
- package/lib/orchestrators/user-loop.js +444 -22
- package/lib/templates/soup-presets.js +2 -2
- package/lib/utils/utils.js +4 -5
- package/package.json +4 -3
- package/scripts/dungeon-to-json.mjs +163 -0
- package/scripts/extract-dungeon-schema.mjs +343 -0
- package/scripts/json-to-dungeon.mjs +200 -0
- package/scripts/run-dungeon.mjs +76 -0
- package/scripts/run-many.mjs +118 -0
- package/scripts/verify-runner.mjs +42 -0
- package/types.d.ts +316 -1
- package/dungeons/array-of-object-lookup-schema.json +0 -327
- package/dungeons/ecommerce-schema.json +0 -462
- package/dungeons/education-schema.json +0 -2409
- package/dungeons/fintech-schema.json +0 -14034
- package/dungeons/foobar-schema.json +0 -403
- package/dungeons/food-delivery-schema.json +0 -192
- package/dungeons/food-delivery.js +0 -602
- package/dungeons/food-schema.json +0 -1152
- package/dungeons/gaming-schema.json +0 -1270
- package/dungeons/insurance-application-schema.json +0 -204
- package/dungeons/media-schema.json +0 -906
- package/dungeons/retention-cadence-schema.json +0 -78
- package/dungeons/rpg-schema.json +0 -4526
- package/dungeons/sanity-schema.json +0 -255
- package/dungeons/sass-schema.json +0 -1291
- package/dungeons/scd-schema.json +0 -919
- package/dungeons/simple-schema.json +0 -608
- package/dungeons/simplest-schema.json +0 -1418
- package/dungeons/social-schema.json +0 -1118
- 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
|
+
}
|