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
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
// Web-based AI Story Game with Reflexive
|
|
2
|
+
// Same dynamic engine, but playable in a browser
|
|
3
|
+
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import { makeReflexive } from './src/reflexive.js';
|
|
6
|
+
|
|
7
|
+
// Story Configuration - CHANGE THIS TO CHANGE THE ENTIRE GAME
|
|
8
|
+
const STORY_CONFIG = {
|
|
9
|
+
genre: 'comedy adventure',
|
|
10
|
+
theme: 'a hapless lounge lizard trying to find love and fortune in the big city',
|
|
11
|
+
tone: 'humorous, playful, cheeky with clever wordplay and puns',
|
|
12
|
+
craftingGuidelines: [
|
|
13
|
+
'Use witty observations and self-deprecating humor',
|
|
14
|
+
'Include double entendres and playful innuendo (keep it PG-13)',
|
|
15
|
+
'Make situations comedically awkward and embarrassing',
|
|
16
|
+
'Add unexpected punchlines and comedic timing to descriptions'
|
|
17
|
+
],
|
|
18
|
+
lengthControl: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
showDebug: true,
|
|
21
|
+
qualityThresholds: {
|
|
22
|
+
excellent: 0.8,
|
|
23
|
+
good: 0.5,
|
|
24
|
+
poor: 0.3
|
|
25
|
+
},
|
|
26
|
+
targetLengths: {
|
|
27
|
+
excellent: '12-15 scenes',
|
|
28
|
+
good: '6-8 scenes',
|
|
29
|
+
poor: '3-4 scenes'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Initialize Reflexive
|
|
35
|
+
const reflexive = makeReflexive({
|
|
36
|
+
port: 3097, // Different port from CLI version
|
|
37
|
+
title: `Reflexive ${STORY_CONFIG.genre.charAt(0).toUpperCase() + STORY_CONFIG.genre.slice(1)} Game (Web)`,
|
|
38
|
+
systemPrompt: `You are a creative storytelling AI for an interactive ${STORY_CONFIG.genre} game.
|
|
39
|
+
|
|
40
|
+
Theme: ${STORY_CONFIG.theme}
|
|
41
|
+
Tone: ${STORY_CONFIG.tone}
|
|
42
|
+
|
|
43
|
+
Crafting Guidelines:
|
|
44
|
+
${STORY_CONFIG.craftingGuidelines.map((g, i) => `${i + 1}. ${g}`).join('\n')}
|
|
45
|
+
|
|
46
|
+
Your role is to:
|
|
47
|
+
1. Generate atmospheric descriptions that enhance the narrative and match the tone
|
|
48
|
+
2. Create dynamic consequences based on player choices that deepen the experience
|
|
49
|
+
3. Suggest creative new choice options that fit the theme and genre
|
|
50
|
+
4. Evolve the story based on player's history and established narrative arcs
|
|
51
|
+
5. Maintain consistency with the emotional tone and thematic elements
|
|
52
|
+
|
|
53
|
+
Always be concise (1-3 sentences) and match the specified tone perfectly.`
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Import the story classes (same as CLI version)
|
|
57
|
+
class StoryGameBuilder {
|
|
58
|
+
constructor() {
|
|
59
|
+
this.nodes = new Map();
|
|
60
|
+
this.arcs = new Map();
|
|
61
|
+
this.globalState = {};
|
|
62
|
+
this.metadata = {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setMetadata(data) {
|
|
66
|
+
this.metadata = { ...this.metadata, ...data };
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addNode(id, data) {
|
|
71
|
+
this.nodes.set(id, {
|
|
72
|
+
id,
|
|
73
|
+
text: data.text || '',
|
|
74
|
+
choices: data.choices || [],
|
|
75
|
+
onEnter: data.onEnter || null,
|
|
76
|
+
onExit: data.onExit || null,
|
|
77
|
+
arcTags: data.arcTags || [],
|
|
78
|
+
...data
|
|
79
|
+
});
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addArc(id, data) {
|
|
84
|
+
this.arcs.set(id, {
|
|
85
|
+
id,
|
|
86
|
+
name: data.name || id,
|
|
87
|
+
weight: data.weight || 1.0,
|
|
88
|
+
description: data.description || '',
|
|
89
|
+
...data
|
|
90
|
+
});
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
addChoice(nodeId, choice) {
|
|
95
|
+
const node = this.nodes.get(nodeId);
|
|
96
|
+
if (!node) throw new Error(`Node ${nodeId} not found`);
|
|
97
|
+
|
|
98
|
+
node.choices.push({
|
|
99
|
+
text: choice.text,
|
|
100
|
+
target: choice.target,
|
|
101
|
+
weight: choice.weight || 1.0,
|
|
102
|
+
weightFormula: choice.weightFormula || null,
|
|
103
|
+
condition: choice.condition || null,
|
|
104
|
+
effects: choice.effects || {},
|
|
105
|
+
arcModifiers: choice.arcModifiers || {}
|
|
106
|
+
});
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getGame() {
|
|
111
|
+
return {
|
|
112
|
+
nodes: Array.from(this.nodes.values()),
|
|
113
|
+
arcs: Array.from(this.arcs.values()),
|
|
114
|
+
globalState: this.globalState,
|
|
115
|
+
metadata: this.metadata
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class AIStorySimulator {
|
|
121
|
+
constructor(game, storyConfig) {
|
|
122
|
+
this.nodes = new Map(game.nodes.map(n => [n.id, n]));
|
|
123
|
+
this.arcs = new Map(game.arcs.map(a => [a.id, a]));
|
|
124
|
+
this.state = { ...game.globalState };
|
|
125
|
+
this.metadata = game.metadata || {};
|
|
126
|
+
this.storyConfig = storyConfig;
|
|
127
|
+
this.currentNode = null;
|
|
128
|
+
this.history = [];
|
|
129
|
+
this.arcWeights = new Map();
|
|
130
|
+
this.storyMemory = [];
|
|
131
|
+
|
|
132
|
+
this.qualityMetrics = {
|
|
133
|
+
sceneCount: 0,
|
|
134
|
+
engagementScore: 0.5,
|
|
135
|
+
arcProgression: 0,
|
|
136
|
+
lastEnhancementLength: 0
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
this.arcs.forEach((arc, id) => {
|
|
140
|
+
this.arcWeights.set(id, arc.weight);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
reflexive.setState('storyState', this.state);
|
|
144
|
+
reflexive.setState('currentNode', null);
|
|
145
|
+
reflexive.setState('storyConfig', this.storyConfig);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
calculateStoryQuality() {
|
|
149
|
+
const dominantArcWeight = Math.max(...Array.from(this.arcWeights.values()));
|
|
150
|
+
const arcDiversity = this.arcWeights.size > 0 ?
|
|
151
|
+
new Set(Array.from(this.arcWeights.values()).map(v => Math.floor(v))).size / this.arcWeights.size : 0.5;
|
|
152
|
+
|
|
153
|
+
const quality = (
|
|
154
|
+
(dominantArcWeight / 10) * 0.4 +
|
|
155
|
+
this.qualityMetrics.engagementScore * 0.3 +
|
|
156
|
+
arcDiversity * 0.3
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return Math.min(1.0, Math.max(0, quality));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getPacingGuidance() {
|
|
163
|
+
if (!this.storyConfig.lengthControl?.enabled) {
|
|
164
|
+
return 'normal pacing';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const quality = this.calculateStoryQuality();
|
|
168
|
+
const thresholds = this.storyConfig.lengthControl.qualityThresholds;
|
|
169
|
+
const sceneCount = this.qualityMetrics.sceneCount;
|
|
170
|
+
|
|
171
|
+
if (quality >= thresholds.excellent) {
|
|
172
|
+
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}.`;
|
|
173
|
+
} else if (quality >= thresholds.good) {
|
|
174
|
+
return `Story is good (quality: ${quality.toFixed(2)}). Maintain steady pacing with meaningful progression. Target: ${this.storyConfig.lengthControl.targetLengths.good}. Currently at scene ${sceneCount}.`;
|
|
175
|
+
} else {
|
|
176
|
+
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}.`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async getEnhancedNodeText(nodeId) {
|
|
181
|
+
const node = this.nodes.get(nodeId);
|
|
182
|
+
if (!node) return '';
|
|
183
|
+
|
|
184
|
+
this.qualityMetrics.sceneCount++;
|
|
185
|
+
|
|
186
|
+
const recentJourney = this.history.slice(-3).map(h => h.nodeId).join(' ā ');
|
|
187
|
+
const dominantArc = this.getDominantArc();
|
|
188
|
+
const arcDescription = this.arcs.get(dominantArc)?.description || dominantArc;
|
|
189
|
+
const pacingGuidance = this.getPacingGuidance();
|
|
190
|
+
|
|
191
|
+
const prompt = `Scene: "${node.text}"
|
|
192
|
+
Recent journey: ${recentJourney || 'Beginning'}
|
|
193
|
+
Dominant narrative arc: ${arcDescription}
|
|
194
|
+
Player state: ${JSON.stringify(this.state)}
|
|
195
|
+
|
|
196
|
+
PACING GUIDANCE: ${pacingGuidance}
|
|
197
|
+
|
|
198
|
+
Add ONE atmospheric sentence that enhances this scene in the style of ${this.storyConfig.genre}. Match the tone: ${this.storyConfig.tone}.
|
|
199
|
+
${pacingGuidance.includes('Extend') ? 'Make it rich and detailed to build narrative depth.' : ''}
|
|
200
|
+
${pacingGuidance.includes('wrapping up') ? 'Keep it concise and hint at resolution approaching.' : ''}
|
|
201
|
+
Return ONLY the enhancement sentence, nothing else.`;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const enhancement = await reflexive.chat(prompt);
|
|
205
|
+
this.qualityMetrics.lastEnhancementLength = enhancement.length;
|
|
206
|
+
|
|
207
|
+
this.qualityMetrics.engagementScore =
|
|
208
|
+
(this.qualityMetrics.engagementScore * 0.7) +
|
|
209
|
+
(Math.min(1.0, enhancement.length / 150) * 0.3);
|
|
210
|
+
|
|
211
|
+
this.storyMemory.push({ type: 'enhancement', node: nodeId, text: enhancement });
|
|
212
|
+
return `${node.text}\n\n${enhancement.trim()}`;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('AI enhancement failed:', error.message);
|
|
215
|
+
return node.text;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async getChoiceConsequence(choice, choiceIndex) {
|
|
220
|
+
const prompt = `The player chose: "${choice.text}"
|
|
221
|
+
Genre: ${this.storyConfig.genre}
|
|
222
|
+
Current state: ${JSON.stringify(this.state)}
|
|
223
|
+
Arc weights: ${JSON.stringify(Object.fromEntries(this.arcWeights))}
|
|
224
|
+
|
|
225
|
+
Generate a brief (1 sentence) consequence that describes what happens. Match the tone: ${this.storyConfig.tone}.
|
|
226
|
+
Return ONLY the consequence text.`;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const consequence = await reflexive.chat(prompt);
|
|
230
|
+
return consequence.trim();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return "Your choice sets events in motion.";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async generateBonusChoice(nodeId) {
|
|
237
|
+
const node = this.nodes.get(nodeId);
|
|
238
|
+
const dominantArc = this.getDominantArc();
|
|
239
|
+
const arcDescription = this.arcs.get(dominantArc)?.description || dominantArc;
|
|
240
|
+
const pacingGuidance = this.getPacingGuidance();
|
|
241
|
+
const quality = this.calculateStoryQuality();
|
|
242
|
+
|
|
243
|
+
const shouldGenerateBonus = quality > 0.4;
|
|
244
|
+
if (!shouldGenerateBonus) return null;
|
|
245
|
+
|
|
246
|
+
const prompt = `Scene: "${node.text}"
|
|
247
|
+
Genre: ${this.storyConfig.genre}
|
|
248
|
+
Dominant arc: ${arcDescription}
|
|
249
|
+
Recent events: ${this.history.slice(-2).map(h => h.nodeId).join(', ')}
|
|
250
|
+
|
|
251
|
+
PACING GUIDANCE: ${pacingGuidance}
|
|
252
|
+
|
|
253
|
+
Suggest ONE creative choice (4-7 words) that fits this moment and matches the ${this.storyConfig.genre} theme.
|
|
254
|
+
${pacingGuidance.includes('Extend') ? 'Suggest a choice that opens new story possibilities.' : ''}
|
|
255
|
+
${pacingGuidance.includes('wrapping up') ? 'Suggest a choice that moves toward resolution.' : ''}
|
|
256
|
+
Return ONLY the choice text, nothing else.`;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const bonusChoice = await reflexive.chat(prompt);
|
|
260
|
+
return bonusChoice.trim();
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getDominantArc() {
|
|
267
|
+
let maxWeight = 0;
|
|
268
|
+
let dominant = 'neutral';
|
|
269
|
+
|
|
270
|
+
for (const [arc, weight] of this.arcWeights) {
|
|
271
|
+
if (weight > maxWeight) {
|
|
272
|
+
maxWeight = weight;
|
|
273
|
+
dominant = arc;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return dominant;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
start(startNodeId) {
|
|
280
|
+
this.currentNode = startNodeId;
|
|
281
|
+
this.enterNode(startNodeId);
|
|
282
|
+
return this.getCurrentState();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
enterNode(nodeId) {
|
|
286
|
+
const node = this.nodes.get(nodeId);
|
|
287
|
+
if (!node) throw new Error(`Node ${nodeId} not found`);
|
|
288
|
+
|
|
289
|
+
this.history.push({
|
|
290
|
+
nodeId,
|
|
291
|
+
timestamp: Date.now(),
|
|
292
|
+
state: { ...this.state }
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (node.onEnter) {
|
|
296
|
+
node.onEnter(this.state, this);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
node.arcTags.forEach(arcId => {
|
|
300
|
+
const current = this.arcWeights.get(arcId) || 1.0;
|
|
301
|
+
this.arcWeights.set(arcId, current * 1.2);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
reflexive.setState('currentNode', nodeId);
|
|
305
|
+
reflexive.setState('storyState', this.state);
|
|
306
|
+
reflexive.setState('history', this.history.map(h => h.nodeId));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
getAvailableChoices() {
|
|
310
|
+
const node = this.nodes.get(this.currentNode);
|
|
311
|
+
if (!node) return [];
|
|
312
|
+
|
|
313
|
+
return node.choices
|
|
314
|
+
.filter(choice => {
|
|
315
|
+
if (choice.condition) {
|
|
316
|
+
return choice.condition(this.state, this);
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
})
|
|
320
|
+
.map(choice => {
|
|
321
|
+
let weight = choice.weight;
|
|
322
|
+
|
|
323
|
+
if (choice.weightFormula) {
|
|
324
|
+
weight = choice.weightFormula(this.state, this.arcWeights, this);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
Object.keys(choice.arcModifiers).forEach(arcId => {
|
|
328
|
+
const arcWeight = this.arcWeights.get(arcId) || 1.0;
|
|
329
|
+
weight *= arcWeight * choice.arcModifiers[arcId];
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
...choice,
|
|
334
|
+
calculatedWeight: weight
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async makeChoice(choiceIndex) {
|
|
340
|
+
const choices = this.getAvailableChoices();
|
|
341
|
+
if (choiceIndex >= choices.length) {
|
|
342
|
+
throw new Error('Invalid choice index');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const choice = choices[choiceIndex];
|
|
346
|
+
|
|
347
|
+
const consequence = await this.getChoiceConsequence(choice, choiceIndex);
|
|
348
|
+
|
|
349
|
+
const currentNode = this.nodes.get(this.currentNode);
|
|
350
|
+
if (currentNode.onExit) {
|
|
351
|
+
currentNode.onExit(this.state, this);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
Object.assign(this.state, choice.effects);
|
|
355
|
+
|
|
356
|
+
Object.keys(choice.arcModifiers).forEach(arcId => {
|
|
357
|
+
const current = this.arcWeights.get(arcId) || 1.0;
|
|
358
|
+
const modifier = choice.arcModifiers[arcId];
|
|
359
|
+
this.arcWeights.set(arcId, current * modifier);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
this.currentNode = choice.target;
|
|
363
|
+
this.enterNode(choice.target);
|
|
364
|
+
|
|
365
|
+
return { consequence, newState: this.getCurrentState() };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
getCurrentState() {
|
|
369
|
+
const node = this.nodes.get(this.currentNode);
|
|
370
|
+
const choices = this.getAvailableChoices();
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
nodeId: this.currentNode,
|
|
374
|
+
text: node.text,
|
|
375
|
+
choices: choices.map((c, i) => ({
|
|
376
|
+
index: i,
|
|
377
|
+
text: c.text,
|
|
378
|
+
weight: c.calculatedWeight
|
|
379
|
+
})),
|
|
380
|
+
state: { ...this.state },
|
|
381
|
+
arcWeights: Object.fromEntries(this.arcWeights),
|
|
382
|
+
history: this.history.length,
|
|
383
|
+
quality: this.calculateStoryQuality(),
|
|
384
|
+
qualityMetrics: { ...this.qualityMetrics }
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Create story from config (same as CLI version)
|
|
390
|
+
function createStoryFromConfig(config) {
|
|
391
|
+
const builder = new StoryGameBuilder();
|
|
392
|
+
|
|
393
|
+
builder.setMetadata({
|
|
394
|
+
genre: config.genre,
|
|
395
|
+
theme: config.theme,
|
|
396
|
+
tone: config.tone
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
if (config.genre === 'comedy adventure') {
|
|
400
|
+
builder
|
|
401
|
+
.addArc('smooth', {
|
|
402
|
+
name: 'Smooth Operator',
|
|
403
|
+
weight: 1.0,
|
|
404
|
+
description: 'trying to be cool and suave (with mixed results)'
|
|
405
|
+
})
|
|
406
|
+
.addArc('awkward', {
|
|
407
|
+
name: 'Awkward Moments',
|
|
408
|
+
weight: 1.0,
|
|
409
|
+
description: 'stumbling through embarrassing social situations'
|
|
410
|
+
})
|
|
411
|
+
.addArc('lucky', {
|
|
412
|
+
name: 'Dumb Luck',
|
|
413
|
+
weight: 0.5,
|
|
414
|
+
description: 'succeeding through sheer coincidence and fortune'
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
builder
|
|
418
|
+
.addNode('start', {
|
|
419
|
+
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.',
|
|
420
|
+
arcTags: []
|
|
421
|
+
})
|
|
422
|
+
.addNode('smooth_talk', {
|
|
423
|
+
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.',
|
|
424
|
+
arcTags: ['awkward'],
|
|
425
|
+
onEnter: (state) => { state.confidence = (state.confidence || 50) - 10; state.money = (state.money || 100) - 0; }
|
|
426
|
+
})
|
|
427
|
+
.addNode('sneak_in', {
|
|
428
|
+
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.',
|
|
429
|
+
arcTags: ['lucky'],
|
|
430
|
+
onEnter: (state) => { state.confidence = (state.confidence || 50) + 20; }
|
|
431
|
+
})
|
|
432
|
+
.addNode('bar', {
|
|
433
|
+
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."',
|
|
434
|
+
arcTags: []
|
|
435
|
+
})
|
|
436
|
+
.addNode('pickup_line', {
|
|
437
|
+
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.',
|
|
438
|
+
arcTags: ['awkward'],
|
|
439
|
+
onEnter: (state) => { state.charisma = (state.charisma || 0) + 5; state.reputation = 'endearing_disaster'; }
|
|
440
|
+
})
|
|
441
|
+
.addNode('be_genuine', {
|
|
442
|
+
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?"',
|
|
443
|
+
arcTags: ['smooth'],
|
|
444
|
+
onEnter: (state) => { state.connection = true; state.confidence = (state.confidence || 50) + 30; }
|
|
445
|
+
})
|
|
446
|
+
.addNode('awkward_ending', {
|
|
447
|
+
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!',
|
|
448
|
+
arcTags: ['awkward']
|
|
449
|
+
})
|
|
450
|
+
.addNode('smooth_ending', {
|
|
451
|
+
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.',
|
|
452
|
+
arcTags: ['smooth']
|
|
453
|
+
})
|
|
454
|
+
.addNode('lucky_ending', {
|
|
455
|
+
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.',
|
|
456
|
+
arcTags: ['lucky']
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
builder
|
|
460
|
+
.addChoice('start', {
|
|
461
|
+
text: 'Try to smooth talk the bouncer',
|
|
462
|
+
target: 'smooth_talk',
|
|
463
|
+
arcModifiers: { awkward: 1.8 }
|
|
464
|
+
})
|
|
465
|
+
.addChoice('start', {
|
|
466
|
+
text: 'Find another way in',
|
|
467
|
+
target: 'sneak_in',
|
|
468
|
+
arcModifiers: { lucky: 2.0 }
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
builder
|
|
472
|
+
.addChoice('smooth_talk', {
|
|
473
|
+
text: 'Head to the bar',
|
|
474
|
+
target: 'bar'
|
|
475
|
+
})
|
|
476
|
+
.addChoice('sneak_in', {
|
|
477
|
+
text: 'Head to the bar',
|
|
478
|
+
target: 'bar'
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
builder
|
|
482
|
+
.addChoice('bar', {
|
|
483
|
+
text: 'Use your best pickup line',
|
|
484
|
+
target: 'pickup_line',
|
|
485
|
+
arcModifiers: { awkward: 2.5 },
|
|
486
|
+
weightFormula: (state, arcWeights) => (arcWeights.get('awkward') || 1.0)
|
|
487
|
+
})
|
|
488
|
+
.addChoice('bar', {
|
|
489
|
+
text: 'Just be yourself',
|
|
490
|
+
target: 'be_genuine',
|
|
491
|
+
arcModifiers: { smooth: 2.5 },
|
|
492
|
+
weightFormula: (state, arcWeights) => (arcWeights.get('smooth') || 1.0)
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
builder
|
|
496
|
+
.addChoice('pickup_line', {
|
|
497
|
+
text: 'Suggest getting some fresh air',
|
|
498
|
+
target: 'awkward_ending',
|
|
499
|
+
arcModifiers: { awkward: 2.0 }
|
|
500
|
+
})
|
|
501
|
+
.addChoice('pickup_line', {
|
|
502
|
+
text: 'Ask them to teach you to be cool',
|
|
503
|
+
target: 'lucky_ending',
|
|
504
|
+
arcModifiers: { lucky: 3.0 }
|
|
505
|
+
})
|
|
506
|
+
.addChoice('be_genuine', {
|
|
507
|
+
text: 'Leave together',
|
|
508
|
+
target: 'smooth_ending',
|
|
509
|
+
arcModifiers: { smooth: 2.0 }
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return builder.getGame();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Web server logic
|
|
517
|
+
const activeSessions = new Map();
|
|
518
|
+
|
|
519
|
+
function generateSessionId() {
|
|
520
|
+
return Math.random().toString(36).substring(2, 15);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function handleRequest(req, res) {
|
|
524
|
+
// Enable CORS
|
|
525
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
526
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
527
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
528
|
+
|
|
529
|
+
if (req.method === 'OPTIONS') {
|
|
530
|
+
res.writeHead(200);
|
|
531
|
+
res.end();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
536
|
+
|
|
537
|
+
// Serve the HTML game interface
|
|
538
|
+
if (url.pathname === '/' || url.pathname === '/game') {
|
|
539
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
540
|
+
res.end(getHTMLInterface());
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// API: Start new game
|
|
545
|
+
if (url.pathname === '/api/start' && req.method === 'POST') {
|
|
546
|
+
const sessionId = generateSessionId();
|
|
547
|
+
const game = createStoryFromConfig(STORY_CONFIG);
|
|
548
|
+
const simulator = new AIStorySimulator(game, STORY_CONFIG);
|
|
549
|
+
simulator.start('start');
|
|
550
|
+
|
|
551
|
+
const enhancedText = await simulator.getEnhancedNodeText('start');
|
|
552
|
+
const state = simulator.getCurrentState();
|
|
553
|
+
|
|
554
|
+
// Generate bonus choice
|
|
555
|
+
const bonusChoice = Math.random() > 0.7 ? await simulator.generateBonusChoice('start') : null;
|
|
556
|
+
|
|
557
|
+
activeSessions.set(sessionId, simulator);
|
|
558
|
+
|
|
559
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
560
|
+
res.end(JSON.stringify({
|
|
561
|
+
sessionId,
|
|
562
|
+
state,
|
|
563
|
+
enhancedText,
|
|
564
|
+
bonusChoice,
|
|
565
|
+
config: STORY_CONFIG
|
|
566
|
+
}));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// API: Make choice
|
|
571
|
+
if (url.pathname === '/api/choice' && req.method === 'POST') {
|
|
572
|
+
let body = '';
|
|
573
|
+
req.on('data', chunk => { body += chunk.toString(); });
|
|
574
|
+
req.on('end', async () => {
|
|
575
|
+
try {
|
|
576
|
+
const { sessionId, choiceIndex } = JSON.parse(body);
|
|
577
|
+
const simulator = activeSessions.get(sessionId);
|
|
578
|
+
|
|
579
|
+
if (!simulator) {
|
|
580
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
581
|
+
res.end(JSON.stringify({ error: 'Session not found' }));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const result = await simulator.makeChoice(choiceIndex);
|
|
586
|
+
const enhancedText = await simulator.getEnhancedNodeText(result.newState.nodeId);
|
|
587
|
+
const bonusChoice = Math.random() > 0.7 ? await simulator.generateBonusChoice(result.newState.nodeId) : null;
|
|
588
|
+
|
|
589
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
590
|
+
res.end(JSON.stringify({
|
|
591
|
+
...result,
|
|
592
|
+
enhancedText,
|
|
593
|
+
bonusChoice
|
|
594
|
+
}));
|
|
595
|
+
} catch (error) {
|
|
596
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
597
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 404
|
|
604
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
605
|
+
res.end('Not Found');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function getHTMLInterface() {
|
|
609
|
+
const emoji = STORY_CONFIG.genre === 'horror' ? 'šÆļø' : STORY_CONFIG.genre === 'comedy adventure' ? 'š' : 'š®';
|
|
610
|
+
|
|
611
|
+
return `<!DOCTYPE html>
|
|
612
|
+
<html lang="en">
|
|
613
|
+
<head>
|
|
614
|
+
<meta charset="UTF-8">
|
|
615
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
616
|
+
<title>${emoji} Reflexive Story Game</title>
|
|
617
|
+
<style>
|
|
618
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
619
|
+
|
|
620
|
+
body {
|
|
621
|
+
font-family: 'Courier New', monospace;
|
|
622
|
+
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d44 100%);
|
|
623
|
+
color: #e0e0e0;
|
|
624
|
+
line-height: 1.6;
|
|
625
|
+
min-height: 100vh;
|
|
626
|
+
padding: 20px;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.container {
|
|
630
|
+
max-width: 900px;
|
|
631
|
+
margin: 0 auto;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
header {
|
|
635
|
+
text-align: center;
|
|
636
|
+
padding: 30px 0;
|
|
637
|
+
border-bottom: 2px solid #444;
|
|
638
|
+
margin-bottom: 30px;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
h1 {
|
|
642
|
+
font-size: 2.5em;
|
|
643
|
+
color: #ffd700;
|
|
644
|
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
|
645
|
+
margin-bottom: 10px;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.theme {
|
|
649
|
+
color: #aaa;
|
|
650
|
+
font-style: italic;
|
|
651
|
+
font-size: 1.1em;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.info-box {
|
|
655
|
+
background: rgba(255, 255, 255, 0.05);
|
|
656
|
+
border: 1px solid #555;
|
|
657
|
+
border-radius: 8px;
|
|
658
|
+
padding: 15px;
|
|
659
|
+
margin-bottom: 20px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.info-box h3 {
|
|
663
|
+
color: #ffd700;
|
|
664
|
+
margin-bottom: 10px;
|
|
665
|
+
font-size: 1.1em;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.info-box ul {
|
|
669
|
+
list-style: none;
|
|
670
|
+
padding-left: 20px;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.info-box li:before {
|
|
674
|
+
content: "⢠";
|
|
675
|
+
color: #ffd700;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
#game-area {
|
|
679
|
+
background: rgba(0, 0, 0, 0.3);
|
|
680
|
+
border: 2px solid #555;
|
|
681
|
+
border-radius: 12px;
|
|
682
|
+
padding: 30px;
|
|
683
|
+
margin-bottom: 20px;
|
|
684
|
+
min-height: 400px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.scene-header {
|
|
688
|
+
color: #ffd700;
|
|
689
|
+
font-size: 1.3em;
|
|
690
|
+
margin-bottom: 15px;
|
|
691
|
+
padding-bottom: 10px;
|
|
692
|
+
border-bottom: 1px dashed #555;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.scene-text {
|
|
696
|
+
font-size: 1.1em;
|
|
697
|
+
line-height: 1.8;
|
|
698
|
+
margin-bottom: 25px;
|
|
699
|
+
white-space: pre-wrap;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.consequence {
|
|
703
|
+
background: rgba(255, 215, 0, 0.1);
|
|
704
|
+
border-left: 3px solid #ffd700;
|
|
705
|
+
padding: 15px;
|
|
706
|
+
margin-bottom: 20px;
|
|
707
|
+
font-style: italic;
|
|
708
|
+
color: #ffd700;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.choices {
|
|
712
|
+
margin-top: 25px;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.choice-button {
|
|
716
|
+
display: block;
|
|
717
|
+
width: 100%;
|
|
718
|
+
background: linear-gradient(135deg, #3a3a5c 0%, #2d2d44 100%);
|
|
719
|
+
border: 2px solid #555;
|
|
720
|
+
color: #e0e0e0;
|
|
721
|
+
padding: 15px 20px;
|
|
722
|
+
margin-bottom: 12px;
|
|
723
|
+
border-radius: 8px;
|
|
724
|
+
cursor: pointer;
|
|
725
|
+
font-family: 'Courier New', monospace;
|
|
726
|
+
font-size: 1em;
|
|
727
|
+
text-align: left;
|
|
728
|
+
transition: all 0.3s ease;
|
|
729
|
+
position: relative;
|
|
730
|
+
overflow: hidden;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.choice-button:hover {
|
|
734
|
+
background: linear-gradient(135deg, #4a4a6c 0%, #3d3d54 100%);
|
|
735
|
+
border-color: #ffd700;
|
|
736
|
+
transform: translateX(5px);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.choice-button:active {
|
|
740
|
+
transform: scale(0.98);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.choice-button.bonus {
|
|
744
|
+
border-color: #ffd700;
|
|
745
|
+
background: linear-gradient(135deg, #4a4a2c 0%, #3d3d24 100%);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.choice-button .weight-bar {
|
|
749
|
+
position: absolute;
|
|
750
|
+
bottom: 0;
|
|
751
|
+
left: 0;
|
|
752
|
+
height: 3px;
|
|
753
|
+
background: #ffd700;
|
|
754
|
+
transition: width 0.3s ease;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.choice-text {
|
|
758
|
+
display: flex;
|
|
759
|
+
justify-content: space-between;
|
|
760
|
+
align-items: center;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.choice-weight {
|
|
764
|
+
font-size: 0.9em;
|
|
765
|
+
color: #888;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.metrics {
|
|
769
|
+
display: grid;
|
|
770
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
771
|
+
gap: 15px;
|
|
772
|
+
margin-top: 20px;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.metric-card {
|
|
776
|
+
background: rgba(0, 0, 0, 0.3);
|
|
777
|
+
border: 1px solid #555;
|
|
778
|
+
border-radius: 8px;
|
|
779
|
+
padding: 15px;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.metric-card h4 {
|
|
783
|
+
color: #ffd700;
|
|
784
|
+
font-size: 0.9em;
|
|
785
|
+
margin-bottom: 8px;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.metric-value {
|
|
789
|
+
font-size: 1.3em;
|
|
790
|
+
color: #e0e0e0;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.quality-bar {
|
|
794
|
+
width: 100%;
|
|
795
|
+
height: 8px;
|
|
796
|
+
background: rgba(255, 255, 255, 0.1);
|
|
797
|
+
border-radius: 4px;
|
|
798
|
+
overflow: hidden;
|
|
799
|
+
margin-top: 8px;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.quality-fill {
|
|
803
|
+
height: 100%;
|
|
804
|
+
background: linear-gradient(90deg, #ff4444, #ffaa00, #44ff44);
|
|
805
|
+
transition: width 0.5s ease;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.loading {
|
|
809
|
+
text-align: center;
|
|
810
|
+
padding: 40px;
|
|
811
|
+
color: #ffd700;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.spinner {
|
|
815
|
+
border: 4px solid rgba(255, 215, 0, 0.1);
|
|
816
|
+
border-top: 4px solid #ffd700;
|
|
817
|
+
border-radius: 50%;
|
|
818
|
+
width: 40px;
|
|
819
|
+
height: 40px;
|
|
820
|
+
animation: spin 1s linear infinite;
|
|
821
|
+
margin: 20px auto;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
@keyframes spin {
|
|
825
|
+
0% { transform: rotate(0deg); }
|
|
826
|
+
100% { transform: rotate(360deg); }
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.btn-new-game {
|
|
830
|
+
display: inline-block;
|
|
831
|
+
background: #ffd700;
|
|
832
|
+
color: #1e1e2e;
|
|
833
|
+
padding: 12px 30px;
|
|
834
|
+
border-radius: 8px;
|
|
835
|
+
border: none;
|
|
836
|
+
font-family: 'Courier New', monospace;
|
|
837
|
+
font-size: 1.1em;
|
|
838
|
+
cursor: pointer;
|
|
839
|
+
font-weight: bold;
|
|
840
|
+
transition: all 0.3s ease;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.btn-new-game:hover {
|
|
844
|
+
background: #ffed4e;
|
|
845
|
+
transform: scale(1.05);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.ending {
|
|
849
|
+
background: rgba(255, 215, 0, 0.1);
|
|
850
|
+
border: 2px solid #ffd700;
|
|
851
|
+
border-radius: 12px;
|
|
852
|
+
padding: 30px;
|
|
853
|
+
text-align: center;
|
|
854
|
+
margin-top: 20px;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.ending h2 {
|
|
858
|
+
color: #ffd700;
|
|
859
|
+
font-size: 2em;
|
|
860
|
+
margin-bottom: 20px;
|
|
861
|
+
}
|
|
862
|
+
</style>
|
|
863
|
+
</head>
|
|
864
|
+
<body>
|
|
865
|
+
<div class="container">
|
|
866
|
+
<header>
|
|
867
|
+
<h1>${emoji} REFLEXIVE AI STORY GAME ${emoji}</h1>
|
|
868
|
+
<p class="theme">${STORY_CONFIG.theme}</p>
|
|
869
|
+
</header>
|
|
870
|
+
|
|
871
|
+
<div class="info-box" id="length-control-info">
|
|
872
|
+
<h3>ā” Dynamic Length Control</h3>
|
|
873
|
+
<p>The AI adapts story length based on quality:</p>
|
|
874
|
+
<ul>
|
|
875
|
+
<li>Excellent stories ā Extended narrative (${STORY_CONFIG.lengthControl.targetLengths.excellent})</li>
|
|
876
|
+
<li>Good stories ā Normal pacing (${STORY_CONFIG.lengthControl.targetLengths.good})</li>
|
|
877
|
+
<li>Weak stories ā Graceful wrap-up (${STORY_CONFIG.lengthControl.targetLengths.poor})</li>
|
|
878
|
+
</ul>
|
|
879
|
+
</div>
|
|
880
|
+
|
|
881
|
+
<div id="game-area">
|
|
882
|
+
<div class="loading">
|
|
883
|
+
<div class="spinner"></div>
|
|
884
|
+
<p>Starting your adventure...</p>
|
|
885
|
+
</div>
|
|
886
|
+
</div>
|
|
887
|
+
|
|
888
|
+
<div class="metrics" id="metrics" style="display: none;"></div>
|
|
889
|
+
|
|
890
|
+
<div style="text-align: center; margin-top: 20px;">
|
|
891
|
+
<button class="btn-new-game" onclick="startNewGame()">š New Game</button>
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
|
|
895
|
+
<script>
|
|
896
|
+
let sessionId = null;
|
|
897
|
+
let currentState = null;
|
|
898
|
+
|
|
899
|
+
async function startNewGame() {
|
|
900
|
+
document.getElementById('game-area').innerHTML = '<div class="loading"><div class="spinner"></div><p>Starting your adventure...</p></div>';
|
|
901
|
+
document.getElementById('metrics').style.display = 'none';
|
|
902
|
+
|
|
903
|
+
const response = await fetch('/api/start', { method: 'POST' });
|
|
904
|
+
const data = await response.json();
|
|
905
|
+
|
|
906
|
+
sessionId = data.sessionId;
|
|
907
|
+
currentState = data.state;
|
|
908
|
+
|
|
909
|
+
renderScene(data.enhancedText, data.state, data.bonusChoice);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function renderScene(enhancedText, state, bonusChoice, consequence = null) {
|
|
913
|
+
const gameArea = document.getElementById('game-area');
|
|
914
|
+
|
|
915
|
+
let html = \`<div class="scene-header">š \${state.nodeId.toUpperCase().replace(/_/g, ' ')}</div>\`;
|
|
916
|
+
|
|
917
|
+
if (consequence) {
|
|
918
|
+
html += \`<div class="consequence">š« \${consequence}</div>\`;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
html += \`<div class="scene-text">\${enhancedText}</div>\`;
|
|
922
|
+
|
|
923
|
+
if (state.choices && state.choices.length > 0) {
|
|
924
|
+
html += '<div class="choices">';
|
|
925
|
+
state.choices.forEach((choice, i) => {
|
|
926
|
+
const weightPercent = Math.min(100, (choice.weight / 15) * 100);
|
|
927
|
+
html += \`
|
|
928
|
+
<button class="choice-button" onclick="makeChoice(\${i})">
|
|
929
|
+
<div class="choice-text">
|
|
930
|
+
<span>[\${i + 1}] \${choice.text}</span>
|
|
931
|
+
<span class="choice-weight">\${choice.weight.toFixed(2)}</span>
|
|
932
|
+
</div>
|
|
933
|
+
<div class="weight-bar" style="width: \${weightPercent}%"></div>
|
|
934
|
+
</button>
|
|
935
|
+
\`;
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
if (bonusChoice) {
|
|
939
|
+
html += \`
|
|
940
|
+
<button class="choice-button bonus">
|
|
941
|
+
<div class="choice-text">
|
|
942
|
+
<span>[āØ] \${bonusChoice} (AI-generated)</span>
|
|
943
|
+
</div>
|
|
944
|
+
</button>
|
|
945
|
+
\`;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
html += '</div>';
|
|
949
|
+
} else {
|
|
950
|
+
html += \`
|
|
951
|
+
<div class="ending">
|
|
952
|
+
<h2>š STORY COMPLETE!</h2>
|
|
953
|
+
<p>\${enhancedText}</p>
|
|
954
|
+
</div>
|
|
955
|
+
\`;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
gameArea.innerHTML = html;
|
|
959
|
+
|
|
960
|
+
// Update metrics
|
|
961
|
+
if (state.qualityMetrics) {
|
|
962
|
+
renderMetrics(state);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function renderMetrics(state) {
|
|
967
|
+
const metricsDiv = document.getElementById('metrics');
|
|
968
|
+
metricsDiv.style.display = 'grid';
|
|
969
|
+
|
|
970
|
+
const quality = state.quality * 100;
|
|
971
|
+
const engagement = state.qualityMetrics.engagementScore * 100;
|
|
972
|
+
|
|
973
|
+
metricsDiv.innerHTML = \`
|
|
974
|
+
<div class="metric-card">
|
|
975
|
+
<h4>š State</h4>
|
|
976
|
+
<div class="metric-value">\${JSON.stringify(state.state).substring(0, 50)}...</div>
|
|
977
|
+
</div>
|
|
978
|
+
<div class="metric-card">
|
|
979
|
+
<h4>š Quality</h4>
|
|
980
|
+
<div class="metric-value">\${quality.toFixed(1)}%</div>
|
|
981
|
+
<div class="quality-bar">
|
|
982
|
+
<div class="quality-fill" style="width: \${quality}%"></div>
|
|
983
|
+
</div>
|
|
984
|
+
</div>
|
|
985
|
+
<div class="metric-card">
|
|
986
|
+
<h4>šÆ Scene</h4>
|
|
987
|
+
<div class="metric-value">\${state.qualityMetrics.sceneCount}</div>
|
|
988
|
+
</div>
|
|
989
|
+
<div class="metric-card">
|
|
990
|
+
<h4>š« Engagement</h4>
|
|
991
|
+
<div class="metric-value">\${engagement.toFixed(1)}%</div>
|
|
992
|
+
</div>
|
|
993
|
+
\`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
async function makeChoice(choiceIndex) {
|
|
997
|
+
document.getElementById('game-area').innerHTML = '<div class="loading"><div class="spinner"></div><p>Processing your choice...</p></div>';
|
|
998
|
+
|
|
999
|
+
const response = await fetch('/api/choice', {
|
|
1000
|
+
method: 'POST',
|
|
1001
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1002
|
+
body: JSON.stringify({ sessionId, choiceIndex })
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
const data = await response.json();
|
|
1006
|
+
currentState = data.newState;
|
|
1007
|
+
|
|
1008
|
+
renderScene(data.enhancedText, data.newState, data.bonusChoice, data.consequence);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Start game on load
|
|
1012
|
+
window.onload = startNewGame;
|
|
1013
|
+
</script>
|
|
1014
|
+
</body>
|
|
1015
|
+
</html>`;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Start the game server on a different port from Reflexive
|
|
1019
|
+
const GAME_PORT = 3100;
|
|
1020
|
+
const server = http.createServer(handleRequest);
|
|
1021
|
+
|
|
1022
|
+
server.listen(GAME_PORT, () => {
|
|
1023
|
+
console.log(`\n${STORY_CONFIG.genre === 'horror' ? 'šÆļø' : STORY_CONFIG.genre === 'comedy adventure' ? 'š' : 'š®'} Reflexive Story Game - Web Version\n`);
|
|
1024
|
+
console.log(`š Game URL: http://localhost:${GAME_PORT}/game`);
|
|
1025
|
+
console.log(`š Game Root: http://localhost:${GAME_PORT}/`);
|
|
1026
|
+
console.log(`š Reflexive Dashboard: http://localhost:3097/reflexive`);
|
|
1027
|
+
console.log(`\nā” Dynamic Length Control: ${STORY_CONFIG.lengthControl.enabled ? 'ENABLED' : 'DISABLED'}`);
|
|
1028
|
+
console.log(`š Genre: ${STORY_CONFIG.genre}`);
|
|
1029
|
+
console.log(`š Theme: ${STORY_CONFIG.theme}\n`);
|
|
1030
|
+
});
|