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/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);