reflexive 0.1.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/CLAUDE.md +77 -0
- package/FAILURES.md +245 -0
- package/README.md +264 -0
- package/Screenshot 2026-01-22 at 6.31.27/342/200/257AM.png +0 -0
- package/dashboard.html +620 -0
- package/demo-ai-features.js +571 -0
- package/demo-app.js +210 -0
- package/demo-inject.js +212 -0
- package/demo-instrumented.js +272 -0
- package/docs/BREAKPOINT-AUDIT.md +293 -0
- package/docs/GENESIS.md +110 -0
- package/docs/HN-LAUNCH-PLAN-V2.md +631 -0
- package/docs/HN-LAUNCH-PLAN.md +492 -0
- package/docs/TODO.md +69 -0
- package/docs/V8-INSPECTOR-RESEARCH.md +1231 -0
- package/logo-carbon.png +0 -0
- package/logo0.jpg +0 -0
- package/logo1.jpg +0 -0
- package/logo2.jpg +0 -0
- package/new-ui-template.html +435 -0
- package/one-shot.js +1109 -0
- package/package.json +47 -0
- package/play-story.sh +10 -0
- package/src/demo-inject.js +3 -0
- package/src/inject.cjs +474 -0
- package/src/reflexive.js +6214 -0
- package/story-game-reflexive.js +1246 -0
- package/story-game-web.js +1030 -0
- package/story-mystery-1769171430377.js +162 -0
package/one-shot.js
ADDED
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
// Path-Based Story Game Builder & Simulator
|
|
2
|
+
// Created by Reflexive
|
|
3
|
+
// Enhanced with AI-Driven Dynamic Storytelling
|
|
4
|
+
|
|
5
|
+
// AI Story Generator using Reflexive
|
|
6
|
+
class AIStoryGenerator {
|
|
7
|
+
static async generateDynamicConsequence(choice, currentState, arcWeights) {
|
|
8
|
+
// Use AI to generate contextual consequences
|
|
9
|
+
const prompt = `You are a fantasy story narrator. Given the player's choice "${choice.text}" in a story where they have:
|
|
10
|
+
- Current state: ${JSON.stringify(currentState)}
|
|
11
|
+
- Arc weights: ${JSON.stringify(Object.fromEntries(arcWeights))}
|
|
12
|
+
|
|
13
|
+
Generate a brief (1-2 sentence) consequence or flavor text that describes what happens next. Make it dramatic and fitting to the tone.`;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (typeof process !== 'undefined' && process.reflexive && process.reflexive.prompt) {
|
|
17
|
+
const result = await process.reflexive.prompt(prompt);
|
|
18
|
+
return result || "Your choice sets events in motion.";
|
|
19
|
+
}
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// Fallback if AI not available
|
|
22
|
+
}
|
|
23
|
+
return "Your choice sets events in motion.";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static async generateDynamicChoice(currentNode, arcWeights, stateContext) {
|
|
27
|
+
const dominantArc = this.getDominantArc(arcWeights);
|
|
28
|
+
|
|
29
|
+
const prompt = `You are creating a choice for an interactive fantasy story. The player is at: "${currentNode.text}"
|
|
30
|
+
Current dominant arc: ${dominantArc}
|
|
31
|
+
Context: ${JSON.stringify(stateContext)}
|
|
32
|
+
|
|
33
|
+
Generate ONE interesting choice option (just the text, 4-8 words) that fits the current situation and dominant arc. Make it dramatic and engaging.`;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
if (typeof process !== 'undefined' && process.reflexive && process.reflexive.prompt) {
|
|
37
|
+
const result = await process.reflexive.prompt(prompt);
|
|
38
|
+
return result || "Continue your journey";
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// Fallback
|
|
42
|
+
}
|
|
43
|
+
return "Continue your journey";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static getDominantArc(arcWeights) {
|
|
47
|
+
let maxWeight = 0;
|
|
48
|
+
let dominant = 'neutral';
|
|
49
|
+
|
|
50
|
+
for (const [arc, weight] of arcWeights) {
|
|
51
|
+
if (weight > maxWeight) {
|
|
52
|
+
maxWeight = weight;
|
|
53
|
+
dominant = arc;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return dominant;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static async generateRandomEncounter(currentState, arcWeights) {
|
|
60
|
+
const dominantArc = this.getDominantArc(arcWeights);
|
|
61
|
+
|
|
62
|
+
const prompt = `Generate a brief (2-3 sentences) random encounter for a fantasy story.
|
|
63
|
+
The player has alignment: ${currentState.alignment || 'neutral'}
|
|
64
|
+
Dominant story arc: ${dominantArc}
|
|
65
|
+
|
|
66
|
+
Create an unexpected event that fits the tone. Keep it concise and dramatic.`;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
if (typeof process !== 'undefined' && process.reflexive && process.reflexive.prompt) {
|
|
70
|
+
const result = await process.reflexive.prompt(prompt);
|
|
71
|
+
return result || "You encounter something unexpected on the road.";
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Fallback
|
|
75
|
+
}
|
|
76
|
+
return "You encounter something unexpected on the road.";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class StoryGameBuilder {
|
|
81
|
+
constructor() {
|
|
82
|
+
this.nodes = new Map();
|
|
83
|
+
this.arcs = new Map();
|
|
84
|
+
this.globalState = {};
|
|
85
|
+
this.enableAI = true; // Toggle for AI-enhanced storytelling
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Define a story node (scene/moment in the story)
|
|
89
|
+
addNode(id, data) {
|
|
90
|
+
this.nodes.set(id, {
|
|
91
|
+
id,
|
|
92
|
+
text: data.text || '',
|
|
93
|
+
choices: data.choices || [],
|
|
94
|
+
onEnter: data.onEnter || null,
|
|
95
|
+
onExit: data.onExit || null,
|
|
96
|
+
arcTags: data.arcTags || [], // Tags for tracking which arc we're in
|
|
97
|
+
...data
|
|
98
|
+
});
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Define a story arc (collection of nodes with shared theme/plot)
|
|
103
|
+
addArc(id, data) {
|
|
104
|
+
this.arcs.set(id, {
|
|
105
|
+
id,
|
|
106
|
+
name: data.name || id,
|
|
107
|
+
weight: data.weight || 1.0, // Base weight for this arc
|
|
108
|
+
prerequisites: data.prerequisites || {}, // State requirements to access
|
|
109
|
+
...data
|
|
110
|
+
});
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add a choice to a node with math-based weighting
|
|
115
|
+
addChoice(nodeId, choice) {
|
|
116
|
+
const node = this.nodes.get(nodeId);
|
|
117
|
+
if (!node) throw new Error(`Node ${nodeId} not found`);
|
|
118
|
+
|
|
119
|
+
node.choices.push({
|
|
120
|
+
text: choice.text,
|
|
121
|
+
target: choice.target,
|
|
122
|
+
weight: choice.weight || 1.0, // Base weight
|
|
123
|
+
weightFormula: choice.weightFormula || null, // Dynamic weight calculation
|
|
124
|
+
condition: choice.condition || null, // Function to check if available
|
|
125
|
+
effects: choice.effects || {}, // State changes when chosen
|
|
126
|
+
arcModifiers: choice.arcModifiers || {} // Affect arc weights
|
|
127
|
+
});
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getGame() {
|
|
132
|
+
return {
|
|
133
|
+
nodes: Array.from(this.nodes.values()),
|
|
134
|
+
arcs: Array.from(this.arcs.values()),
|
|
135
|
+
globalState: this.globalState
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class StorySimulator {
|
|
141
|
+
constructor(game, enableAI = false) {
|
|
142
|
+
this.nodes = new Map(game.nodes.map(n => [n.id, n]));
|
|
143
|
+
this.arcs = new Map(game.arcs.map(a => [a.id, a]));
|
|
144
|
+
this.state = { ...game.globalState };
|
|
145
|
+
this.currentNode = null;
|
|
146
|
+
this.history = [];
|
|
147
|
+
this.arcWeights = new Map();
|
|
148
|
+
this.enableAI = enableAI;
|
|
149
|
+
this.aiGeneratedContent = []; // Track AI-generated story elements
|
|
150
|
+
|
|
151
|
+
// Initialize arc weights
|
|
152
|
+
this.arcs.forEach((arc, id) => {
|
|
153
|
+
this.arcWeights.set(id, arc.weight);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Generate AI-enhanced flavor text for the current situation
|
|
158
|
+
async getEnhancedNodeText(nodeId) {
|
|
159
|
+
const node = this.nodes.get(nodeId);
|
|
160
|
+
if (!this.enableAI || !node) return node.text;
|
|
161
|
+
|
|
162
|
+
// Add AI-generated context based on player's journey
|
|
163
|
+
try {
|
|
164
|
+
const journeyContext = this.history.slice(-3).map(h => h.nodeId).join(' ā ');
|
|
165
|
+
const prompt = `Add a brief atmospheric detail (1 sentence) to this scene: "${node.text}"
|
|
166
|
+
Recent journey: ${journeyContext}
|
|
167
|
+
Player state: ${JSON.stringify(this.state)}
|
|
168
|
+
|
|
169
|
+
Just return the additional atmospheric sentence, nothing else.`;
|
|
170
|
+
|
|
171
|
+
if (typeof process !== 'undefined' && process.reflexive && process.reflexive.prompt) {
|
|
172
|
+
const enhancement = await process.reflexive.prompt(prompt);
|
|
173
|
+
if (enhancement && enhancement.length > 0 && enhancement.length < 200) {
|
|
174
|
+
this.aiGeneratedContent.push({ type: 'enhancement', text: enhancement, node: nodeId });
|
|
175
|
+
return `${node.text} ${enhancement}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// Fall back to original text
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return node.text;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Start the game at a specific node
|
|
186
|
+
start(startNodeId) {
|
|
187
|
+
this.currentNode = startNodeId;
|
|
188
|
+
this.enterNode(startNodeId);
|
|
189
|
+
return this.getCurrentState();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
enterNode(nodeId) {
|
|
193
|
+
const node = this.nodes.get(nodeId);
|
|
194
|
+
if (!node) throw new Error(`Node ${nodeId} not found`);
|
|
195
|
+
|
|
196
|
+
this.history.push({
|
|
197
|
+
nodeId,
|
|
198
|
+
timestamp: Date.now(),
|
|
199
|
+
state: { ...this.state }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Execute onEnter callback
|
|
203
|
+
if (node.onEnter) {
|
|
204
|
+
node.onEnter(this.state, this);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Update arc weights based on node's arc tags
|
|
208
|
+
node.arcTags.forEach(arcId => {
|
|
209
|
+
const current = this.arcWeights.get(arcId) || 1.0;
|
|
210
|
+
this.arcWeights.set(arcId, current * 1.2); // Boost active arcs
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get available choices with weighted probabilities
|
|
215
|
+
getAvailableChoices() {
|
|
216
|
+
const node = this.nodes.get(this.currentNode);
|
|
217
|
+
if (!node) return [];
|
|
218
|
+
|
|
219
|
+
return node.choices
|
|
220
|
+
.filter(choice => {
|
|
221
|
+
// Check if choice is available based on condition
|
|
222
|
+
if (choice.condition) {
|
|
223
|
+
return choice.condition(this.state, this);
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
})
|
|
227
|
+
.map(choice => {
|
|
228
|
+
// Calculate dynamic weight
|
|
229
|
+
let weight = choice.weight;
|
|
230
|
+
|
|
231
|
+
if (choice.weightFormula) {
|
|
232
|
+
weight = choice.weightFormula(this.state, this.arcWeights, this);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Apply arc modifiers
|
|
236
|
+
Object.keys(choice.arcModifiers).forEach(arcId => {
|
|
237
|
+
const arcWeight = this.arcWeights.get(arcId) || 1.0;
|
|
238
|
+
weight *= arcWeight * choice.arcModifiers[arcId];
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
...choice,
|
|
243
|
+
calculatedWeight: weight
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Make a choice (can be player-driven or AI-simulated)
|
|
249
|
+
makeChoice(choiceIndex) {
|
|
250
|
+
const choices = this.getAvailableChoices();
|
|
251
|
+
if (choiceIndex >= choices.length) {
|
|
252
|
+
throw new Error('Invalid choice index');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const choice = choices[choiceIndex];
|
|
256
|
+
|
|
257
|
+
// Execute onExit callback
|
|
258
|
+
const currentNode = this.nodes.get(this.currentNode);
|
|
259
|
+
if (currentNode.onExit) {
|
|
260
|
+
currentNode.onExit(this.state, this);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Apply choice effects to state
|
|
264
|
+
Object.assign(this.state, choice.effects);
|
|
265
|
+
|
|
266
|
+
// Apply arc weight modifiers
|
|
267
|
+
Object.keys(choice.arcModifiers).forEach(arcId => {
|
|
268
|
+
const current = this.arcWeights.get(arcId) || 1.0;
|
|
269
|
+
const modifier = choice.arcModifiers[arcId];
|
|
270
|
+
this.arcWeights.set(arcId, current * modifier);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Move to next node
|
|
274
|
+
this.currentNode = choice.target;
|
|
275
|
+
this.enterNode(choice.target);
|
|
276
|
+
|
|
277
|
+
return this.getCurrentState();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// AI auto-play: choose based on weighted probability
|
|
281
|
+
autoChoose() {
|
|
282
|
+
const choices = this.getAvailableChoices();
|
|
283
|
+
if (choices.length === 0) return null;
|
|
284
|
+
|
|
285
|
+
// Calculate total weight
|
|
286
|
+
const totalWeight = choices.reduce((sum, c) => sum + c.calculatedWeight, 0);
|
|
287
|
+
|
|
288
|
+
// Random weighted selection
|
|
289
|
+
let random = Math.random() * totalWeight;
|
|
290
|
+
|
|
291
|
+
for (let i = 0; i < choices.length; i++) {
|
|
292
|
+
random -= choices[i].calculatedWeight;
|
|
293
|
+
if (random <= 0) {
|
|
294
|
+
return this.makeChoice(i);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Fallback to first choice
|
|
299
|
+
return this.makeChoice(0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getCurrentState() {
|
|
303
|
+
const node = this.nodes.get(this.currentNode);
|
|
304
|
+
const choices = this.getAvailableChoices();
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
nodeId: this.currentNode,
|
|
308
|
+
text: node.text,
|
|
309
|
+
choices: choices.map((c, i) => ({
|
|
310
|
+
index: i,
|
|
311
|
+
text: c.text,
|
|
312
|
+
weight: c.calculatedWeight
|
|
313
|
+
})),
|
|
314
|
+
state: { ...this.state },
|
|
315
|
+
arcWeights: Object.fromEntries(this.arcWeights),
|
|
316
|
+
history: this.history.length
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Save game state to JSON
|
|
321
|
+
saveGame(filename) {
|
|
322
|
+
const saveData = {
|
|
323
|
+
currentNode: this.currentNode,
|
|
324
|
+
state: this.state,
|
|
325
|
+
arcWeights: Object.fromEntries(this.arcWeights),
|
|
326
|
+
history: this.history,
|
|
327
|
+
timestamp: new Date().toISOString()
|
|
328
|
+
};
|
|
329
|
+
return saveData;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Load game state from JSON
|
|
333
|
+
loadGame(saveData) {
|
|
334
|
+
this.currentNode = saveData.currentNode;
|
|
335
|
+
this.state = { ...saveData.state };
|
|
336
|
+
this.arcWeights = new Map(Object.entries(saveData.arcWeights));
|
|
337
|
+
this.history = [...saveData.history];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Export story playthrough as narrative text
|
|
341
|
+
exportNarrative() {
|
|
342
|
+
let narrative = '='.repeat(60) + '\n';
|
|
343
|
+
narrative += 'STORY PLAYTHROUGH\n';
|
|
344
|
+
narrative += '='.repeat(60) + '\n\n';
|
|
345
|
+
|
|
346
|
+
this.history.forEach((entry, index) => {
|
|
347
|
+
const node = this.nodes.get(entry.nodeId);
|
|
348
|
+
narrative += `Chapter ${index + 1}: ${entry.nodeId.toUpperCase()}\n`;
|
|
349
|
+
narrative += '-'.repeat(60) + '\n';
|
|
350
|
+
narrative += `${node.text}\n\n`;
|
|
351
|
+
|
|
352
|
+
if (Object.keys(entry.state).length > 0) {
|
|
353
|
+
narrative += `State: ${JSON.stringify(entry.state)}\n\n`;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const finalState = this.getCurrentState();
|
|
358
|
+
narrative += '='.repeat(60) + '\n';
|
|
359
|
+
narrative += 'THE END\n';
|
|
360
|
+
narrative += '='.repeat(60) + '\n';
|
|
361
|
+
narrative += `${finalState.text}\n\n`;
|
|
362
|
+
narrative += `Final State: ${JSON.stringify(finalState.state)}\n`;
|
|
363
|
+
narrative += `Arc Weights: ${JSON.stringify(finalState.arcWeights)}\n`;
|
|
364
|
+
|
|
365
|
+
return narrative;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Run simulation for N steps
|
|
369
|
+
simulate(steps = 10) {
|
|
370
|
+
const results = [];
|
|
371
|
+
for (let i = 0; i < steps && this.getAvailableChoices().length > 0; i++) {
|
|
372
|
+
results.push(this.autoChoose());
|
|
373
|
+
}
|
|
374
|
+
return results;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ===== DEMO STORY: Fantasy Adventure with Multiple Arcs =====
|
|
379
|
+
|
|
380
|
+
function createDemoStory() {
|
|
381
|
+
const builder = new StoryGameBuilder();
|
|
382
|
+
|
|
383
|
+
// Define story arcs
|
|
384
|
+
builder
|
|
385
|
+
.addArc('hero', { name: 'Heroic Path', weight: 1.0 })
|
|
386
|
+
.addArc('dark', { name: 'Dark Path', weight: 1.0 })
|
|
387
|
+
.addArc('diplomatic', { name: 'Diplomatic Path', weight: 1.0 })
|
|
388
|
+
.addArc('chaos', { name: 'Chaos Path', weight: 0.5 });
|
|
389
|
+
|
|
390
|
+
// Build the story nodes
|
|
391
|
+
builder
|
|
392
|
+
.addNode('start', {
|
|
393
|
+
text: 'You stand at the gates of the ancient city. A guard blocks your path.',
|
|
394
|
+
arcTags: []
|
|
395
|
+
})
|
|
396
|
+
.addNode('heroic_entrance', {
|
|
397
|
+
text: 'You offer to help the guard with a monster problem. He gratefully lets you pass.',
|
|
398
|
+
arcTags: ['hero'],
|
|
399
|
+
onEnter: (state) => { state.reputation = (state.reputation || 0) + 10; }
|
|
400
|
+
})
|
|
401
|
+
.addNode('sneak_in', {
|
|
402
|
+
text: 'You slip past the guard in the shadows. No one notices you.',
|
|
403
|
+
arcTags: ['dark'],
|
|
404
|
+
onEnter: (state) => { state.stealth = (state.stealth || 0) + 5; }
|
|
405
|
+
})
|
|
406
|
+
.addNode('bribe_guard', {
|
|
407
|
+
text: 'You slip the guard some gold. He smiles and steps aside.',
|
|
408
|
+
arcTags: ['diplomatic'],
|
|
409
|
+
onEnter: (state) => { state.gold = (state.gold || 100) - 20; state.connections = (state.connections || 0) + 1; }
|
|
410
|
+
})
|
|
411
|
+
.addNode('tavern', {
|
|
412
|
+
text: 'Inside the city, you enter a busy tavern. A hooded figure watches you from the corner.',
|
|
413
|
+
arcTags: []
|
|
414
|
+
})
|
|
415
|
+
.addNode('hero_quest', {
|
|
416
|
+
text: 'The figure reveals a quest to save the kingdom from an ancient evil. You accept with honor.',
|
|
417
|
+
arcTags: ['hero'],
|
|
418
|
+
onEnter: (state) => { state.questActive = true; state.alignment = 'good'; }
|
|
419
|
+
})
|
|
420
|
+
.addNode('dark_alliance', {
|
|
421
|
+
text: 'The figure offers you power in exchange for... questionable deeds. You accept.',
|
|
422
|
+
arcTags: ['dark'],
|
|
423
|
+
onEnter: (state) => { state.power = (state.power || 0) + 15; state.alignment = 'dark'; }
|
|
424
|
+
})
|
|
425
|
+
.addNode('chaos_unleashed', {
|
|
426
|
+
text: 'You decide to burn the tavern down just for fun. Chaos erupts!',
|
|
427
|
+
arcTags: ['chaos'],
|
|
428
|
+
onEnter: (state) => { state.chaos = (state.chaos || 0) + 20; state.wanted = true; }
|
|
429
|
+
})
|
|
430
|
+
.addNode('final_battle', {
|
|
431
|
+
text: 'Your choices have led you here. The final confrontation awaits.',
|
|
432
|
+
arcTags: []
|
|
433
|
+
})
|
|
434
|
+
.addNode('hero_ending', {
|
|
435
|
+
text: 'You defeat the evil and are celebrated as a hero! The kingdom prospers.',
|
|
436
|
+
arcTags: ['hero']
|
|
437
|
+
})
|
|
438
|
+
.addNode('dark_ending', {
|
|
439
|
+
text: 'You claim dark power and rule with an iron fist. Fear spreads across the land.',
|
|
440
|
+
arcTags: ['dark']
|
|
441
|
+
})
|
|
442
|
+
.addNode('diplomat_ending', {
|
|
443
|
+
text: 'Through careful negotiation, you broker peace. Everyone wins.',
|
|
444
|
+
arcTags: ['diplomatic']
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Add choices with weights and arc modifiers
|
|
448
|
+
builder
|
|
449
|
+
.addChoice('start', {
|
|
450
|
+
text: 'Offer to help the guard',
|
|
451
|
+
target: 'heroic_entrance',
|
|
452
|
+
weight: 1.0,
|
|
453
|
+
arcModifiers: { hero: 2.0, dark: 0.5 }
|
|
454
|
+
})
|
|
455
|
+
.addChoice('start', {
|
|
456
|
+
text: 'Sneak past in the shadows',
|
|
457
|
+
target: 'sneak_in',
|
|
458
|
+
weight: 1.0,
|
|
459
|
+
arcModifiers: { dark: 2.0, hero: 0.5 }
|
|
460
|
+
})
|
|
461
|
+
.addChoice('start', {
|
|
462
|
+
text: 'Bribe the guard',
|
|
463
|
+
target: 'bribe_guard',
|
|
464
|
+
weight: 1.0,
|
|
465
|
+
arcModifiers: { diplomatic: 2.0 }
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
builder
|
|
469
|
+
.addChoice('heroic_entrance', {
|
|
470
|
+
text: 'Head to the tavern',
|
|
471
|
+
target: 'tavern',
|
|
472
|
+
weight: 1.0
|
|
473
|
+
})
|
|
474
|
+
.addChoice('sneak_in', {
|
|
475
|
+
text: 'Head to the tavern',
|
|
476
|
+
target: 'tavern',
|
|
477
|
+
weight: 1.0
|
|
478
|
+
})
|
|
479
|
+
.addChoice('bribe_guard', {
|
|
480
|
+
text: 'Head to the tavern',
|
|
481
|
+
target: 'tavern',
|
|
482
|
+
weight: 1.0
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
builder
|
|
486
|
+
.addChoice('tavern', {
|
|
487
|
+
text: 'Accept the heroic quest',
|
|
488
|
+
target: 'hero_quest',
|
|
489
|
+
weight: 1.0,
|
|
490
|
+
arcModifiers: { hero: 2.5, dark: 0.3 },
|
|
491
|
+
weightFormula: (state, arcWeights) => {
|
|
492
|
+
return 1.0 * (arcWeights.get('hero') || 1.0);
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
.addChoice('tavern', {
|
|
496
|
+
text: 'Accept the dark offer',
|
|
497
|
+
target: 'dark_alliance',
|
|
498
|
+
weight: 1.0,
|
|
499
|
+
arcModifiers: { dark: 2.5, hero: 0.3 },
|
|
500
|
+
weightFormula: (state, arcWeights) => {
|
|
501
|
+
return 1.0 * (arcWeights.get('dark') || 1.0);
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
.addChoice('tavern', {
|
|
505
|
+
text: 'Burn it all down!',
|
|
506
|
+
target: 'chaos_unleashed',
|
|
507
|
+
weight: 0.3,
|
|
508
|
+
arcModifiers: { chaos: 5.0, hero: 0.1, dark: 0.5 },
|
|
509
|
+
weightFormula: (state, arcWeights) => {
|
|
510
|
+
return 0.3 * (arcWeights.get('chaos') || 0.5);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
builder
|
|
515
|
+
.addChoice('hero_quest', {
|
|
516
|
+
text: 'Face the final battle',
|
|
517
|
+
target: 'final_battle',
|
|
518
|
+
weight: 1.0
|
|
519
|
+
})
|
|
520
|
+
.addChoice('dark_alliance', {
|
|
521
|
+
text: 'Face the final battle',
|
|
522
|
+
target: 'final_battle',
|
|
523
|
+
weight: 1.0
|
|
524
|
+
})
|
|
525
|
+
.addChoice('chaos_unleashed', {
|
|
526
|
+
text: 'Face the final battle',
|
|
527
|
+
target: 'final_battle',
|
|
528
|
+
weight: 1.0
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
builder
|
|
532
|
+
.addChoice('final_battle', {
|
|
533
|
+
text: 'Fight with honor',
|
|
534
|
+
target: 'hero_ending',
|
|
535
|
+
weight: 1.0,
|
|
536
|
+
condition: (state) => state.alignment !== 'dark',
|
|
537
|
+
weightFormula: (state, arcWeights) => {
|
|
538
|
+
return (arcWeights.get('hero') || 1.0) * 2;
|
|
539
|
+
}
|
|
540
|
+
})
|
|
541
|
+
.addChoice('final_battle', {
|
|
542
|
+
text: 'Use dark power',
|
|
543
|
+
target: 'dark_ending',
|
|
544
|
+
weight: 1.0,
|
|
545
|
+
condition: (state) => state.alignment !== 'good',
|
|
546
|
+
weightFormula: (state, arcWeights) => {
|
|
547
|
+
return (arcWeights.get('dark') || 1.0) * 2;
|
|
548
|
+
}
|
|
549
|
+
})
|
|
550
|
+
.addChoice('final_battle', {
|
|
551
|
+
text: 'Negotiate peace',
|
|
552
|
+
target: 'diplomat_ending',
|
|
553
|
+
weight: 1.0,
|
|
554
|
+
weightFormula: (state, arcWeights) => {
|
|
555
|
+
return (arcWeights.get('diplomatic') || 1.0) * 1.5;
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return builder.getGame();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ===== INTERACTIVE GAME SYSTEM =====
|
|
563
|
+
|
|
564
|
+
import readline from 'readline';
|
|
565
|
+
import fs from 'fs';
|
|
566
|
+
import path from 'path';
|
|
567
|
+
|
|
568
|
+
const rl = readline.createInterface({
|
|
569
|
+
input: process.stdin,
|
|
570
|
+
output: process.stdout
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
function question(prompt) {
|
|
574
|
+
return new Promise((resolve) => {
|
|
575
|
+
rl.question(prompt, resolve);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let currentSimulator = null;
|
|
580
|
+
const game = createDemoStory();
|
|
581
|
+
let aiEnhancedMode = false; // Toggle for AI-enhanced storytelling
|
|
582
|
+
|
|
583
|
+
async function showMainMenu() {
|
|
584
|
+
console.clear();
|
|
585
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
586
|
+
console.log('ā Path-Based Story Game Builder & Simulator ā');
|
|
587
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
588
|
+
console.log(`š Story loaded: ${game.nodes.length} nodes, ${game.arcs.length} arcs`);
|
|
589
|
+
console.log(`${aiEnhancedMode ? 'š¤ AI-Enhanced Mode: ON' : 'š Classic Mode: ON'}\n`);
|
|
590
|
+
console.log('MAIN MENU:');
|
|
591
|
+
console.log(' 1. š® Play Interactive Story (You choose)');
|
|
592
|
+
console.log(' 2. š¤ Run AI Simulation (Watch AI play)');
|
|
593
|
+
console.log(' 3. š Run Multiple Simulations (Statistics)');
|
|
594
|
+
console.log(' 4. šļø Show Story Structure');
|
|
595
|
+
console.log(' 5. š¾ Load Saved Game');
|
|
596
|
+
console.log(' 6. š View Story Graph');
|
|
597
|
+
console.log(' 7. š Achievements');
|
|
598
|
+
console.log(` 8. ${aiEnhancedMode ? 'š' : 'š¤'} Toggle AI Enhancement (${aiEnhancedMode ? 'ON' : 'OFF'})`);
|
|
599
|
+
console.log(' 9. šŖ Exit');
|
|
600
|
+
console.log(' 10. šØ Build Custom Story (Claude AI Planning)\n');
|
|
601
|
+
|
|
602
|
+
const choice = await question('Enter your choice (1-10): ');
|
|
603
|
+
return choice.trim();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function playInteractive() {
|
|
607
|
+
console.clear();
|
|
608
|
+
console.log('š® INTERACTIVE MODE\n');
|
|
609
|
+
if (aiEnhancedMode) {
|
|
610
|
+
console.log('š¤ AI-Enhanced Storytelling: Dynamic content and atmosphere enabled!\n');
|
|
611
|
+
}
|
|
612
|
+
console.log('You make the choices. Type the number of your choice and press Enter.\n');
|
|
613
|
+
console.log('='.repeat(60) + '\n');
|
|
614
|
+
|
|
615
|
+
currentSimulator = new StorySimulator(game, aiEnhancedMode);
|
|
616
|
+
currentSimulator.start('start');
|
|
617
|
+
|
|
618
|
+
while (currentSimulator.getAvailableChoices().length > 0) {
|
|
619
|
+
const state = currentSimulator.getCurrentState();
|
|
620
|
+
|
|
621
|
+
console.log(`\nš ${state.nodeId.toUpperCase()}`);
|
|
622
|
+
console.log('ā'.repeat(60));
|
|
623
|
+
|
|
624
|
+
// Use AI-enhanced text if available
|
|
625
|
+
let displayText = state.text;
|
|
626
|
+
if (aiEnhancedMode) {
|
|
627
|
+
displayText = await currentSimulator.getEnhancedNodeText(state.nodeId);
|
|
628
|
+
}
|
|
629
|
+
console.log(`\n${displayText}\n`);
|
|
630
|
+
|
|
631
|
+
if (state.choices.length === 0) {
|
|
632
|
+
console.log('š THE END\n');
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
console.log('Your choices:');
|
|
637
|
+
state.choices.forEach((choice, i) => {
|
|
638
|
+
const weightBar = 'ā'.repeat(Math.min(Math.floor(choice.weight), 15));
|
|
639
|
+
console.log(` [${i + 1}] ${choice.text}`);
|
|
640
|
+
console.log(` Probability: ${weightBar} ${choice.weight.toFixed(2)}`);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
console.log(`\nš Current State: ${JSON.stringify(state.state)}`);
|
|
644
|
+
console.log(`š Arc Weights: ${JSON.stringify(state.arcWeights)}\n`);
|
|
645
|
+
|
|
646
|
+
const input = await question('Choose (1-' + state.choices.length + '), "save", "export", or "back": ');
|
|
647
|
+
|
|
648
|
+
if (input.toLowerCase() === 'back') {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (input.toLowerCase() === 'save') {
|
|
653
|
+
const saveData = currentSimulator.saveGame();
|
|
654
|
+
const savePath = path.join(process.cwd(), 'savegame.json');
|
|
655
|
+
fs.writeFileSync(savePath, JSON.stringify(saveData, null, 2));
|
|
656
|
+
console.log(`\nš¾ Game saved to ${savePath}\n`);
|
|
657
|
+
await question('Press Enter to continue...');
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (input.toLowerCase() === 'export') {
|
|
662
|
+
const narrative = currentSimulator.exportNarrative();
|
|
663
|
+
const exportPath = path.join(process.cwd(), `story-${Date.now()}.txt`);
|
|
664
|
+
fs.writeFileSync(exportPath, narrative);
|
|
665
|
+
console.log(`\nš Story exported to ${exportPath}\n`);
|
|
666
|
+
await question('Press Enter to continue...');
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const choiceIndex = parseInt(input) - 1;
|
|
671
|
+
if (choiceIndex >= 0 && choiceIndex < state.choices.length) {
|
|
672
|
+
currentSimulator.makeChoice(choiceIndex);
|
|
673
|
+
} else {
|
|
674
|
+
console.log('ā Invalid choice. Try again.');
|
|
675
|
+
await question('Press Enter to continue...');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const finalState = currentSimulator.getCurrentState();
|
|
680
|
+
console.log('\n' + '='.repeat(60));
|
|
681
|
+
console.log('š STORY COMPLETE!');
|
|
682
|
+
console.log('='.repeat(60));
|
|
683
|
+
console.log(`\n${finalState.text}\n`);
|
|
684
|
+
console.log(`š Final State: ${JSON.stringify(finalState.state)}`);
|
|
685
|
+
console.log(`š Final Arc Weights: ${JSON.stringify(finalState.arcWeights)}`);
|
|
686
|
+
console.log(`š Total Decisions: ${finalState.history}\n`);
|
|
687
|
+
|
|
688
|
+
// Check for achievements
|
|
689
|
+
const newAchievements = checkAchievements(currentSimulator);
|
|
690
|
+
if (newAchievements.length > 0) {
|
|
691
|
+
console.log('š NEW ACHIEVEMENTS UNLOCKED!\n');
|
|
692
|
+
newAchievements.forEach(achievement => {
|
|
693
|
+
console.log(`⨠${achievement.name}: ${achievement.desc}`);
|
|
694
|
+
});
|
|
695
|
+
console.log('');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
await question('Press Enter to return to menu...');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function runAISimulation() {
|
|
702
|
+
console.clear();
|
|
703
|
+
console.log('š¤ AI SIMULATION MODE\n');
|
|
704
|
+
console.log('Watch as the AI makes weighted random choices...\n');
|
|
705
|
+
console.log('='.repeat(60) + '\n');
|
|
706
|
+
|
|
707
|
+
const simulator = new StorySimulator(game);
|
|
708
|
+
simulator.start('start');
|
|
709
|
+
|
|
710
|
+
let step = 0;
|
|
711
|
+
while (simulator.getAvailableChoices().length > 0 && step < 20) {
|
|
712
|
+
const state = simulator.getCurrentState();
|
|
713
|
+
|
|
714
|
+
console.log(`š Step ${step + 1}: ${state.nodeId}`);
|
|
715
|
+
console.log(` ${state.text}\n`);
|
|
716
|
+
|
|
717
|
+
if (state.choices.length > 0) {
|
|
718
|
+
console.log(' Available choices:');
|
|
719
|
+
state.choices.forEach((choice, i) => {
|
|
720
|
+
const weightBar = 'ā'.repeat(Math.min(Math.floor(choice.weight * 2), 20));
|
|
721
|
+
console.log(` ${i + 1}. ${choice.text} [${weightBar}] (${choice.weight.toFixed(2)})`);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
console.log('\n š¤ AI choosing...\n');
|
|
725
|
+
await new Promise(resolve => setTimeout(resolve, 800)); // Pause for effect
|
|
726
|
+
simulator.autoChoose();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
step++;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const finalState = simulator.getCurrentState();
|
|
733
|
+
console.log(`\nš ENDING: ${finalState.nodeId}`);
|
|
734
|
+
console.log(` ${finalState.text}\n`);
|
|
735
|
+
console.log(`š Final State: ${JSON.stringify(finalState.state)}`);
|
|
736
|
+
console.log(`š Arc Weights: ${JSON.stringify(finalState.arcWeights)}\n`);
|
|
737
|
+
|
|
738
|
+
await question('Press Enter to return to menu...');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async function runMultipleSimulations() {
|
|
742
|
+
console.clear();
|
|
743
|
+
const count = await question('How many simulations to run? (1-10): ');
|
|
744
|
+
const numSims = Math.min(Math.max(parseInt(count) || 3, 1), 10);
|
|
745
|
+
|
|
746
|
+
console.log(`\nš Running ${numSims} simulations...\n`);
|
|
747
|
+
|
|
748
|
+
const endingCounts = {};
|
|
749
|
+
const arcStats = { hero: [], dark: [], diplomatic: [], chaos: [] };
|
|
750
|
+
|
|
751
|
+
for (let run = 1; run <= numSims; run++) {
|
|
752
|
+
console.log(`š² Simulation ${run}/${numSims}...`);
|
|
753
|
+
|
|
754
|
+
const simulator = new StorySimulator(game);
|
|
755
|
+
simulator.start('start');
|
|
756
|
+
|
|
757
|
+
let step = 0;
|
|
758
|
+
while (simulator.getAvailableChoices().length > 0 && step < 20) {
|
|
759
|
+
simulator.autoChoose();
|
|
760
|
+
step++;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const finalState = simulator.getCurrentState();
|
|
764
|
+
endingCounts[finalState.nodeId] = (endingCounts[finalState.nodeId] || 0) + 1;
|
|
765
|
+
|
|
766
|
+
Object.keys(arcStats).forEach(arc => {
|
|
767
|
+
arcStats[arc].push(finalState.arcWeights[arc] || 0);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
console.log('\n' + '='.repeat(60));
|
|
772
|
+
console.log('š SIMULATION RESULTS');
|
|
773
|
+
console.log('='.repeat(60) + '\n');
|
|
774
|
+
|
|
775
|
+
console.log('Ending Distribution:');
|
|
776
|
+
Object.keys(endingCounts).forEach(ending => {
|
|
777
|
+
const percentage = ((endingCounts[ending] / numSims) * 100).toFixed(1);
|
|
778
|
+
const bar = 'ā'.repeat(Math.floor((endingCounts[ending] / numSims) * 30));
|
|
779
|
+
console.log(` ${ending}: ${bar} ${percentage}% (${endingCounts[ending]}/${numSims})`);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
console.log('\nAverage Final Arc Weights:');
|
|
783
|
+
Object.keys(arcStats).forEach(arc => {
|
|
784
|
+
const avg = arcStats[arc].reduce((a, b) => a + b, 0) / arcStats[arc].length;
|
|
785
|
+
console.log(` ${arc}: ${avg.toFixed(2)}`);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
console.log('');
|
|
789
|
+
await question('Press Enter to return to menu...');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function showStructure() {
|
|
793
|
+
console.clear();
|
|
794
|
+
console.log('šļø STORY STRUCTURE\n');
|
|
795
|
+
console.log('='.repeat(60) + '\n');
|
|
796
|
+
|
|
797
|
+
console.log('ARCS:');
|
|
798
|
+
game.arcs.forEach(arc => {
|
|
799
|
+
console.log(` ⢠${arc.name} (weight: ${arc.weight})`);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
console.log('\nNODES:');
|
|
803
|
+
game.nodes.forEach(node => {
|
|
804
|
+
const tags = node.arcTags.length > 0 ? `[${node.arcTags.join(', ')}]` : '[neutral]';
|
|
805
|
+
console.log(` ⢠${node.id} ${tags}`);
|
|
806
|
+
console.log(` "${node.text.substring(0, 50)}..."`);
|
|
807
|
+
console.log(` ${node.choices.length} choice(s)\n`);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function showStructureAndWait() {
|
|
812
|
+
showStructure();
|
|
813
|
+
await question('\nPress Enter to return to menu...');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Load saved game
|
|
817
|
+
async function loadSavedGame() {
|
|
818
|
+
console.clear();
|
|
819
|
+
const savePath = path.join(process.cwd(), 'savegame.json');
|
|
820
|
+
|
|
821
|
+
if (!fs.existsSync(savePath)) {
|
|
822
|
+
console.log('ā No saved game found!\n');
|
|
823
|
+
await question('Press Enter to return to menu...');
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
const saveData = JSON.parse(fs.readFileSync(savePath, 'utf8'));
|
|
829
|
+
console.log('š¾ Loading saved game...\n');
|
|
830
|
+
console.log(`Saved on: ${saveData.timestamp}`);
|
|
831
|
+
console.log(`Current node: ${saveData.currentNode}`);
|
|
832
|
+
console.log(`State: ${JSON.stringify(saveData.state)}\n`);
|
|
833
|
+
|
|
834
|
+
const confirm = await question('Load this save? (y/n): ');
|
|
835
|
+
if (confirm.toLowerCase() === 'y') {
|
|
836
|
+
currentSimulator = new StorySimulator(game);
|
|
837
|
+
currentSimulator.loadGame(saveData);
|
|
838
|
+
console.log('\nā
Game loaded successfully!\n');
|
|
839
|
+
await question('Press Enter to continue...');
|
|
840
|
+
await playInteractive();
|
|
841
|
+
}
|
|
842
|
+
} catch (error) {
|
|
843
|
+
console.log(`ā Error loading save: ${error.message}\n`);
|
|
844
|
+
await question('Press Enter to return to menu...');
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// View story graph
|
|
849
|
+
async function viewStoryGraph() {
|
|
850
|
+
console.clear();
|
|
851
|
+
console.log('š STORY GRAPH VISUALIZATION\n');
|
|
852
|
+
console.log('='.repeat(60) + '\n');
|
|
853
|
+
|
|
854
|
+
// Build adjacency map
|
|
855
|
+
const connections = new Map();
|
|
856
|
+
game.nodes.forEach(node => {
|
|
857
|
+
const targets = node.choices.map(c => c.target);
|
|
858
|
+
connections.set(node.id, targets);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// Display graph
|
|
862
|
+
game.nodes.forEach(node => {
|
|
863
|
+
const tags = node.arcTags.length > 0 ? `[${node.arcTags.join(', ')}]` : '[neutral]';
|
|
864
|
+
console.log(`š ${node.id} ${tags}`);
|
|
865
|
+
|
|
866
|
+
const targets = connections.get(node.id);
|
|
867
|
+
if (targets.length > 0) {
|
|
868
|
+
targets.forEach((target, i) => {
|
|
869
|
+
const choice = node.choices[i];
|
|
870
|
+
const isLast = i === targets.length - 1;
|
|
871
|
+
const prefix = isLast ? 'āāā' : 'āāā';
|
|
872
|
+
console.log(`${prefix}> ${choice.text} ā ${target}`);
|
|
873
|
+
});
|
|
874
|
+
} else {
|
|
875
|
+
console.log(' (ending)');
|
|
876
|
+
}
|
|
877
|
+
console.log('');
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// Show path statistics
|
|
881
|
+
console.log('\nš PATH STATISTICS:');
|
|
882
|
+
const endingNodes = game.nodes.filter(n => n.choices.length === 0);
|
|
883
|
+
console.log(` Total nodes: ${game.nodes.length}`);
|
|
884
|
+
console.log(` Ending nodes: ${endingNodes.length}`);
|
|
885
|
+
console.log(` Possible endings: ${endingNodes.map(n => n.id).join(', ')}`);
|
|
886
|
+
|
|
887
|
+
await question('\nPress Enter to return to menu...');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Achievement system
|
|
891
|
+
const achievements = [
|
|
892
|
+
{ id: 'hero_path', name: 'True Hero', desc: 'Complete the heroic ending', check: (sim) => sim.currentNode === 'hero_ending' },
|
|
893
|
+
{ id: 'dark_path', name: 'Dark Lord', desc: 'Complete the dark ending', check: (sim) => sim.currentNode === 'dark_ending' },
|
|
894
|
+
{ id: 'diplomat', name: 'Peacemaker', desc: 'Complete the diplomatic ending', check: (sim) => sim.currentNode === 'diplomat_ending' },
|
|
895
|
+
{ id: 'high_rep', name: 'Renowned', desc: 'Achieve reputation of 10+', check: (sim) => (sim.state.reputation || 0) >= 10 },
|
|
896
|
+
{ id: 'power_hungry', name: 'Power Seeker', desc: 'Accumulate 15+ power', check: (sim) => (sim.state.power || 0) >= 15 },
|
|
897
|
+
{ id: 'all_paths', name: 'Master Explorer', desc: 'Complete all three endings', unlocked: false }
|
|
898
|
+
];
|
|
899
|
+
|
|
900
|
+
let unlockedAchievements = new Set();
|
|
901
|
+
|
|
902
|
+
function checkAchievements(simulator) {
|
|
903
|
+
const newUnlocks = [];
|
|
904
|
+
|
|
905
|
+
achievements.forEach(achievement => {
|
|
906
|
+
if (!unlockedAchievements.has(achievement.id) && achievement.check && achievement.check(simulator)) {
|
|
907
|
+
unlockedAchievements.add(achievement.id);
|
|
908
|
+
newUnlocks.push(achievement);
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Check meta achievement
|
|
913
|
+
const endings = ['hero_ending', 'dark_ending', 'diplomat_ending'];
|
|
914
|
+
if (endings.every(e => unlockedAchievements.has(e.replace('_ending', '_path')))) {
|
|
915
|
+
if (!unlockedAchievements.has('all_paths')) {
|
|
916
|
+
unlockedAchievements.add('all_paths');
|
|
917
|
+
newUnlocks.push(achievements.find(a => a.id === 'all_paths'));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return newUnlocks;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function showAchievements() {
|
|
925
|
+
console.clear();
|
|
926
|
+
console.log('š ACHIEVEMENTS\n');
|
|
927
|
+
console.log('='.repeat(60) + '\n');
|
|
928
|
+
|
|
929
|
+
achievements.forEach(achievement => {
|
|
930
|
+
const unlocked = unlockedAchievements.has(achievement.id);
|
|
931
|
+
const icon = unlocked ? 'ā
' : 'š';
|
|
932
|
+
console.log(`${icon} ${achievement.name}`);
|
|
933
|
+
console.log(` ${achievement.desc}\n`);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
console.log(`Progress: ${unlockedAchievements.size}/${achievements.length} unlocked\n`);
|
|
937
|
+
await question('Press Enter to return to menu...');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// AI-Assisted Story Planning using Reflexive Breakpoints
|
|
941
|
+
async function buildCustomStory() {
|
|
942
|
+
console.clear();
|
|
943
|
+
console.log('šØ CUSTOM STORY BUILDER\n');
|
|
944
|
+
console.log('='.repeat(60) + '\n');
|
|
945
|
+
console.log('This feature uses Reflexive AI breakpoints to pause execution');
|
|
946
|
+
console.log('and get help from Claude to plan your custom story!\n');
|
|
947
|
+
|
|
948
|
+
console.log('š Let\'s gather some information about your story:\n');
|
|
949
|
+
|
|
950
|
+
const storyTheme = await question('What is the theme/genre? (fantasy, sci-fi, mystery, etc.): ');
|
|
951
|
+
const storyTone = await question('What tone? (dark, heroic, comedic, dramatic, etc.): ');
|
|
952
|
+
const numNodes = await question('How many key story nodes? (5-15 recommended): ');
|
|
953
|
+
const numArcs = await question('How many story arcs/paths? (2-5 recommended): ');
|
|
954
|
+
|
|
955
|
+
console.log('\nš¤ Triggering Reflexive AI Breakpoint...');
|
|
956
|
+
console.log('Claude will help plan your story structure!\n');
|
|
957
|
+
|
|
958
|
+
// Set state for Claude to see
|
|
959
|
+
if (typeof process !== 'undefined' && process.reflexive && process.reflexive.setState) {
|
|
960
|
+
process.reflexive.setState({
|
|
961
|
+
mode: 'story_planning',
|
|
962
|
+
userInput: {
|
|
963
|
+
theme: storyTheme,
|
|
964
|
+
tone: storyTone,
|
|
965
|
+
targetNodes: parseInt(numNodes) || 10,
|
|
966
|
+
targetArcs: parseInt(numArcs) || 3
|
|
967
|
+
},
|
|
968
|
+
timestamp: new Date().toISOString()
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Trigger breakpoint for Claude to help with planning
|
|
973
|
+
console.log('āøļø PAUSED: Waiting for Claude AI to help design the story...\n');
|
|
974
|
+
|
|
975
|
+
let planResult = null;
|
|
976
|
+
if (typeof process !== 'undefined' && process.reflexive && process.reflexive.breakpoint) {
|
|
977
|
+
planResult = await process.reflexive.breakpoint('story_planning', {
|
|
978
|
+
task: 'Help design a custom story game',
|
|
979
|
+
context: {
|
|
980
|
+
theme: storyTheme,
|
|
981
|
+
tone: storyTone,
|
|
982
|
+
targetNodes: parseInt(numNodes) || 10,
|
|
983
|
+
targetArcs: parseInt(numArcs) || 3
|
|
984
|
+
},
|
|
985
|
+
instructions: `Please help plan a ${storyTheme} story with a ${storyTone} tone.
|
|
986
|
+
Create a structure with ${numNodes} nodes and ${numArcs} arcs.
|
|
987
|
+
Suggest: node names, arc themes, key choice points, and how they connect.
|
|
988
|
+
Return a structured plan with arcs, nodes, and choices arrays.`
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
console.log('\nā
Claude has provided planning assistance!');
|
|
992
|
+
if (planResult && planResult.plan) {
|
|
993
|
+
console.log('\nš Plan Summary:');
|
|
994
|
+
console.log(`Theme: ${planResult.plan.theme}`);
|
|
995
|
+
console.log(`Tone: ${planResult.plan.tone}`);
|
|
996
|
+
console.log(`Arcs: ${planResult.plan.arcs.length}`);
|
|
997
|
+
console.log(`Nodes: ${planResult.plan.nodes.length}`);
|
|
998
|
+
console.log(`Choices: ${planResult.plan.choices.length}`);
|
|
999
|
+
|
|
1000
|
+
console.log('\nšÆ Story Arcs:');
|
|
1001
|
+
planResult.plan.arcs.forEach(arc => {
|
|
1002
|
+
console.log(` ⢠${arc.name}: ${arc.theme}`);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// Ask if user wants to generate the code
|
|
1006
|
+
const generate = await question('\nš Generate story code file? (y/n): ');
|
|
1007
|
+
if (generate.toLowerCase() === 'y') {
|
|
1008
|
+
console.log('\nš¤ Triggering code generation breakpoint...');
|
|
1009
|
+
|
|
1010
|
+
const codeResult = await process.reflexive.breakpoint('code_generation', {
|
|
1011
|
+
task: 'Generate executable story game code',
|
|
1012
|
+
context: {
|
|
1013
|
+
plan: planResult.plan,
|
|
1014
|
+
theme: storyTheme,
|
|
1015
|
+
tone: storyTone
|
|
1016
|
+
},
|
|
1017
|
+
instructions: `Generate a complete JavaScript file that implements this story using the StoryGameBuilder class.
|
|
1018
|
+
Include all arcs, nodes, and choices from the plan.
|
|
1019
|
+
Make it a standalone file that can be run.
|
|
1020
|
+
Use the same patterns as the demo story in one-shot.js.
|
|
1021
|
+
Return the complete code as a string.`
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
if (codeResult && codeResult.code) {
|
|
1025
|
+
const filename = `story-${storyTheme.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}.js`;
|
|
1026
|
+
const filepath = path.join(process.cwd(), filename);
|
|
1027
|
+
|
|
1028
|
+
fs.writeFileSync(filepath, codeResult.code);
|
|
1029
|
+
console.log(`\nā
Story generated: ${filename}`);
|
|
1030
|
+
console.log(`š Location: ${filepath}`);
|
|
1031
|
+
console.log('\nš® You can now run: node ${filename}');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} else {
|
|
1036
|
+
console.log('ā ļø Reflexive breakpoint API not available.');
|
|
1037
|
+
console.log('This feature requires running with Reflexive CLI integration.');
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
console.log('\nš” TIP: The Reflexive CLI agent can see your requirements');
|
|
1041
|
+
console.log('and help generate the story structure code!\n');
|
|
1042
|
+
|
|
1043
|
+
await question('Press Enter to return to menu...');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// ===== MAIN EXECUTION =====
|
|
1047
|
+
|
|
1048
|
+
async function main() {
|
|
1049
|
+
console.log('š Starting Story Game Engine...\n');
|
|
1050
|
+
|
|
1051
|
+
let running = true;
|
|
1052
|
+
while (running) {
|
|
1053
|
+
const choice = await showMainMenu();
|
|
1054
|
+
|
|
1055
|
+
switch (choice) {
|
|
1056
|
+
case '1':
|
|
1057
|
+
await playInteractive();
|
|
1058
|
+
if (currentSimulator) {
|
|
1059
|
+
const newAchievements = checkAchievements(currentSimulator);
|
|
1060
|
+
if (newAchievements.length > 0) {
|
|
1061
|
+
console.log('\nš NEW ACHIEVEMENTS UNLOCKED! š\n');
|
|
1062
|
+
newAchievements.forEach(a => console.log(`š ${a.name}: ${a.desc}`));
|
|
1063
|
+
await question('\nPress Enter to continue...');
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
break;
|
|
1067
|
+
case '2':
|
|
1068
|
+
await runAISimulation();
|
|
1069
|
+
break;
|
|
1070
|
+
case '3':
|
|
1071
|
+
await runMultipleSimulations();
|
|
1072
|
+
break;
|
|
1073
|
+
case '4':
|
|
1074
|
+
await showStructureAndWait();
|
|
1075
|
+
break;
|
|
1076
|
+
case '5':
|
|
1077
|
+
await loadSavedGame();
|
|
1078
|
+
break;
|
|
1079
|
+
case '6':
|
|
1080
|
+
await viewStoryGraph();
|
|
1081
|
+
break;
|
|
1082
|
+
case '7':
|
|
1083
|
+
await showAchievements();
|
|
1084
|
+
break;
|
|
1085
|
+
case '8':
|
|
1086
|
+
aiEnhancedMode = !aiEnhancedMode;
|
|
1087
|
+
console.log(`\n${aiEnhancedMode ? 'š¤ AI-Enhanced Mode ENABLED!' : 'š Classic Mode ENABLED!'}`);
|
|
1088
|
+
console.log(aiEnhancedMode
|
|
1089
|
+
? 'The AI will now generate dynamic atmospheric details and enhance your story!\n'
|
|
1090
|
+
: 'Story will use original static content.\n');
|
|
1091
|
+
await question('Press Enter to continue...');
|
|
1092
|
+
break;
|
|
1093
|
+
case '9':
|
|
1094
|
+
console.log('\nš Thanks for playing! Goodbye.\n');
|
|
1095
|
+
running = false;
|
|
1096
|
+
break;
|
|
1097
|
+
case '10':
|
|
1098
|
+
await buildCustomStory();
|
|
1099
|
+
break;
|
|
1100
|
+
default:
|
|
1101
|
+
console.log('ā Invalid choice. Press Enter...');
|
|
1102
|
+
await question('');
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
rl.close();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
main().catch(console.error);
|