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.
@@ -0,0 +1,1246 @@
1
+ // AI-Driven Story Game with Reflexive
2
+ // Fully dynamic - story theme driven by prompts, not hardcoded
3
+
4
+ import readline from 'readline';
5
+ import http from 'http';
6
+ import express from 'express';
7
+ import { readFileSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+ import { makeReflexive } from './src/reflexive.js';
11
+
12
+ // Web server port configuration
13
+ const WEB_PORT = 3100;
14
+
15
+ // Story Configuration - CHANGE THIS TO CHANGE THE ENTIRE GAME
16
+ const STORY_CONFIG = {
17
+ genre: 'comedy adventure',
18
+ theme: 'a hapless lounge lizard trying to find love and fortune in the big city',
19
+ tone: 'humorous, playful, cheeky with clever wordplay and puns',
20
+ craftingGuidelines: [
21
+ 'Use witty observations and self-deprecating humor',
22
+ 'Include double entendres and playful innuendo (keep it PG-13)',
23
+ 'Make situations comedically awkward and embarrassing',
24
+ 'Add unexpected punchlines and comedic timing to descriptions'
25
+ ],
26
+ // Dynamic length control - prompts will adjust story pacing based on quality
27
+ lengthControl: {
28
+ enabled: true,
29
+ showDebug: true, // Display quality metrics during gameplay
30
+ qualityThresholds: {
31
+ excellent: 0.8, // Story is highly engaging, extend it
32
+ good: 0.5, // Story is decent, normal length
33
+ poor: 0.3 // Story is weak, wrap it up
34
+ },
35
+ targetLengths: {
36
+ excellent: '12-15 scenes',
37
+ good: '6-8 scenes',
38
+ poor: '3-4 scenes'
39
+ }
40
+ }
41
+ };
42
+
43
+ // Initialize Reflexive with dynamic configuration
44
+ const reflexive = makeReflexive({
45
+ port: 3098,
46
+ title: `Reflexive ${STORY_CONFIG.genre.charAt(0).toUpperCase() + STORY_CONFIG.genre.slice(1)} Game`,
47
+ systemPrompt: `You are a creative storytelling AI for an interactive ${STORY_CONFIG.genre} game.
48
+
49
+ Theme: ${STORY_CONFIG.theme}
50
+ Tone: ${STORY_CONFIG.tone}
51
+
52
+ Crafting Guidelines:
53
+ ${STORY_CONFIG.craftingGuidelines.map((g, i) => `${i + 1}. ${g}`).join('\n')}
54
+
55
+ Your role is to:
56
+ 1. Generate atmospheric descriptions that enhance the narrative and match the tone
57
+ 2. Create dynamic consequences based on player choices that deepen the experience
58
+ 3. Suggest creative new choice options that fit the theme and genre
59
+ 4. Evolve the story based on player's history and established narrative arcs
60
+ 5. Maintain consistency with the emotional tone and thematic elements
61
+
62
+ Always be concise (1-3 sentences) and match the specified tone perfectly.`
63
+ });
64
+
65
+ // Story Builder Class
66
+ class StoryGameBuilder {
67
+ constructor() {
68
+ this.nodes = new Map();
69
+ this.arcs = new Map();
70
+ this.globalState = {};
71
+ this.metadata = {};
72
+ }
73
+
74
+ setMetadata(data) {
75
+ this.metadata = { ...this.metadata, ...data };
76
+ return this;
77
+ }
78
+
79
+ addNode(id, data) {
80
+ this.nodes.set(id, {
81
+ id,
82
+ text: data.text || '',
83
+ choices: data.choices || [],
84
+ onEnter: data.onEnter || null,
85
+ onExit: data.onExit || null,
86
+ arcTags: data.arcTags || [],
87
+ ...data
88
+ });
89
+ return this;
90
+ }
91
+
92
+ addArc(id, data) {
93
+ this.arcs.set(id, {
94
+ id,
95
+ name: data.name || id,
96
+ weight: data.weight || 1.0,
97
+ description: data.description || '',
98
+ ...data
99
+ });
100
+ return this;
101
+ }
102
+
103
+ addChoice(nodeId, choice) {
104
+ const node = this.nodes.get(nodeId);
105
+ if (!node) throw new Error(`Node ${nodeId} not found`);
106
+
107
+ node.choices.push({
108
+ text: choice.text,
109
+ target: choice.target,
110
+ weight: choice.weight || 1.0,
111
+ weightFormula: choice.weightFormula || null,
112
+ condition: choice.condition || null,
113
+ effects: choice.effects || {},
114
+ arcModifiers: choice.arcModifiers || {}
115
+ });
116
+ return this;
117
+ }
118
+
119
+ getGame() {
120
+ return {
121
+ nodes: Array.from(this.nodes.values()),
122
+ arcs: Array.from(this.arcs.values()),
123
+ globalState: this.globalState,
124
+ metadata: this.metadata
125
+ };
126
+ }
127
+ }
128
+
129
+ // Enhanced Simulator with Reflexive AI
130
+ class AIStorySimulator {
131
+ constructor(game, storyConfig) {
132
+ this.nodes = new Map(game.nodes.map(n => [n.id, n]));
133
+ this.arcs = new Map(game.arcs.map(a => [a.id, a]));
134
+ this.state = { ...game.globalState };
135
+ this.metadata = game.metadata || {};
136
+ this.storyConfig = storyConfig;
137
+ this.currentNode = null;
138
+ this.history = [];
139
+ this.arcWeights = new Map();
140
+ this.storyMemory = [];
141
+
142
+ // Quality tracking for dynamic length adjustment
143
+ this.qualityMetrics = {
144
+ sceneCount: 0,
145
+ engagementScore: 0.5, // Start neutral
146
+ arcProgression: 0,
147
+ lastEnhancementLength: 0
148
+ };
149
+
150
+ // Initialize arc weights
151
+ this.arcs.forEach((arc, id) => {
152
+ this.arcWeights.set(id, arc.weight);
153
+ });
154
+
155
+ reflexive.setState('storyState', this.state);
156
+ reflexive.setState('currentNode', null);
157
+ reflexive.setState('storyConfig', this.storyConfig);
158
+ }
159
+
160
+ // Calculate current story quality for dynamic length adjustment
161
+ calculateStoryQuality() {
162
+ const dominantArcWeight = Math.max(...Array.from(this.arcWeights.values()));
163
+ const arcDiversity = this.arcWeights.size > 0 ?
164
+ new Set(Array.from(this.arcWeights.values()).map(v => Math.floor(v))).size / this.arcWeights.size : 0.5;
165
+
166
+ // Quality based on arc progression, engagement, and narrative momentum
167
+ const quality = (
168
+ (dominantArcWeight / 10) * 0.4 + // Arc development (40%)
169
+ this.qualityMetrics.engagementScore * 0.3 + // Player engagement (30%)
170
+ arcDiversity * 0.3 // Story variety (30%)
171
+ );
172
+
173
+ return Math.min(1.0, Math.max(0, quality));
174
+ }
175
+
176
+ // Get pacing guidance based on quality
177
+ getPacingGuidance() {
178
+ if (!this.storyConfig.lengthControl?.enabled) {
179
+ return 'normal pacing';
180
+ }
181
+
182
+ const quality = this.calculateStoryQuality();
183
+ const thresholds = this.storyConfig.lengthControl.qualityThresholds;
184
+ const sceneCount = this.qualityMetrics.sceneCount;
185
+
186
+ if (quality >= thresholds.excellent) {
187
+ return `Story is excellent (quality: ${quality.toFixed(2)}). Extend the narrative with rich details and new plot threads. Target: ${this.storyConfig.lengthControl.targetLengths.excellent}. Currently at scene ${sceneCount}.`;
188
+ } else if (quality >= thresholds.good) {
189
+ return `Story is good (quality: ${quality.toFixed(2)}). Maintain steady pacing with meaningful progression. Target: ${this.storyConfig.lengthControl.targetLengths.good}. Currently at scene ${sceneCount}.`;
190
+ } else {
191
+ return `Story quality is low (${quality.toFixed(2)}). Begin wrapping up toward a satisfying conclusion. Target: ${this.storyConfig.lengthControl.targetLengths.poor}. Currently at scene ${sceneCount}.`;
192
+ }
193
+ }
194
+
195
+ // Use AI to enhance node text with atmosphere
196
+ async getEnhancedNodeText(nodeId) {
197
+ const node = this.nodes.get(nodeId);
198
+ if (!node) return '';
199
+
200
+ this.qualityMetrics.sceneCount++;
201
+
202
+ const recentJourney = this.history.slice(-3).map(h => h.nodeId).join(' → ');
203
+ const dominantArc = this.getDominantArc();
204
+ const arcDescription = this.arcs.get(dominantArc)?.description || dominantArc;
205
+ const pacingGuidance = this.getPacingGuidance();
206
+
207
+ const prompt = `Scene: "${node.text}"
208
+ Recent journey: ${recentJourney || 'Beginning'}
209
+ Dominant narrative arc: ${arcDescription}
210
+ Player state: ${JSON.stringify(this.state)}
211
+
212
+ PACING GUIDANCE: ${pacingGuidance}
213
+
214
+ Add ONE atmospheric sentence that enhances this scene in the style of ${this.storyConfig.genre}. Match the tone: ${this.storyConfig.tone}.
215
+ ${pacingGuidance.includes('Extend') ? 'Make it rich and detailed to build narrative depth.' : ''}
216
+ ${pacingGuidance.includes('wrapping up') ? 'Keep it concise and hint at resolution approaching.' : ''}
217
+ Return ONLY the enhancement sentence, nothing else.`;
218
+
219
+ try {
220
+ const enhancement = await reflexive.chat(prompt);
221
+ this.qualityMetrics.lastEnhancementLength = enhancement.length;
222
+
223
+ // Update engagement score based on enhancement quality
224
+ this.qualityMetrics.engagementScore =
225
+ (this.qualityMetrics.engagementScore * 0.7) +
226
+ (Math.min(1.0, enhancement.length / 150) * 0.3);
227
+
228
+ this.storyMemory.push({ type: 'enhancement', node: nodeId, text: enhancement });
229
+ return `${node.text}\n\n${enhancement.trim()}`;
230
+ } catch (error) {
231
+ console.error('AI enhancement failed:', error.message);
232
+ return node.text;
233
+ }
234
+ }
235
+
236
+ // Use AI to generate dynamic choice consequences
237
+ async getChoiceConsequence(choice, choiceIndex) {
238
+ const prompt = `The player chose: "${choice.text}"
239
+ Genre: ${this.storyConfig.genre}
240
+ Current state: ${JSON.stringify(this.state)}
241
+ Arc weights: ${JSON.stringify(Object.fromEntries(this.arcWeights))}
242
+
243
+ Generate a brief (1 sentence) consequence that describes what happens. Match the tone: ${this.storyConfig.tone}.
244
+ Return ONLY the consequence text.`;
245
+
246
+ try {
247
+ const consequence = await reflexive.chat(prompt);
248
+ return consequence.trim();
249
+ } catch (error) {
250
+ return "Your choice sets events in motion.";
251
+ }
252
+ }
253
+
254
+ // Use AI to suggest an extra dynamic choice
255
+ async generateBonusChoice(nodeId) {
256
+ const node = this.nodes.get(nodeId);
257
+ const dominantArc = this.getDominantArc();
258
+ const arcDescription = this.arcs.get(dominantArc)?.description || dominantArc;
259
+ const pacingGuidance = this.getPacingGuidance();
260
+ const quality = this.calculateStoryQuality();
261
+
262
+ // Adjust bonus choice frequency based on quality
263
+ const shouldGenerateBonus = quality > 0.4; // Only generate if quality is decent
264
+ if (!shouldGenerateBonus) return null;
265
+
266
+ const prompt = `Scene: "${node.text}"
267
+ Genre: ${this.storyConfig.genre}
268
+ Dominant arc: ${arcDescription}
269
+ Recent events: ${this.history.slice(-2).map(h => h.nodeId).join(', ')}
270
+
271
+ PACING GUIDANCE: ${pacingGuidance}
272
+
273
+ Suggest ONE creative choice (4-7 words) that fits this moment and matches the ${this.storyConfig.genre} theme.
274
+ ${pacingGuidance.includes('Extend') ? 'Suggest a choice that opens new story possibilities.' : ''}
275
+ ${pacingGuidance.includes('wrapping up') ? 'Suggest a choice that moves toward resolution.' : ''}
276
+ Return ONLY the choice text, nothing else.`;
277
+
278
+ try {
279
+ const bonusChoice = await reflexive.chat(prompt);
280
+ return bonusChoice.trim();
281
+ } catch (error) {
282
+ return null;
283
+ }
284
+ }
285
+
286
+ getDominantArc() {
287
+ let maxWeight = 0;
288
+ let dominant = 'neutral';
289
+
290
+ for (const [arc, weight] of this.arcWeights) {
291
+ if (weight > maxWeight) {
292
+ maxWeight = weight;
293
+ dominant = arc;
294
+ }
295
+ }
296
+ return dominant;
297
+ }
298
+
299
+ start(startNodeId) {
300
+ this.currentNode = startNodeId;
301
+ this.enterNode(startNodeId);
302
+ return this.getCurrentState();
303
+ }
304
+
305
+ enterNode(nodeId) {
306
+ const node = this.nodes.get(nodeId);
307
+ if (!node) throw new Error(`Node ${nodeId} not found`);
308
+
309
+ this.history.push({
310
+ nodeId,
311
+ timestamp: Date.now(),
312
+ state: { ...this.state }
313
+ });
314
+
315
+ if (node.onEnter) {
316
+ node.onEnter(this.state, this);
317
+ }
318
+
319
+ node.arcTags.forEach(arcId => {
320
+ const current = this.arcWeights.get(arcId) || 1.0;
321
+ this.arcWeights.set(arcId, current * 1.2);
322
+ });
323
+
324
+ reflexive.setState('currentNode', nodeId);
325
+ reflexive.setState('storyState', this.state);
326
+ reflexive.setState('history', this.history.map(h => h.nodeId));
327
+ }
328
+
329
+ getAvailableChoices() {
330
+ const node = this.nodes.get(this.currentNode);
331
+ if (!node) return [];
332
+
333
+ return node.choices
334
+ .filter(choice => {
335
+ if (choice.condition) {
336
+ return choice.condition(this.state, this);
337
+ }
338
+ return true;
339
+ })
340
+ .map(choice => {
341
+ let weight = choice.weight;
342
+
343
+ if (choice.weightFormula) {
344
+ weight = choice.weightFormula(this.state, this.arcWeights, this);
345
+ }
346
+
347
+ Object.keys(choice.arcModifiers).forEach(arcId => {
348
+ const arcWeight = this.arcWeights.get(arcId) || 1.0;
349
+ weight *= arcWeight * choice.arcModifiers[arcId];
350
+ });
351
+
352
+ return {
353
+ ...choice,
354
+ calculatedWeight: weight
355
+ };
356
+ });
357
+ }
358
+
359
+ async makeChoice(choiceIndex) {
360
+ const choices = this.getAvailableChoices();
361
+ if (choiceIndex >= choices.length) {
362
+ throw new Error('Invalid choice index');
363
+ }
364
+
365
+ const choice = choices[choiceIndex];
366
+
367
+ // Get AI-generated consequence
368
+ const consequence = await this.getChoiceConsequence(choice, choiceIndex);
369
+ console.log(`\nšŸ’« ${consequence}\n`);
370
+
371
+ const currentNode = this.nodes.get(this.currentNode);
372
+ if (currentNode.onExit) {
373
+ currentNode.onExit(this.state, this);
374
+ }
375
+
376
+ Object.assign(this.state, choice.effects);
377
+
378
+ Object.keys(choice.arcModifiers).forEach(arcId => {
379
+ const current = this.arcWeights.get(arcId) || 1.0;
380
+ const modifier = choice.arcModifiers[arcId];
381
+ this.arcWeights.set(arcId, current * modifier);
382
+ });
383
+
384
+ this.currentNode = choice.target;
385
+ this.enterNode(choice.target);
386
+
387
+ return this.getCurrentState();
388
+ }
389
+
390
+ getCurrentState() {
391
+ const node = this.nodes.get(this.currentNode);
392
+ const choices = this.getAvailableChoices();
393
+
394
+ return {
395
+ nodeId: this.currentNode,
396
+ text: node.text,
397
+ choices: choices.map((c, i) => ({
398
+ index: i,
399
+ text: c.text,
400
+ weight: c.calculatedWeight
401
+ })),
402
+ state: { ...this.state },
403
+ arcWeights: Object.fromEntries(this.arcWeights),
404
+ history: this.history.length
405
+ };
406
+ }
407
+ }
408
+
409
+ // Dynamic Story Generator - driven by STORY_CONFIG
410
+ async function generateStoryFromPrompt(config) {
411
+ const prompt = `Create a ${config.genre} interactive story with the theme: "${config.theme}".
412
+
413
+ Generate a story structure with:
414
+ 1. Three narrative arcs that fit the theme (give each a name and description)
415
+ 2. A compelling starting scenario
416
+ 3. 2-3 branching paths that lead to different outcomes
417
+
418
+ Return a JSON object with this structure:
419
+ {
420
+ "arcs": [{"id": "arc1", "name": "Arc Name", "description": "...", "weight": 1.0}],
421
+ "startNode": {"id": "start", "text": "Opening scene...", "arcTags": []},
422
+ "nodes": [{"id": "node1", "text": "...", "arcTags": ["arc1"], "stateEffects": {...}}],
423
+ "choices": [{"from": "start", "text": "...", "to": "node1", "arcModifiers": {"arc1": 1.5}}]
424
+ }
425
+
426
+ Keep descriptions vivid and match the tone: ${config.tone}.`;
427
+
428
+ try {
429
+ const response = await reflexive.chat(prompt);
430
+ // Try to parse JSON from response
431
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
432
+ if (jsonMatch) {
433
+ return JSON.parse(jsonMatch[0]);
434
+ }
435
+ } catch (error) {
436
+ console.log('āš ļø AI story generation not available, using fallback story\n');
437
+ }
438
+
439
+ return null;
440
+ }
441
+
442
+ // Fallback: Manual story based on config
443
+ function createStoryFromConfig(config) {
444
+ const builder = new StoryGameBuilder();
445
+
446
+ builder.setMetadata({
447
+ genre: config.genre,
448
+ theme: config.theme,
449
+ tone: config.tone
450
+ });
451
+
452
+ if (config.genre === 'comedy adventure') {
453
+ builder
454
+ .addArc('smooth', {
455
+ name: 'Smooth Operator',
456
+ weight: 1.0,
457
+ description: 'trying to be cool and suave (with mixed results)'
458
+ })
459
+ .addArc('awkward', {
460
+ name: 'Awkward Moments',
461
+ weight: 1.0,
462
+ description: 'stumbling through embarrassing social situations'
463
+ })
464
+ .addArc('lucky', {
465
+ name: 'Dumb Luck',
466
+ weight: 0.5,
467
+ description: 'succeeding through sheer coincidence and fortune'
468
+ });
469
+
470
+ builder
471
+ .addNode('start', {
472
+ text: 'You stand outside Club Polyester, the hottest disco in town. Your leisure suit is freshly pressed, your cologne is... aggressive. A bouncer with arms like tree trunks blocks the velvet rope.',
473
+ arcTags: []
474
+ })
475
+ .addNode('smooth_talk', {
476
+ text: 'You flash your "winning" smile and slip the bouncer a twenty. He looks at the bill, then at you, then bursts out laughing. "Kid, this is a Monopoly twenty." But he lets you in anyway out of pity.',
477
+ arcTags: ['awkward'],
478
+ onEnter: (state) => { state.confidence = (state.confidence || 50) - 10; state.money = (state.money || 100) - 0; }
479
+ })
480
+ .addNode('sneak_in', {
481
+ text: 'You spot a delivery entrance and slip in behind a crate of disco balls. Unfortunately, you trip and send them rolling across the dance floor. Everyone applauds, thinking it\'s part of the show.',
482
+ arcTags: ['lucky'],
483
+ onEnter: (state) => { state.confidence = (state.confidence || 50) + 20; }
484
+ })
485
+ .addNode('bar', {
486
+ text: 'At the bar, you order "something sophisticated" and the bartender hands you a glass of chocolate milk with a tiny umbrella. Next to you sits an attractive stranger reading "The Art of Conversation."',
487
+ arcTags: []
488
+ })
489
+ .addNode('pickup_line', {
490
+ text: 'You deploy your best pickup line: "Are you a parking ticket? Because you\'ve got FINE written all over you." They groan audibly but can\'t help smiling. "Points for commitment to the bit," they say.',
491
+ arcTags: ['awkward'],
492
+ onEnter: (state) => { state.charisma = (state.charisma || 0) + 5; state.reputation = 'endearing_disaster'; }
493
+ })
494
+ .addNode('be_genuine', {
495
+ text: 'You nervously admit you have no idea what you\'re doing and just wanted to meet someone interesting. They laugh warmly. "Finally, some honesty in this place. I\'m Alex. Want to get out of here?"',
496
+ arcTags: ['smooth'],
497
+ onEnter: (state) => { state.connection = true; state.confidence = (state.confidence || 50) + 30; }
498
+ })
499
+ .addNode('awkward_ending', {
500
+ text: 'You both leave the club, but in your excitement you walk into a glass door. Alex helps you up, still laughing. "You know what? That was actually kind of adorable." Score!',
501
+ arcTags: ['awkward']
502
+ })
503
+ .addNode('smooth_ending', {
504
+ text: 'You walk out together under the neon lights. For once, everything went right. Well, mostly right. Okay, you tripped once. But Alex caught you, and that counts for something.',
505
+ arcTags: ['smooth']
506
+ })
507
+ .addNode('lucky_ending', {
508
+ text: 'As you leave together, a street photographer mistakes you two for models and takes your picture. It ends up on the cover of "Awkward Romance Monthly." You frame it.',
509
+ arcTags: ['lucky']
510
+ });
511
+
512
+ builder
513
+ .addChoice('start', {
514
+ text: 'Try to smooth talk the bouncer',
515
+ target: 'smooth_talk',
516
+ arcModifiers: { awkward: 1.8 }
517
+ })
518
+ .addChoice('start', {
519
+ text: 'Find another way in',
520
+ target: 'sneak_in',
521
+ arcModifiers: { lucky: 2.0 }
522
+ });
523
+
524
+ builder
525
+ .addChoice('smooth_talk', {
526
+ text: 'Head to the bar',
527
+ target: 'bar'
528
+ })
529
+ .addChoice('sneak_in', {
530
+ text: 'Head to the bar',
531
+ target: 'bar'
532
+ });
533
+
534
+ builder
535
+ .addChoice('bar', {
536
+ text: 'Use your best pickup line',
537
+ target: 'pickup_line',
538
+ arcModifiers: { awkward: 2.5 },
539
+ weightFormula: (state, arcWeights) => (arcWeights.get('awkward') || 1.0)
540
+ })
541
+ .addChoice('bar', {
542
+ text: 'Just be yourself',
543
+ target: 'be_genuine',
544
+ arcModifiers: { smooth: 2.5 },
545
+ weightFormula: (state, arcWeights) => (arcWeights.get('smooth') || 1.0)
546
+ });
547
+
548
+ builder
549
+ .addChoice('pickup_line', {
550
+ text: 'Suggest getting some fresh air',
551
+ target: 'awkward_ending',
552
+ arcModifiers: { awkward: 2.0 }
553
+ })
554
+ .addChoice('pickup_line', {
555
+ text: 'Ask them to teach you to be cool',
556
+ target: 'lucky_ending',
557
+ arcModifiers: { lucky: 3.0 }
558
+ })
559
+ .addChoice('be_genuine', {
560
+ text: 'Leave together',
561
+ target: 'smooth_ending',
562
+ arcModifiers: { smooth: 2.0 }
563
+ });
564
+ } else if (config.genre === 'horror') {
565
+ builder
566
+ .addArc('survival', {
567
+ name: 'Survival Path',
568
+ weight: 1.0,
569
+ description: 'fighting to stay alive and escape'
570
+ })
571
+ .addArc('madness', {
572
+ name: 'Madness Path',
573
+ weight: 1.0,
574
+ description: 'descending into psychological horror and insanity'
575
+ })
576
+ .addArc('cursed', {
577
+ name: 'Cursed Path',
578
+ weight: 0.5,
579
+ description: 'becoming entangled with dark supernatural forces'
580
+ });
581
+
582
+ builder
583
+ .addNode('start', {
584
+ text: 'You awaken in a decrepit mansion. The air reeks of decay. Through the window, you see only darkness—no moon, no stars. A door creaks open down the hall.',
585
+ arcTags: []
586
+ })
587
+ .addNode('investigate_sound', {
588
+ text: 'You follow the sound down the hallway. The floorboards groan beneath your feet. You find a room with strange symbols drawn in blood on the walls.',
589
+ arcTags: ['survival'],
590
+ onEnter: (state) => { state.sanity = (state.sanity || 100) - 10; }
591
+ })
592
+ .addNode('hide_in_room', {
593
+ text: 'You lock yourself in the nearest room. Something scratches at the door from outside. The scratching grows more frantic.',
594
+ arcTags: ['survival'],
595
+ onEnter: (state) => { state.fear = (state.fear || 0) + 15; }
596
+ })
597
+ .addNode('basement', {
598
+ text: 'You descend into the basement. The smell is overwhelming. In the dim light, you see a shrine made of bones. A journal lies open beside it.',
599
+ arcTags: ['cursed']
600
+ })
601
+ .addNode('read_journal', {
602
+ text: 'The journal speaks of an ancient ritual. The previous owner tried to summon something. The last entry reads: "It worked. God help me, it worked."',
603
+ arcTags: ['madness'],
604
+ onEnter: (state) => { state.knowledge = 'ritual'; state.sanity = (state.sanity || 100) - 20; }
605
+ })
606
+ .addNode('destroy_shrine', {
607
+ text: 'You smash the bone shrine. A deafening screech echoes through the house. Everything goes silent. Too silent.',
608
+ arcTags: ['survival'],
609
+ onEnter: (state) => { state.angry_entity = true; }
610
+ })
611
+ .addNode('trapped_ending', {
612
+ text: 'The doors and windows seal themselves. The walls begin to bleed. You realize with horror: you are not trapped in here with it. It is trapped in here with you. But you cannot leave.',
613
+ arcTags: ['cursed']
614
+ })
615
+ .addNode('escape_ending', {
616
+ text: 'You break through a window and run. Behind you, the mansion collapses into the earth. But in your pocket, you find a bone from the shrine. It pulses with warmth.',
617
+ arcTags: ['survival']
618
+ })
619
+ .addNode('ritual_ending', {
620
+ text: 'You complete the ritual from the journal. The entity appears before you, ancient and terrible. You become its vessel. Your consciousness fades as something else takes control.',
621
+ arcTags: ['madness']
622
+ });
623
+
624
+ builder
625
+ .addChoice('start', {
626
+ text: 'Investigate the creaking door',
627
+ target: 'investigate_sound',
628
+ arcModifiers: { survival: 1.8 }
629
+ })
630
+ .addChoice('start', {
631
+ text: 'Hide and barricade yourself',
632
+ target: 'hide_in_room',
633
+ arcModifiers: { survival: 2.0 }
634
+ });
635
+
636
+ builder
637
+ .addChoice('investigate_sound', {
638
+ text: 'Search for a way out',
639
+ target: 'basement',
640
+ arcModifiers: { cursed: 1.5 }
641
+ })
642
+ .addChoice('hide_in_room', {
643
+ text: 'The scratching stops. Venture out',
644
+ target: 'basement'
645
+ });
646
+
647
+ builder
648
+ .addChoice('basement', {
649
+ text: 'Read the journal carefully',
650
+ target: 'read_journal',
651
+ arcModifiers: { madness: 2.5 },
652
+ weightFormula: (state, arcWeights) => (arcWeights.get('madness') || 1.0)
653
+ })
654
+ .addChoice('basement', {
655
+ text: 'Destroy the shrine',
656
+ target: 'destroy_shrine',
657
+ arcModifiers: { survival: 2.0 },
658
+ weightFormula: (state, arcWeights) => (arcWeights.get('survival') || 1.0)
659
+ });
660
+
661
+ builder
662
+ .addChoice('read_journal', {
663
+ text: 'Attempt to complete the ritual',
664
+ target: 'ritual_ending',
665
+ arcModifiers: { madness: 3.0 }
666
+ })
667
+ .addChoice('read_journal', {
668
+ text: 'Flee the house immediately',
669
+ target: 'escape_ending',
670
+ arcModifiers: { survival: 2.0 }
671
+ })
672
+ .addChoice('destroy_shrine', {
673
+ text: 'Run while you still can',
674
+ target: 'escape_ending'
675
+ })
676
+ .addChoice('destroy_shrine', {
677
+ text: 'Stand your ground',
678
+ target: 'trapped_ending',
679
+ arcModifiers: { cursed: 2.0 }
680
+ });
681
+ }
682
+
683
+ return builder.getGame();
684
+ }
685
+
686
+ // Web Server Setup
687
+ const app = express();
688
+ const activeSessions = new Map();
689
+
690
+ app.use(express.json());
691
+
692
+ // Serve the game interface
693
+ const __filename = fileURLToPath(import.meta.url);
694
+ const __dirname = dirname(__filename);
695
+
696
+ app.get('/', (req, res) => {
697
+ const html = readFileSync(join(__dirname, 'dashboard.html'), 'utf8');
698
+ res.send(html);
699
+ });
700
+
701
+ app.get('/old', (req, res) => {
702
+ let html = readFileSync(join(__dirname, 'new-ui-template.html'), 'utf8');
703
+ html = html.replace('THEME_PLACEHOLDER', STORY_CONFIG.theme);
704
+ res.send(html);
705
+ });
706
+
707
+ // API endpoint to start a new game session
708
+ app.post('/api/start', async (req, res) => {
709
+ const sessionId = Date.now().toString();
710
+ const game = createStoryFromConfig(STORY_CONFIG);
711
+ const simulator = new AIStorySimulator(game, STORY_CONFIG);
712
+ simulator.start('start');
713
+
714
+ activeSessions.set(sessionId, simulator);
715
+
716
+ const state = simulator.getCurrentState();
717
+ const enhancedText = await simulator.getEnhancedNodeText(state.nodeId);
718
+ const quality = simulator.calculateStoryQuality();
719
+
720
+ res.json({
721
+ sessionId,
722
+ state: {
723
+ text: enhancedText,
724
+ choices: state.choices,
725
+ quality: quality,
726
+ sceneCount: simulator.qualityMetrics.sceneCount,
727
+ engagement: simulator.qualityMetrics.engagementScore
728
+ }
729
+ });
730
+ });
731
+
732
+ // API endpoint to make a choice
733
+ app.post('/api/choice', async (req, res) => {
734
+ const { sessionId, choiceIndex } = req.body;
735
+ const simulator = activeSessions.get(sessionId);
736
+
737
+ if (!simulator) {
738
+ return res.status(404).json({ error: 'Session not found' });
739
+ }
740
+
741
+ await simulator.makeChoice(choiceIndex);
742
+ const state = simulator.getCurrentState();
743
+ const enhancedText = await simulator.getEnhancedNodeText(state.nodeId);
744
+ const quality = simulator.calculateStoryQuality();
745
+
746
+ res.json({
747
+ state: {
748
+ text: enhancedText,
749
+ choices: state.choices,
750
+ quality: quality,
751
+ sceneCount: simulator.qualityMetrics.sceneCount,
752
+ engagement: simulator.qualityMetrics.engagementScore
753
+ }
754
+ });
755
+ });
756
+
757
+ // API endpoint to handle custom prompts
758
+ app.post('/api/prompt', async (req, res) => {
759
+ const { sessionId, prompt } = req.body;
760
+ const simulator = activeSessions.get(sessionId);
761
+
762
+ if (!simulator) {
763
+ return res.status(404).json({ error: 'Session not found' });
764
+ }
765
+
766
+ const state = simulator.getCurrentState();
767
+ const node = simulator.nodes.get(state.nodeId);
768
+
769
+ // Use AI to generate the next scene based on the custom prompt
770
+ const dominantArc = simulator.getDominantArc();
771
+ const arcDescription = simulator.arcs.get(dominantArc)?.description || dominantArc;
772
+ const pacingGuidance = simulator.getPacingGuidance();
773
+
774
+ const aiPrompt = `Current scene: "${node.text}"
775
+
776
+ Genre: ${simulator.storyConfig.genre}
777
+ Tone: ${simulator.storyConfig.tone}
778
+ Dominant story arc: ${arcDescription}
779
+ Player action: "${prompt}"
780
+
781
+ PACING GUIDANCE: ${pacingGuidance}
782
+
783
+ Generate the NEXT scene that follows from the player's action. Write 2-4 sentences describing what happens next.
784
+ Stay true to the ${simulator.storyConfig.genre} genre and maintain the tone.
785
+ ${simulator.storyConfig.craftingGuidelines.map(g => '- ' + g).join('\\n')}
786
+
787
+ Return ONLY the scene text, nothing else.`;
788
+
789
+ try {
790
+ const nextSceneText = await reflexive.chat(aiPrompt);
791
+
792
+ // Update quality metrics
793
+ simulator.qualityMetrics.sceneCount++;
794
+ simulator.qualityMetrics.lastEnhancementLength = nextSceneText.length;
795
+
796
+ // Generate new choices based on the scene
797
+ const choicesPrompt = `Scene: "${nextSceneText}"
798
+
799
+ Genre: ${simulator.storyConfig.genre}
800
+ Dominant arc: ${arcDescription}
801
+
802
+ Generate 2-3 choice options (each 4-7 words) that fit this moment and the ${simulator.storyConfig.genre} theme.
803
+ ${pacingGuidance.includes('wrapping up') ? 'Include options that move toward resolution.' : ''}
804
+
805
+ Return as a JSON array of strings: ["choice 1", "choice 2", "choice 3"]`;
806
+
807
+ const choicesResponse = await reflexive.chat(choicesPrompt);
808
+ let newChoices = [];
809
+ try {
810
+ newChoices = JSON.parse(choicesResponse.trim());
811
+ } catch (e) {
812
+ // Fallback choices
813
+ newChoices = ["Continue forward", "Look around", "Take a different approach"];
814
+ }
815
+
816
+ const quality = simulator.calculateStoryQuality();
817
+
818
+ res.json({
819
+ state: {
820
+ text: nextSceneText,
821
+ choices: newChoices.map((text, index) => ({
822
+ index,
823
+ text,
824
+ weight: 1.0
825
+ })),
826
+ quality: quality,
827
+ sceneCount: simulator.qualityMetrics.sceneCount,
828
+ engagement: simulator.qualityMetrics.engagementScore
829
+ }
830
+ });
831
+ } catch (error) {
832
+ res.status(500).json({ error: 'Failed to generate response: ' + error.message });
833
+ }
834
+ });
835
+
836
+ // Start web server
837
+ app.listen(WEB_PORT, () => {
838
+ console.log(`🌐 Web interface: http://localhost:${WEB_PORT}`);
839
+ });
840
+
841
+ // Main Game Loop
842
+ const rl = readline.createInterface({
843
+ input: process.stdin,
844
+ output: process.stdout
845
+ });
846
+
847
+ function question(prompt) {
848
+ return new Promise((resolve) => {
849
+ rl.question(prompt, resolve);
850
+ });
851
+ }
852
+
853
+ async function playGame() {
854
+ console.clear();
855
+ const emoji = STORY_CONFIG.genre === 'horror' ? 'šŸ•Æļø' : STORY_CONFIG.genre === 'comedy adventure' ? 'šŸ˜Ž' : 'šŸŽ®';
856
+ console.log(`${emoji} REFLEXIVE ${STORY_CONFIG.genre.toUpperCase()} GAME ${emoji}\n`);
857
+ console.log(`Theme: ${STORY_CONFIG.theme}\n`);
858
+ console.log('='.repeat(60) + '\n');
859
+
860
+ const game = createStoryFromConfig(STORY_CONFIG);
861
+ const simulator = new AIStorySimulator(game, STORY_CONFIG);
862
+ simulator.start('start');
863
+
864
+ while (simulator.getAvailableChoices().length > 0) {
865
+ const state = simulator.getCurrentState();
866
+
867
+ console.log(`\nšŸ“ ${state.nodeId.toUpperCase()}`);
868
+ console.log('─'.repeat(60));
869
+
870
+ // Get AI-enhanced text
871
+ const enhancedText = await simulator.getEnhancedNodeText(state.nodeId);
872
+ console.log(`\n${enhancedText}\n`);
873
+
874
+ if (state.choices.length === 0) {
875
+ console.log('šŸ THE END\n');
876
+ break;
877
+ }
878
+
879
+ console.log('Your choices:');
880
+ state.choices.forEach((choice, i) => {
881
+ const weightBar = 'ā–ˆ'.repeat(Math.min(Math.floor(choice.weight), 15));
882
+ console.log(` [${i + 1}] ${choice.text}`);
883
+ console.log(` ${weightBar} ${choice.weight.toFixed(2)}`);
884
+ });
885
+
886
+ // Optionally generate bonus AI choice
887
+ if (Math.random() > 0.7) {
888
+ const bonusChoice = await simulator.generateBonusChoice(state.nodeId);
889
+ if (bonusChoice) {
890
+ console.log(` [✨] ${bonusChoice} (AI-generated)`);
891
+ }
892
+ }
893
+
894
+ console.log(`\nšŸ“Š State: ${JSON.stringify(state.state)}`);
895
+ console.log(`šŸ“ˆ Arcs: ${JSON.stringify(state.arcWeights)}`);
896
+
897
+ // Optional: Show quality metrics (debug mode)
898
+ if (STORY_CONFIG.lengthControl?.showDebug) {
899
+ const quality = simulator.calculateStoryQuality();
900
+ console.log(`šŸŽÆ Quality: ${(quality * 100).toFixed(1)}% | Scene: ${simulator.qualityMetrics.sceneCount} | Engagement: ${(simulator.qualityMetrics.engagementScore * 100).toFixed(1)}%`);
901
+ }
902
+ console.log();
903
+
904
+ const input = await question(`Choose (1-${state.choices.length}): `);
905
+ const choiceIndex = parseInt(input) - 1;
906
+
907
+ if (choiceIndex >= 0 && choiceIndex < state.choices.length) {
908
+ await simulator.makeChoice(choiceIndex);
909
+ } else {
910
+ console.log('āŒ Invalid choice. Try again.');
911
+ }
912
+ }
913
+
914
+ const finalState = simulator.getCurrentState();
915
+ console.log('\n' + '='.repeat(60));
916
+ console.log('šŸ STORY COMPLETE!');
917
+ console.log('='.repeat(60));
918
+ console.log(`\n${finalState.text}\n`);
919
+ console.log(`šŸ“Š Final State: ${JSON.stringify(finalState.state)}`);
920
+ console.log(`šŸ“ˆ Final Arcs: ${JSON.stringify(finalState.arcWeights)}\n`);
921
+
922
+ rl.close();
923
+ }
924
+
925
+ // Startup - Run both CLI and web server simultaneously
926
+ const emoji = STORY_CONFIG.genre === 'horror' ? 'šŸ•Æļø' : STORY_CONFIG.genre === 'comedy adventure' ? 'šŸ˜Ž' : 'šŸš€';
927
+ console.log(`${emoji} Starting Reflexive ${STORY_CONFIG.genre.charAt(0).toUpperCase() + STORY_CONFIG.genre.slice(1)} Game...\n`);
928
+ console.log('šŸ’” Dashboard: http://localhost:3098/reflexive\n');
929
+ console.log(`šŸ“ Current Configuration:`);
930
+ console.log(` Genre: ${STORY_CONFIG.genre}`);
931
+ console.log(` Theme: ${STORY_CONFIG.theme}`);
932
+ console.log(` Tone: ${STORY_CONFIG.tone}`);
933
+ if (STORY_CONFIG.lengthControl?.enabled) {
934
+ console.log(`\n⚔ Dynamic Length Control: ENABLED`);
935
+ console.log(` The AI will subtly adjust story length based on quality:`);
936
+ console.log(` • Excellent stories → Extended narrative (${STORY_CONFIG.lengthControl.targetLengths.excellent})`);
937
+ console.log(` • Good stories → Normal pacing (${STORY_CONFIG.lengthControl.targetLengths.good})`);
938
+ console.log(` • Weak stories → Graceful wrap-up (${STORY_CONFIG.lengthControl.targetLengths.poor})`);
939
+ }
940
+ console.log();
941
+
942
+ playGame().catch(console.error);
943
+
944
+ // Old server mode code removed - everything below this can be deleted
945
+ if (false) {
946
+ // Server Mode - Run as web server
947
+ const sessions = new Map();
948
+
949
+ function generateSessionId() {
950
+ return Math.random().toString(36).substring(2, 15);
951
+ }
952
+
953
+ const server = http.createServer(async (req, res) => {
954
+ const url = new URL(req.url, `http://${req.headers.host}`);
955
+
956
+ // CORS headers
957
+ res.setHeader('Access-Control-Allow-Origin', '*');
958
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
959
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
960
+
961
+ if (req.method === 'OPTIONS') {
962
+ res.writeHead(200);
963
+ res.end();
964
+ return;
965
+ }
966
+
967
+ // Serve HTML interface
968
+ if (url.pathname === '/' && req.method === 'GET') {
969
+ res.writeHead(200, { 'Content-Type': 'text/html' });
970
+ res.end(`
971
+ <!DOCTYPE html>
972
+ <html lang="en">
973
+ <head>
974
+ <meta charset="UTF-8">
975
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
976
+ <title>${STORY_CONFIG.genre.charAt(0).toUpperCase() + STORY_CONFIG.genre.slice(1)} Story Game</title>
977
+ <style>
978
+ * { margin: 0; padding: 0; box-sizing: border-box; }
979
+ body {
980
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
981
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
982
+ min-height: 100vh;
983
+ padding: 20px;
984
+ color: #333;
985
+ }
986
+ .container {
987
+ max-width: 900px;
988
+ margin: 0 auto;
989
+ background: white;
990
+ border-radius: 20px;
991
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
992
+ overflow: hidden;
993
+ }
994
+ .header {
995
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
996
+ color: white;
997
+ padding: 30px;
998
+ text-align: center;
999
+ }
1000
+ .header h1 { font-size: 2.5em; margin-bottom: 10px; }
1001
+ .header .config { font-size: 0.9em; opacity: 0.9; margin-top: 10px; }
1002
+ .content {
1003
+ padding: 30px;
1004
+ }
1005
+ .story-text {
1006
+ background: #f8f9fa;
1007
+ padding: 25px;
1008
+ border-radius: 15px;
1009
+ margin-bottom: 25px;
1010
+ line-height: 1.8;
1011
+ font-size: 1.1em;
1012
+ white-space: pre-wrap;
1013
+ border-left: 5px solid #667eea;
1014
+ }
1015
+ .choices {
1016
+ display: flex;
1017
+ flex-direction: column;
1018
+ gap: 15px;
1019
+ }
1020
+ .choice-btn {
1021
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1022
+ color: white;
1023
+ border: none;
1024
+ padding: 20px;
1025
+ border-radius: 12px;
1026
+ font-size: 1.1em;
1027
+ cursor: pointer;
1028
+ transition: all 0.3s ease;
1029
+ text-align: left;
1030
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
1031
+ }
1032
+ .choice-btn:hover {
1033
+ transform: translateY(-2px);
1034
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
1035
+ }
1036
+ .choice-btn:active { transform: translateY(0); }
1037
+ .start-btn {
1038
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
1039
+ color: white;
1040
+ border: none;
1041
+ padding: 20px 40px;
1042
+ border-radius: 12px;
1043
+ font-size: 1.3em;
1044
+ cursor: pointer;
1045
+ transition: all 0.3s ease;
1046
+ box-shadow: 0 4px 15px rgba(245, 87, 108, 0.4);
1047
+ }
1048
+ .start-btn:hover {
1049
+ transform: scale(1.05);
1050
+ box-shadow: 0 6px 20px rgba(245, 87, 108, 0.6);
1051
+ }
1052
+ .quality-metrics {
1053
+ background: #e3f2fd;
1054
+ padding: 15px;
1055
+ border-radius: 10px;
1056
+ margin-bottom: 20px;
1057
+ font-size: 0.9em;
1058
+ border-left: 4px solid #2196f3;
1059
+ }
1060
+ .quality-metrics strong { color: #1976d2; }
1061
+ .loading {
1062
+ text-align: center;
1063
+ padding: 40px;
1064
+ font-size: 1.2em;
1065
+ color: #667eea;
1066
+ }
1067
+ .game-complete {
1068
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
1069
+ color: white;
1070
+ padding: 30px;
1071
+ border-radius: 15px;
1072
+ text-align: center;
1073
+ font-size: 1.3em;
1074
+ margin-top: 20px;
1075
+ }
1076
+ </style>
1077
+ </head>
1078
+ <body>
1079
+ <div class="container">
1080
+ <div class="header">
1081
+ <h1>šŸŽ­ ${STORY_CONFIG.genre.charAt(0).toUpperCase() + STORY_CONFIG.genre.slice(1)} Story Game</h1>
1082
+ <div class="config">
1083
+ <div><strong>Theme:</strong> ${STORY_CONFIG.theme}</div>
1084
+ <div><strong>Tone:</strong> ${STORY_CONFIG.tone}</div>
1085
+ ${STORY_CONFIG.lengthControl?.enabled ? '<div>⚔ Dynamic Length Control: ENABLED</div>' : ''}
1086
+ </div>
1087
+ </div>
1088
+ <div class="content" id="game-content">
1089
+ <div style="text-align: center; padding: 40px;">
1090
+ <button class="start-btn" onclick="startGame()">šŸš€ Start Adventure</button>
1091
+ </div>
1092
+ </div>
1093
+ </div>
1094
+
1095
+ <script>
1096
+ let sessionId = null;
1097
+
1098
+ async function startGame() {
1099
+ document.getElementById('game-content').innerHTML = '<div class="loading">šŸŽ² Creating your story...</div>';
1100
+
1101
+ const response = await fetch('/api/start', { method: 'POST' });
1102
+ const data = await response.json();
1103
+ sessionId = data.sessionId;
1104
+
1105
+ displayGameState(data.state);
1106
+ }
1107
+
1108
+ async function makeChoice(index) {
1109
+ document.getElementById('game-content').innerHTML = '<div class="loading">šŸ“– Writing next chapter...</div>';
1110
+
1111
+ const response = await fetch('/api/choice', {
1112
+ method: 'POST',
1113
+ headers: { 'Content-Type': 'application/json' },
1114
+ body: JSON.stringify({ sessionId, choiceIndex: index })
1115
+ });
1116
+ const data = await response.json();
1117
+
1118
+ displayGameState(data.state);
1119
+ }
1120
+
1121
+ function displayGameState(state) {
1122
+ let html = '';
1123
+
1124
+ if (state.qualityInfo && ${STORY_CONFIG.lengthControl?.showDebug}) {
1125
+ html += \`<div class="quality-metrics">
1126
+ <strong>šŸ“Š Story Quality:</strong> \${(state.qualityInfo.quality * 100).toFixed(1)}% |
1127
+ <strong>Rating:</strong> \${state.qualityInfo.rating} |
1128
+ <strong>Scene:</strong> \${state.qualityInfo.sceneNumber}
1129
+ </div>\`;
1130
+ }
1131
+
1132
+ html += \`<div class="story-text">\${state.text}</div>\`;
1133
+
1134
+ if (state.gameOver) {
1135
+ html += \`<div class="game-complete">
1136
+ šŸ STORY COMPLETE!<br><br>
1137
+ <button class="start-btn" onclick="startGame()">šŸ”„ Play Again</button>
1138
+ </div>\`;
1139
+ } else {
1140
+ html += '<div class="choices">';
1141
+ state.choices.forEach((choice, index) => {
1142
+ html += \`<button class="choice-btn" onclick="makeChoice(\${index})">\${choice}</button>\`;
1143
+ });
1144
+ html += '</div>';
1145
+ }
1146
+
1147
+ document.getElementById('game-content').innerHTML = html;
1148
+ }
1149
+ </script>
1150
+ </body>
1151
+ </html>
1152
+ `);
1153
+ return;
1154
+ }
1155
+
1156
+ // API: Start new game
1157
+ if (url.pathname === '/api/start' && req.method === 'POST') {
1158
+ const sessionId = generateSessionId();
1159
+ const game = reflexive.createChat('story-game');
1160
+ const simulator = new AIStorySimulator(game, STORY_CONFIG);
1161
+
1162
+ await simulator.initializeStory();
1163
+ sessions.set(sessionId, simulator);
1164
+
1165
+ const state = simulator.getCurrentState();
1166
+ const qualityInfo = STORY_CONFIG.lengthControl?.enabled ? {
1167
+ quality: simulator.calculateStoryQuality(),
1168
+ sceneNumber: simulator.sceneNumber,
1169
+ rating: simulator.getQualityRating()
1170
+ } : null;
1171
+
1172
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1173
+ res.end(JSON.stringify({
1174
+ sessionId,
1175
+ state: { ...state, qualityInfo }
1176
+ }));
1177
+ return;
1178
+ }
1179
+
1180
+ // API: Make choice
1181
+ if (url.pathname === '/api/choice' && req.method === 'POST') {
1182
+ let body = '';
1183
+ req.on('data', chunk => body += chunk);
1184
+ req.on('end', async () => {
1185
+ const { sessionId, choiceIndex } = JSON.parse(body);
1186
+ const simulator = sessions.get(sessionId);
1187
+
1188
+ if (!simulator) {
1189
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1190
+ res.end(JSON.stringify({ error: 'Session not found' }));
1191
+ return;
1192
+ }
1193
+
1194
+ await simulator.makeChoice(choiceIndex);
1195
+ const state = simulator.getCurrentState();
1196
+ const qualityInfo = STORY_CONFIG.lengthControl?.enabled ? {
1197
+ quality: simulator.calculateStoryQuality(),
1198
+ sceneNumber: simulator.sceneNumber,
1199
+ rating: simulator.getQualityRating()
1200
+ } : null;
1201
+
1202
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1203
+ res.end(JSON.stringify({
1204
+ state: { ...state, qualityInfo }
1205
+ }));
1206
+ });
1207
+ return;
1208
+ }
1209
+
1210
+ // 404
1211
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
1212
+ res.end('Not Found');
1213
+ });
1214
+
1215
+ server.listen(WEB_PORT, () => {
1216
+ console.log(`🌐 Server running at http://localhost:${WEB_PORT}`);
1217
+ console.log(`šŸ’” Reflexive Dashboard: http://localhost:3098/reflexive`);
1218
+ console.log(`\nšŸ“ Configuration:`);
1219
+ console.log(` Genre: ${STORY_CONFIG.genre}`);
1220
+ console.log(` Theme: ${STORY_CONFIG.theme}`);
1221
+ if (STORY_CONFIG.lengthControl?.enabled) {
1222
+ console.log(`\n⚔ Dynamic Length Control: ENABLED`);
1223
+ }
1224
+ console.log(`\nšŸŽ® Open http://localhost:${WEB_PORT} in your browser to play!\n`);
1225
+ });
1226
+
1227
+ } else {
1228
+ // CLI Mode - Original interactive gameplay
1229
+ const emoji = STORY_CONFIG.genre === 'horror' ? 'šŸ•Æļø' : STORY_CONFIG.genre === 'comedy adventure' ? 'šŸ˜Ž' : 'šŸš€';
1230
+ console.log(`${emoji} Starting Reflexive ${STORY_CONFIG.genre.charAt(0).toUpperCase() + STORY_CONFIG.genre.slice(1)} Game...\n`);
1231
+ console.log('šŸ’” Dashboard: http://localhost:3098/reflexive\n');
1232
+ console.log(`šŸ“ Current Configuration:`);
1233
+ console.log(` Genre: ${STORY_CONFIG.genre}`);
1234
+ console.log(` Theme: ${STORY_CONFIG.theme}`);
1235
+ console.log(` Tone: ${STORY_CONFIG.tone}`);
1236
+ if (STORY_CONFIG.lengthControl?.enabled) {
1237
+ console.log(`\n⚔ Dynamic Length Control: ENABLED`);
1238
+ console.log(` The AI will subtly adjust story length based on quality:`);
1239
+ console.log(` • Excellent stories → Extended narrative (${STORY_CONFIG.lengthControl.targetLengths.excellent})`);
1240
+ console.log(` • Good stories → Normal pacing (${STORY_CONFIG.lengthControl.targetLengths.good})`);
1241
+ console.log(` • Weak stories → Graceful wrap-up (${STORY_CONFIG.lengthControl.targetLengths.poor})`);
1242
+ }
1243
+ console.log();
1244
+
1245
+ playGame().catch(console.error);
1246
+ }