qnce-engine 1.2.0 → 1.2.2

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.
Files changed (125) hide show
  1. package/README.md +713 -7
  2. package/dist/cli/audit.js +0 -0
  3. package/dist/cli/init.js +0 -0
  4. package/dist/cli/perf.d.ts.map +1 -1
  5. package/dist/cli/perf.js +2 -1
  6. package/dist/cli/perf.js.map +1 -1
  7. package/dist/cli/play.d.ts +4 -0
  8. package/dist/cli/play.d.ts.map +1 -0
  9. package/dist/cli/play.js +259 -0
  10. package/dist/cli/play.js.map +1 -0
  11. package/dist/engine/condition.d.ts +69 -0
  12. package/dist/engine/condition.d.ts.map +1 -0
  13. package/dist/engine/condition.js +195 -0
  14. package/dist/engine/condition.js.map +1 -0
  15. package/dist/engine/core.d.ts +274 -3
  16. package/dist/engine/core.d.ts.map +1 -1
  17. package/dist/engine/core.js +1148 -9
  18. package/dist/engine/core.js.map +1 -1
  19. package/dist/engine/demo-story.d.ts.map +1 -1
  20. package/dist/engine/demo-story.js +99 -13
  21. package/dist/engine/demo-story.js.map +1 -1
  22. package/dist/engine/errors.d.ts +76 -0
  23. package/dist/engine/errors.d.ts.map +1 -0
  24. package/dist/engine/errors.js +178 -0
  25. package/dist/engine/errors.js.map +1 -0
  26. package/dist/engine/types.d.ts +445 -0
  27. package/dist/engine/types.d.ts.map +1 -0
  28. package/dist/engine/types.js +9 -0
  29. package/dist/engine/types.js.map +1 -0
  30. package/dist/engine/validation.d.ts +110 -0
  31. package/dist/engine/validation.d.ts.map +1 -0
  32. package/dist/engine/validation.js +261 -0
  33. package/dist/engine/validation.js.map +1 -0
  34. package/dist/examples/examples/autosave-undo-demo.js +248 -0
  35. package/dist/examples/examples/persistence-demo.js +63 -0
  36. package/dist/examples/src/engine/condition.js +194 -0
  37. package/dist/examples/src/engine/core.js +1382 -0
  38. package/dist/examples/src/engine/demo-story.js +200 -0
  39. package/dist/examples/src/engine/types.js +8 -0
  40. package/dist/examples/src/index.js +35 -0
  41. package/dist/examples/src/integrations/react.js +322 -0
  42. package/dist/examples/src/narrative/branching/engine-simple.js +348 -0
  43. package/dist/examples/src/narrative/branching/index.js +55 -0
  44. package/dist/examples/src/narrative/branching/models.js +5 -0
  45. package/dist/examples/src/performance/ObjectPool.js +296 -0
  46. package/dist/examples/src/performance/PerfReporter.js +280 -0
  47. package/dist/examples/src/performance/ThreadPool.js +347 -0
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +12 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/integrations/react.d.ts +200 -0
  53. package/dist/integrations/react.d.ts.map +1 -0
  54. package/dist/integrations/react.js +365 -0
  55. package/dist/integrations/react.js.map +1 -0
  56. package/dist/narrative/branching/engine-simple.js +3 -3
  57. package/dist/narrative/branching/engine-simple.js.map +1 -1
  58. package/dist/narrative/branching/engine.d.ts +1 -0
  59. package/dist/narrative/branching/engine.d.ts.map +1 -0
  60. package/dist/narrative/branching/engine.js +2 -0
  61. package/dist/narrative/branching/engine.js.map +1 -0
  62. package/dist/narrative/branching/models.d.ts.map +1 -1
  63. package/dist/performance/HotReloadDelta.d.ts +25 -8
  64. package/dist/performance/HotReloadDelta.d.ts.map +1 -1
  65. package/dist/performance/HotReloadDelta.js +10 -15
  66. package/dist/performance/HotReloadDelta.js.map +1 -1
  67. package/dist/ui/__tests__/AutosaveIndicator.test.d.ts +2 -0
  68. package/dist/ui/__tests__/AutosaveIndicator.test.d.ts.map +1 -0
  69. package/dist/ui/__tests__/AutosaveIndicator.test.js +329 -0
  70. package/dist/ui/__tests__/AutosaveIndicator.test.js.map +1 -0
  71. package/dist/ui/__tests__/UndoRedoControls.test.d.ts +2 -0
  72. package/dist/ui/__tests__/UndoRedoControls.test.d.ts.map +1 -0
  73. package/dist/ui/__tests__/UndoRedoControls.test.js +245 -0
  74. package/dist/ui/__tests__/UndoRedoControls.test.js.map +1 -0
  75. package/dist/ui/__tests__/autosave-simple.test.d.ts +2 -0
  76. package/dist/ui/__tests__/autosave-simple.test.d.ts.map +1 -0
  77. package/dist/ui/__tests__/autosave-simple.test.js +29 -0
  78. package/dist/ui/__tests__/autosave-simple.test.js.map +1 -0
  79. package/dist/ui/__tests__/setup.d.ts +2 -0
  80. package/dist/ui/__tests__/setup.d.ts.map +1 -0
  81. package/dist/ui/__tests__/setup.js +40 -0
  82. package/dist/ui/__tests__/setup.js.map +1 -0
  83. package/dist/ui/__tests__/smoke-test.d.ts +2 -0
  84. package/dist/ui/__tests__/smoke-test.d.ts.map +1 -0
  85. package/dist/ui/__tests__/smoke-test.js +18 -0
  86. package/dist/ui/__tests__/smoke-test.js.map +1 -0
  87. package/dist/ui/__tests__/smoke-test.test.d.ts +2 -0
  88. package/dist/ui/__tests__/smoke-test.test.d.ts.map +1 -0
  89. package/dist/ui/__tests__/smoke-test.test.js +18 -0
  90. package/dist/ui/__tests__/smoke-test.test.js.map +1 -0
  91. package/dist/ui/__tests__/useKeyboardShortcuts.test.d.ts +2 -0
  92. package/dist/ui/__tests__/useKeyboardShortcuts.test.d.ts.map +1 -0
  93. package/dist/ui/__tests__/useKeyboardShortcuts.test.js +374 -0
  94. package/dist/ui/__tests__/useKeyboardShortcuts.test.js.map +1 -0
  95. package/dist/ui/components/AutosaveIndicator.d.ts +18 -0
  96. package/dist/ui/components/AutosaveIndicator.d.ts.map +1 -0
  97. package/dist/ui/components/AutosaveIndicator.js +175 -0
  98. package/dist/ui/components/AutosaveIndicator.js.map +1 -0
  99. package/dist/ui/components/UndoRedoControls.d.ts +16 -0
  100. package/dist/ui/components/UndoRedoControls.d.ts.map +1 -0
  101. package/dist/ui/components/UndoRedoControls.js +144 -0
  102. package/dist/ui/components/UndoRedoControls.js.map +1 -0
  103. package/dist/ui/hooks/useKeyboardShortcuts.d.ts +22 -0
  104. package/dist/ui/hooks/useKeyboardShortcuts.d.ts.map +1 -0
  105. package/dist/ui/hooks/useKeyboardShortcuts.js +162 -0
  106. package/dist/ui/hooks/useKeyboardShortcuts.js.map +1 -0
  107. package/dist/ui/index.d.ts +9 -0
  108. package/dist/ui/index.d.ts.map +1 -0
  109. package/dist/ui/index.js +14 -0
  110. package/dist/ui/index.js.map +1 -0
  111. package/dist/ui/types.d.ts +141 -0
  112. package/dist/ui/types.d.ts.map +1 -0
  113. package/dist/ui/types.js +51 -0
  114. package/dist/ui/types.js.map +1 -0
  115. package/examples/autosave-undo-demo.ts +306 -0
  116. package/examples/branching-demo-simple.ts +0 -0
  117. package/examples/branching-demo.ts +0 -0
  118. package/examples/persistence-demo.ts +84 -0
  119. package/examples/tsconfig.json +13 -0
  120. package/examples/ui-components-demo.tsx +320 -0
  121. package/examples/validation-demo-story.json +177 -0
  122. package/examples/validation-demo.js +163 -0
  123. package/package.json +24 -4
  124. package/docs/branching/PDM.md +0 -443
  125. package/docs/branching/RELEASE-v1.2.0.md +0 -169
@@ -0,0 +1,320 @@
1
+ import React, { useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import {
4
+ createQNCEEngine,
5
+ DEMO_STORY,
6
+ useQNCE,
7
+ UndoRedoControls,
8
+ AutosaveIndicator,
9
+ useKeyboardShortcuts
10
+ } from '../src/index';
11
+
12
+ /**
13
+ * QNCE Engine UI Components Demo
14
+ * Sprint 3.6: Demonstrates autosave indicators, undo/redo controls, and keyboard shortcuts
15
+ *
16
+ * Features showcased:
17
+ * - UndoRedoControls component with different layouts and themes
18
+ * - AutosaveIndicator with various configurations
19
+ * - Keyboard shortcuts integration
20
+ * - Real-time state updates and visual feedback
21
+ * - Accessibility features
22
+ */
23
+
24
+ const QNCEDemo: React.FC = () => {
25
+ const [engine] = useState(() => createQNCEEngine(DEMO_STORY));
26
+ const { currentNode, availableChoices, flags, selectChoice, resetNarrative } = useQNCE(engine);
27
+
28
+ // Enable keyboard shortcuts
29
+ useKeyboardShortcuts(engine, {
30
+ enabled: true,
31
+ bindings: {
32
+ undo: ['ctrl+z', 'cmd+z'],
33
+ redo: ['ctrl+y', 'cmd+y', 'ctrl+shift+z'],
34
+ save: ['ctrl+s', 'cmd+s'],
35
+ reset: ['ctrl+r'] // Enable reset with confirmation
36
+ }
37
+ });
38
+
39
+ const [showAdvanced, setShowAdvanced] = useState(false);
40
+
41
+ const appStyle: React.CSSProperties = {
42
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
43
+ maxWidth: '800px',
44
+ margin: '0 auto',
45
+ padding: '2rem',
46
+ lineHeight: 1.6,
47
+ color: '#333'
48
+ };
49
+
50
+ const headerStyle: React.CSSProperties = {
51
+ textAlign: 'center',
52
+ marginBottom: '2rem',
53
+ padding: '1rem',
54
+ backgroundColor: '#f8f9fa',
55
+ borderRadius: '8px',
56
+ border: '1px solid #e9ecef'
57
+ };
58
+
59
+ const sectionStyle: React.CSSProperties = {
60
+ marginBottom: '2rem',
61
+ padding: '1rem',
62
+ backgroundColor: '#ffffff',
63
+ borderRadius: '8px',
64
+ border: '1px solid #e9ecef',
65
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
66
+ };
67
+
68
+ const narrativeStyle: React.CSSProperties = {
69
+ fontSize: '1.1rem',
70
+ lineHeight: 1.8,
71
+ marginBottom: '1rem',
72
+ padding: '1rem',
73
+ backgroundColor: '#f8f9fa',
74
+ borderLeft: '4px solid #007bff',
75
+ borderRadius: '0 4px 4px 0'
76
+ };
77
+
78
+ const choiceButtonStyle: React.CSSProperties = {
79
+ display: 'block',
80
+ width: '100%',
81
+ padding: '0.75rem 1rem',
82
+ margin: '0.5rem 0',
83
+ fontSize: '1rem',
84
+ backgroundColor: '#007bff',
85
+ color: 'white',
86
+ border: 'none',
87
+ borderRadius: '4px',
88
+ cursor: 'pointer',
89
+ transition: 'background-color 0.2s'
90
+ };
91
+
92
+ const controlsContainerStyle: React.CSSProperties = {
93
+ display: 'flex',
94
+ gap: '1rem',
95
+ alignItems: 'center',
96
+ flexWrap: 'wrap',
97
+ padding: '1rem',
98
+ backgroundColor: '#f8f9fa',
99
+ borderRadius: '8px',
100
+ marginBottom: '1rem'
101
+ };
102
+
103
+ const flagsStyle: React.CSSProperties = {
104
+ fontSize: '0.9rem',
105
+ color: '#6c757d',
106
+ backgroundColor: '#e9ecef',
107
+ padding: '0.5rem',
108
+ borderRadius: '4px',
109
+ fontFamily: 'monospace'
110
+ };
111
+
112
+ return (
113
+ <div style={appStyle}>
114
+ {/* Header */}
115
+ <header style={headerStyle}>
116
+ <h1>🎮 QNCE Engine UI Components Demo</h1>
117
+ <p>Sprint 3.6: Interactive showcase of autosave indicators, undo/redo controls, and keyboard shortcuts</p>
118
+ <p style={{ fontSize: '0.9rem', color: '#6c757d' }}>
119
+ <strong>Keyboard shortcuts:</strong> Ctrl/Cmd+Z (Undo), Ctrl/Cmd+Y (Redo), Ctrl/Cmd+S (Save)
120
+ </p>
121
+ </header>
122
+
123
+ {/* Controls Section */}
124
+ <section style={sectionStyle}>
125
+ <h2>🎛️ Controls & Status</h2>
126
+ <div style={controlsContainerStyle}>
127
+ {/* Autosave Indicator */}
128
+ <AutosaveIndicator
129
+ engine={engine}
130
+ variant="detailed"
131
+ showTimestamp={true}
132
+ autoHideDelay={3000}
133
+ />
134
+
135
+ {/* Undo/Redo Controls - Horizontal Layout */}
136
+ <UndoRedoControls
137
+ engine={engine}
138
+ size="md"
139
+ layout="horizontal"
140
+ showLabels={true}
141
+ onUndo={() => console.log('Undo performed')}
142
+ onRedo={() => console.log('Redo performed')}
143
+ />
144
+
145
+ {/* Reset Button */}
146
+ <button
147
+ onClick={resetNarrative}
148
+ style={{
149
+ padding: '0.5rem 1rem',
150
+ backgroundColor: '#dc3545',
151
+ color: 'white',
152
+ border: 'none',
153
+ borderRadius: '4px',
154
+ cursor: 'pointer'
155
+ }}
156
+ >
157
+ Reset
158
+ </button>
159
+
160
+ {/* Toggle Advanced View */}
161
+ <button
162
+ onClick={() => setShowAdvanced(!showAdvanced)}
163
+ style={{
164
+ padding: '0.5rem 1rem',
165
+ backgroundColor: '#6c757d',
166
+ color: 'white',
167
+ border: 'none',
168
+ borderRadius: '4px',
169
+ cursor: 'pointer'
170
+ }}
171
+ >
172
+ {showAdvanced ? 'Hide' : 'Show'} Advanced
173
+ </button>
174
+ </div>
175
+ </section>
176
+
177
+ {/* Advanced Controls (when enabled) */}
178
+ {showAdvanced && (
179
+ <section style={sectionStyle}>
180
+ <h3>🔧 Advanced Controls</h3>
181
+ <div style={{ display: 'flex', gap: '1rem', alignItems: 'center', flexWrap: 'wrap' }}>
182
+ {/* Vertical Layout Undo/Redo */}
183
+ <div>
184
+ <h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.9rem' }}>Vertical Layout</h4>
185
+ <UndoRedoControls
186
+ engine={engine}
187
+ size="sm"
188
+ layout="vertical"
189
+ showLabels={false}
190
+ />
191
+ </div>
192
+
193
+ {/* Large Size Controls */}
194
+ <div>
195
+ <h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.9rem' }}>Large Size</h4>
196
+ <UndoRedoControls
197
+ engine={engine}
198
+ size="lg"
199
+ layout="horizontal"
200
+ showLabels={true}
201
+ theme={{
202
+ colors: {
203
+ primary: '#28a745',
204
+ background: '#ffffff'
205
+ }
206
+ }}
207
+ />
208
+ </div>
209
+
210
+ {/* Icon-only Autosave */}
211
+ <div>
212
+ <h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.9rem' }}>Icon-only</h4>
213
+ <AutosaveIndicator
214
+ engine={engine}
215
+ variant="icon-only"
216
+ showTimestamp={false}
217
+ />
218
+ </div>
219
+
220
+ {/* Minimal Autosave */}
221
+ <div>
222
+ <h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.9rem' }}>Minimal</h4>
223
+ <AutosaveIndicator
224
+ engine={engine}
225
+ variant="minimal"
226
+ showTimestamp={false}
227
+ theme={{
228
+ colors: {
229
+ success: '#17a2b8',
230
+ warning: '#ffc107'
231
+ }
232
+ }}
233
+ />
234
+ </div>
235
+ </div>
236
+ </section>
237
+ )}
238
+
239
+ {/* Narrative Section */}
240
+ <section style={sectionStyle}>
241
+ <h2>📖 Current Scene</h2>
242
+ <div style={narrativeStyle}>
243
+ {currentNode.text}
244
+ </div>
245
+
246
+ {/* Choices */}
247
+ {availableChoices.length > 0 && (
248
+ <div>
249
+ <h3>Choose your path:</h3>
250
+ {availableChoices.map((choice, index) => (
251
+ <button
252
+ key={index}
253
+ onClick={() => selectChoice(choice)}
254
+ style={choiceButtonStyle}
255
+ onMouseEnter={(e) => {
256
+ e.currentTarget.style.backgroundColor = '#0056b3';
257
+ }}
258
+ onMouseLeave={(e) => {
259
+ e.currentTarget.style.backgroundColor = '#007bff';
260
+ }}
261
+ >
262
+ {choice.text}
263
+ </button>
264
+ ))}
265
+ </div>
266
+ )}
267
+
268
+ {/* End of story message */}
269
+ {availableChoices.length === 0 && (
270
+ <div style={{ textAlign: 'center', padding: '2rem' }}>
271
+ <h3>🎊 The End</h3>
272
+ <p>You've reached the end of this narrative path.</p>
273
+ <button
274
+ onClick={resetNarrative}
275
+ style={{
276
+ ...choiceButtonStyle,
277
+ backgroundColor: '#28a745',
278
+ width: 'auto',
279
+ display: 'inline-block'
280
+ }}
281
+ >
282
+ Start Over
283
+ </button>
284
+ </div>
285
+ )}
286
+ </section>
287
+
288
+ {/* Story State Info */}
289
+ <section style={sectionStyle}>
290
+ <h3>📊 Story State</h3>
291
+ <div style={flagsStyle}>
292
+ <strong>Current Node:</strong> {currentNode.id}<br/>
293
+ <strong>Flags:</strong> {Object.keys(flags).length > 0 ? JSON.stringify(flags, null, 2) : 'None set'}
294
+ </div>
295
+ </section>
296
+
297
+ {/* Instructions */}
298
+ <section style={sectionStyle}>
299
+ <h3>💡 Usage Instructions</h3>
300
+ <ul style={{ lineHeight: 1.8 }}>
301
+ <li><strong>Make choices</strong> to progress through the story and see autosave indicators</li>
302
+ <li><strong>Use keyboard shortcuts:</strong> Ctrl/Cmd+Z to undo, Ctrl/Cmd+Y to redo, Ctrl/Cmd+S to save</li>
303
+ <li><strong>Watch the autosave indicator</strong> show status changes in real-time</li>
304
+ <li><strong>Try the undo/redo buttons</strong> to navigate through your choice history</li>
305
+ <li><strong>Toggle advanced view</strong> to see different component variations</li>
306
+ <li><strong>All components are accessible</strong> with keyboard navigation and screen readers</li>
307
+ </ul>
308
+ </section>
309
+ </div>
310
+ );
311
+ };
312
+
313
+ // Initialize the demo
314
+ const container = document.getElementById('root');
315
+ if (container) {
316
+ const root = createRoot(container);
317
+ root.render(<QNCEDemo />);
318
+ } else {
319
+ console.error('Root container not found');
320
+ }
@@ -0,0 +1,177 @@
1
+ {
2
+ "initialNodeId": "entrance",
3
+ "nodes": [
4
+ {
5
+ "id": "entrance",
6
+ "text": "You stand before an ancient castle. The heavy oak doors are locked, and storm clouds gather overhead.",
7
+ "choices": [
8
+ {
9
+ "text": "Try to force the door open",
10
+ "nextNodeId": "door_attempt"
11
+ },
12
+ {
13
+ "text": "Use the golden key",
14
+ "nextNodeId": "unlock_door",
15
+ "flagRequirements": {
16
+ "hasGoldenKey": true
17
+ }
18
+ },
19
+ {
20
+ "text": "Wait outside in the storm",
21
+ "nextNodeId": "storm_wait",
22
+ "enabled": false
23
+ },
24
+ {
25
+ "text": "Purchase a lockpick from the merchant",
26
+ "nextNodeId": "buy_lockpick",
27
+ "inventoryRequirements": {
28
+ "gold": 25,
29
+ "reputation": 5
30
+ }
31
+ },
32
+ {
33
+ "text": "Visit the tavern (opens at sunset)",
34
+ "nextNodeId": "tavern",
35
+ "timeRequirements": {
36
+ "availableAfter": "2025-01-01T18:00:00.000Z",
37
+ "availableBefore": "2025-01-01T23:59:59.000Z"
38
+ }
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ "id": "door_attempt",
44
+ "text": "You push against the heavy doors, but they remain firmly shut. Your shoulder aches from the effort.",
45
+ "choices": [
46
+ {
47
+ "text": "Return to the entrance",
48
+ "nextNodeId": "entrance"
49
+ }
50
+ ]
51
+ },
52
+ {
53
+ "id": "unlock_door",
54
+ "text": "The golden key turns smoothly in the lock. The doors swing open with a satisfying creak, revealing a grand hallway beyond.",
55
+ "choices": [
56
+ {
57
+ "text": "Enter the castle",
58
+ "nextNodeId": "grand_hall",
59
+ "flagEffects": {
60
+ "enteredCastle": true
61
+ }
62
+ }
63
+ ]
64
+ },
65
+ {
66
+ "id": "storm_wait",
67
+ "text": "You decide to wait outside, but this choice should never be reachable due to validation.",
68
+ "choices": []
69
+ },
70
+ {
71
+ "id": "buy_lockpick",
72
+ "text": "The merchant smiles as you purchase a masterfully crafted lockpick. 'This will open any door,' he promises.",
73
+ "choices": [
74
+ {
75
+ "text": "Return to the entrance with lockpick",
76
+ "nextNodeId": "entrance",
77
+ "flagEffects": {
78
+ "hasLockpick": true
79
+ }
80
+ }
81
+ ]
82
+ },
83
+ {
84
+ "id": "tavern",
85
+ "text": "The warm tavern glows with candlelight. Travelers share stories and the aroma of roasted meat fills the air.",
86
+ "choices": [
87
+ {
88
+ "text": "Listen to the bard's tale",
89
+ "nextNodeId": "bard_story"
90
+ },
91
+ {
92
+ "text": "Ask about the castle",
93
+ "nextNodeId": "castle_info"
94
+ }
95
+ ]
96
+ },
97
+ {
98
+ "id": "grand_hall",
99
+ "text": "You stand in an opulent grand hall with marble columns and tapestries depicting ancient battles.",
100
+ "choices": [
101
+ {
102
+ "text": "Explore the east wing",
103
+ "nextNodeId": "east_wing"
104
+ },
105
+ {
106
+ "text": "Climb the grand staircase",
107
+ "nextNodeId": "upper_floor"
108
+ }
109
+ ]
110
+ },
111
+ {
112
+ "id": "bard_story",
113
+ "text": "The bard tells of a legendary treasure hidden deep within the castle, guarded by ancient magic.",
114
+ "choices": [
115
+ {
116
+ "text": "Ask about the treasure",
117
+ "nextNodeId": "treasure_info",
118
+ "flagEffects": {
119
+ "knowsAboutTreasure": true
120
+ }
121
+ }
122
+ ]
123
+ },
124
+ {
125
+ "id": "castle_info",
126
+ "text": "A grizzled traveler warns you about the castle's dangers but mentions a golden key hidden in the forest.",
127
+ "choices": [
128
+ {
129
+ "text": "Ask where to find the key",
130
+ "nextNodeId": "key_location",
131
+ "flagEffects": {
132
+ "knowsKeyLocation": true
133
+ }
134
+ }
135
+ ]
136
+ },
137
+ {
138
+ "id": "treasure_info",
139
+ "text": "The bard leans closer and whispers the secret of the treasure's location...",
140
+ "choices": []
141
+ },
142
+ {
143
+ "id": "key_location",
144
+ "text": "The traveler draws a rough map on a napkin, marking where the golden key lies buried.",
145
+ "choices": [
146
+ {
147
+ "text": "Search for the key",
148
+ "nextNodeId": "find_key",
149
+ "flagEffects": {
150
+ "hasGoldenKey": true,
151
+ "foundKey": true
152
+ }
153
+ }
154
+ ]
155
+ },
156
+ {
157
+ "id": "find_key",
158
+ "text": "After searching through the undergrowth, your fingers close around a cold, golden key. Success!",
159
+ "choices": [
160
+ {
161
+ "text": "Return to the castle",
162
+ "nextNodeId": "entrance"
163
+ }
164
+ ]
165
+ },
166
+ {
167
+ "id": "east_wing",
168
+ "text": "The east wing is filled with ancient artifacts and dusty paintings.",
169
+ "choices": []
170
+ },
171
+ {
172
+ "id": "upper_floor",
173
+ "text": "The upper floor overlooks the grand hall. Moonlight streams through stained glass windows.",
174
+ "choices": []
175
+ }
176
+ ]
177
+ }
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+
3
+ // QNCE Choice Validation Demo
4
+ // Demonstrates the choice validation system with various error scenarios
5
+
6
+ import {
7
+ createQNCEEngine,
8
+ ChoiceValidationError,
9
+ isChoiceValidationError,
10
+ createChoiceValidator,
11
+ StandardValidationRules
12
+ } from '../src/engine/core.js';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ // Load the validation demo story
20
+ const storyPath = path.join(__dirname, 'validation-demo-story.json');
21
+ const storyData = JSON.parse(fs.readFileSync(storyPath, 'utf8'));
22
+
23
+ console.log('🛡️ QNCE Choice Validation Demo');
24
+ console.log('=====================================\n');
25
+
26
+ // Create engine with demo story
27
+ const engine = createQNCEEngine(storyData);
28
+
29
+ function displayCurrentState() {
30
+ const currentNode = engine.getCurrentNode();
31
+ const availableChoices = engine.getAvailableChoices();
32
+
33
+ console.log(`\n📍 Current Location: ${currentNode.id}`);
34
+ console.log(`📝 ${currentNode.text}\n`);
35
+
36
+ console.log('✅ Available Choices:');
37
+ availableChoices.forEach((choice, index) => {
38
+ console.log(` ${index + 1}. ${choice.text}`);
39
+ });
40
+
41
+ if (availableChoices.length === 0) {
42
+ console.log(' (No choices available - story complete)');
43
+ }
44
+
45
+ // Show current state
46
+ const state = engine.getState();
47
+ console.log(`\n🏷️ Current Flags: ${JSON.stringify(state.flags, null, 2)}`);
48
+ }
49
+
50
+ function demonstrateValidationError(choiceIndex, errorType) {
51
+ console.log(`\n❌ Testing ${errorType}...`);
52
+ try {
53
+ engine.makeChoice(choiceIndex);
54
+ console.log('✅ Choice executed successfully (unexpected!)');
55
+ } catch (error) {
56
+ if (isChoiceValidationError(error)) {
57
+ console.log(`🚫 ChoiceValidationError caught: ${error.message}`);
58
+ console.log(`📋 Failed conditions: ${error.validationResult.failedConditions?.join(', ') || 'none'}`);
59
+
60
+ const userMessage = error.getUserFriendlyMessage();
61
+ console.log(`👤 User-friendly message:\n${userMessage}`);
62
+
63
+ const debugInfo = error.getDebugInfo();
64
+ console.log(`🔧 Debug info: ${JSON.stringify(debugInfo, null, 2)}`);
65
+ } else {
66
+ console.log(`❗ Other error: ${error.message}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ async function runDemo() {
72
+ try {
73
+ // Display initial state
74
+ displayCurrentState();
75
+
76
+ // Test 1: Try a valid choice first
77
+ console.log('\n🧪 Test 1: Valid Choice');
78
+ console.log('Attempting choice 1 (Try to force the door open)...');
79
+ engine.makeChoice(0);
80
+ displayCurrentState();
81
+
82
+ // Reset to entrance
83
+ engine.goToNodeById('entrance');
84
+ displayCurrentState();
85
+
86
+ // Test 2: Flag requirement failure
87
+ console.log('\n🧪 Test 2: Flag Requirement Failure');
88
+ demonstrateValidationError(1, 'missing flag requirement (hasGoldenKey)');
89
+
90
+ // Test 3: Disabled choice
91
+ console.log('\n🧪 Test 3: Disabled Choice');
92
+ demonstrateValidationError(2, 'disabled choice');
93
+
94
+ // Test 4: Inventory requirement failure
95
+ console.log('\n🧪 Test 4: Inventory Requirement Failure');
96
+ demonstrateValidationError(3, 'insufficient inventory (gold and reputation)');
97
+
98
+ // Test 5: Time requirement failure
99
+ console.log('\n🧪 Test 5: Time Requirement Failure');
100
+ demonstrateValidationError(4, 'time condition not met');
101
+
102
+ // Test 6: Out of bounds choice
103
+ console.log('\n🧪 Test 6: Out of Bounds Choice');
104
+ try {
105
+ engine.makeChoice(99);
106
+ } catch (error) {
107
+ console.log(`🚫 Navigation error: ${error.message}`);
108
+ }
109
+
110
+ // Test 7: Add custom validation rule
111
+ console.log('\n🧪 Test 7: Custom Validation Rule');
112
+ const validator = engine.getChoiceValidator();
113
+ validator.addRule({
114
+ name: 'weather-safety',
115
+ priority: 10,
116
+ validate: (choice, context) => {
117
+ if (choice.text.includes('outside') && !context.state.flags.hasUmbrella) {
118
+ return {
119
+ isValid: false,
120
+ reason: 'Too dangerous to go outside without an umbrella!',
121
+ failedConditions: ['no-umbrella'],
122
+ suggestedChoices: context.currentNode.choices.filter(c => !c.text.includes('outside'))
123
+ };
124
+ }
125
+ return { isValid: true };
126
+ }
127
+ });
128
+
129
+ // This should now fail our custom rule
130
+ console.log('Added custom weather-safety validation rule...');
131
+ demonstrateValidationError(2, 'custom weather rule failure');
132
+
133
+ // Test 8: Show successful path with proper flags
134
+ console.log('\n🧪 Test 8: Successful Path with Flags');
135
+
136
+ // Set up proper flags for success
137
+ const state = engine.getState();
138
+ state.flags.hasGoldenKey = true;
139
+ state.flags.inventory = { gold: 100, reputation: 10 };
140
+
141
+ console.log('Set hasGoldenKey=true and sufficient inventory...');
142
+ displayCurrentState();
143
+
144
+ console.log('Now attempting choice 2 (Use the golden key)...');
145
+ engine.makeChoice(1); // This should work now
146
+ displayCurrentState();
147
+
148
+ console.log('\n✅ Demo completed successfully!');
149
+ console.log('\nKey takeaways:');
150
+ console.log('- ChoiceValidationError provides rich error information');
151
+ console.log('- Multiple validation rules can be applied in priority order');
152
+ console.log('- Custom validation rules can be added for game-specific logic');
153
+ console.log('- Flag, inventory, time, and enabled state validation work seamlessly');
154
+ console.log('- Error messages are both user-friendly and developer-friendly');
155
+
156
+ } catch (error) {
157
+ console.error('Demo failed:', error);
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ // Run the demo
163
+ runDemo();