qnce-engine
Version:
Core QNCE (Quantum Narrative Convergence Engine) - Framework agnostic narrative engine with performance optimization
405 lines • 16.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.playInteractive = main;
const fs_1 = require("fs");
const readline_1 = require("readline");
const core_js_1 = require("../engine/core.js");
const core_js_2 = require("../telemetry/core.js");
const StorageAdapters_js_1 = require("../persistence/StorageAdapters.js");
const demo_story_js_1 = require("../engine/demo-story.js");
function displayNode(session) {
const { engine } = session;
const currentNode = engine.getCurrentNode();
const choices = engine.getAvailableChoices();
console.log('\n' + '='.repeat(60));
console.log(currentNode.text);
console.log('='.repeat(60));
if (choices.length > 0) {
console.log('\nChoices:');
choices.forEach((choice, index) => {
console.log(`${index + 1}. ${choice.text}`);
});
}
else {
console.log('\n[Story Complete]');
}
// Show undo/redo status
const undoCount = engine.getUndoCount();
const redoCount = engine.getRedoCount();
console.log(`\nUndo: ${undoCount} available | Redo: ${redoCount} available`);
}
function displayHelp() {
console.log('\nCommands:');
console.log(' 1-9 : Select choice by number');
console.log(' u, undo : Undo last action');
console.log(' r, redo : Redo last undone action');
console.log(' h, help : Show this help');
console.log(' s, save : Save current state');
console.log(' l, load : Load saved state');
console.log(' f, flags: Show current flags');
console.log(' hist : Show history summary');
console.log(' q, quit : Exit the session');
}
function displayFlags(session) {
const { engine } = session;
const flags = engine.getState().flags;
console.log('\n--- Current Flags ---');
if (Object.keys(flags).length === 0) {
console.log('No flags set');
}
else {
Object.entries(flags).forEach(([key, value]) => {
console.log(`${key}: ${JSON.stringify(value)}`);
});
}
}
function displayHistory(session) {
const { engine } = session;
const summary = engine.getHistorySummary();
console.log('\n--- History Summary ---');
console.log(`Undo entries: ${summary.undoEntries.length}`);
console.log(`Redo entries: ${summary.redoEntries.length}`);
if (summary.undoEntries.length > 0) {
console.log('\nRecent undo history:');
summary.undoEntries.slice(-5).forEach((entry, index) => {
console.log(` ${index + 1}. ${entry.action || 'Action'} (${entry.timestamp})`);
});
}
}
async function saveState(session) {
const { engine, rl } = session;
return new Promise((resolve) => {
rl.question('Enter filename to save (or press Enter for default): ', async (filename) => {
const saveFile = filename || 'qnce-save.json';
try {
const serializedState = await engine.saveState({
includeFlowEvents: true
});
require('fs').writeFileSync(saveFile, JSON.stringify(serializedState, null, 2));
console.log(`✅ State saved to ${saveFile}`);
}
catch (error) {
console.error('❌ Failed to save state:', error?.message || error);
}
resolve();
});
});
}
async function loadState(session) {
const { engine, rl } = session;
return new Promise((resolve) => {
rl.question('Enter filename to load (or press Enter for default): ', async (filename) => {
const loadFile = filename || 'qnce-save.json';
try {
const data = (0, fs_1.readFileSync)(loadFile, 'utf-8');
const serializedState = JSON.parse(data);
await engine.loadState(serializedState);
console.log(`✅ State loaded from ${loadFile}`);
displayNode(session);
}
catch (error) {
console.error('❌ Failed to load state:', error?.message || error);
}
resolve();
});
});
}
function handleCommand(session, input) {
const { engine } = session;
const choices = engine.getAvailableChoices();
return new Promise((resolve) => {
const command = input.trim().toLowerCase();
// Handle numeric choices
const choiceNumber = parseInt(command);
if (!isNaN(choiceNumber) && choiceNumber >= 1 && choiceNumber <= choices.length) {
const selectedChoice = choices[choiceNumber - 1];
engine.selectChoice(selectedChoice);
displayNode(session);
resolve(false);
return;
}
// Handle text commands
switch (command) {
case 'u':
case 'undo':
const undoResult = engine.undo();
if (undoResult.success) {
console.log(`✅ Undid: ${undoResult.entry?.action || 'action'}`);
displayNode(session);
}
else {
console.log('❌ Nothing to undo');
}
resolve(false);
break;
case 'r':
case 'redo':
const redoResult = engine.redo();
if (redoResult.success) {
console.log(`✅ Redid: ${redoResult.entry?.action || 'action'}`);
displayNode(session);
}
else {
console.log('❌ Nothing to redo');
}
resolve(false);
break;
case 'h':
case 'help':
displayHelp();
resolve(false);
break;
case 'f':
case 'flags':
displayFlags(session);
resolve(false);
break;
case 'hist':
displayHistory(session);
resolve(false);
break;
case 's':
case 'save':
saveState(session).then(() => resolve(false));
break;
case 'l':
case 'load':
loadState(session).then(() => resolve(false));
break;
case 'q':
case 'quit':
console.log('👋 Thanks for playing!');
resolve(true);
break;
default:
console.log('❓ Unknown command. Type "help" for available commands.');
resolve(false);
break;
}
});
}
async function startInteractiveSession(session) {
const { rl } = session;
console.log('🎮 QNCE Interactive Session Started');
console.log('Type "help" for available commands');
// Configure undo/redo and autosave
session.engine.configureUndoRedo({
enabled: true,
maxUndoEntries: 100,
maxRedoEntries: 50
});
session.engine.configureAutosave({
enabled: true,
triggers: ['choice', 'flag-change'],
throttleMs: 100,
maxEntries: 20,
includeMetadata: true
});
displayNode(session);
const promptUser = async () => {
return new Promise((resolve) => {
rl.question('\n> ', async (input) => {
const shouldQuit = await handleCommand(session, input);
if (shouldQuit) {
rl.close();
resolve();
}
else {
await promptUser();
resolve();
}
});
});
};
await promptUser();
}
function main() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log('QNCE Interactive CLI');
console.log('Usage: qnce-play [story-file.json] [--storage <type>] [--storage-prefix <p>] [--storage-dir <dir>] [--storage-db <name>] [--non-interactive] [--save-key <key>] [--load-key <key>] [--telemetry <console|file|none>] [--telemetry-file <path>] [--telemetry-sample <0..1>] [--telemetry-report]');
console.log('');
console.log('If no story file is provided, the demo story will be used.');
console.log('');
console.log('Commands during play:');
displayHelp();
return;
}
let storyData;
// Parse args to find a positional story file (skip option values)
const optsWithValue = new Set([
'--storage',
'--storage-prefix',
'--storage-dir',
'--storage-db',
'--save-key',
'--load-key',
// telemetry flags with values
'--telemetry',
'--telemetry-file',
'--telemetry-sample'
]);
let storyFile;
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a.startsWith('-')) {
if (optsWithValue.has(a))
i++; // skip its value
continue;
}
// first non-option token is treated as story file
storyFile = a;
break;
}
if (storyFile) {
try {
console.log(`📖 Loading story: ${storyFile}`);
const jsonData = JSON.parse((0, fs_1.readFileSync)(storyFile, 'utf-8'));
storyData = (0, core_js_1.loadStoryData)(jsonData);
}
catch (error) {
console.error(`❌ Failed to load story file: ${error?.message || error}`);
process.exit(1);
}
}
else {
console.log('📖 Using demo story');
storyData = demo_story_js_1.DEMO_STORY;
}
// Telemetry flags
const telemetryIdx = args.indexOf('--telemetry');
const telemetryKind = telemetryIdx >= 0 ? (args[telemetryIdx + 1] || 'none') : 'none';
const telemetrySampleIdx = args.indexOf('--telemetry-sample');
const telemetrySample = telemetrySampleIdx >= 0 ? Math.max(0, Math.min(1, parseFloat(args[telemetrySampleIdx + 1]))) : undefined;
const telemetryFileIdx = args.indexOf('--telemetry-file');
const telemetryPath = telemetryFileIdx >= 0 ? args[telemetryFileIdx + 1] : undefined;
const telemetryReport = args.includes('--telemetry-report');
let telemetry;
if (telemetryKind && telemetryKind !== 'none') {
const adapter = telemetryKind === 'file' ? (0, core_js_2.createTelemetryAdapter)('file', { path: telemetryPath || 'qnce-telemetry.ndjson' }) : (0, core_js_2.createTelemetryAdapter)('console');
telemetry = (0, core_js_2.createTelemetry)({ adapter, enabled: true, sampleRate: telemetrySample ?? (process.env.NODE_ENV === 'production' ? 0 : 0.25), defaultCtx: { engineVersion: '1.3.1', env: process.env.NODE_ENV || 'dev', sessionId: '' } });
}
const engine = (0, core_js_1.createQNCEEngine)(storyData, undefined, false, undefined, telemetry ? { telemetry, env: process.env.NODE_ENV || 'dev' } : undefined);
// Storage adapter selection
const storageIdx = args.indexOf('--storage');
const storageType = storageIdx >= 0 ? args[storageIdx + 1] : undefined;
if (storageType) {
try {
const adapterOptions = {};
const idxPrefix = args.indexOf('--storage-prefix');
if (idxPrefix >= 0)
adapterOptions.prefix = args[idxPrefix + 1];
const idxDir = args.indexOf('--storage-dir');
if (idxDir >= 0)
adapterOptions.directory = args[idxDir + 1];
const idxDb = args.indexOf('--storage-db');
if (idxDb >= 0)
adapterOptions.databaseName = args[idxDb + 1];
const adapter = (0, StorageAdapters_js_1.createStorageAdapter)(storageType, adapterOptions);
engine.attachStorageAdapter?.(adapter);
console.log(`💾 Storage adapter attached: ${storageType}`);
}
catch (e) {
console.error(`❌ Failed to attach storage adapter '${storageType}': ${e?.message || e}`);
process.exit(1);
}
}
// Non-interactive mode for scripting/tests
const nonInteractive = args.includes('--non-interactive');
const saveKeyIdx = args.indexOf('--save-key');
const saveKey = saveKeyIdx >= 0 ? args[saveKeyIdx + 1] : undefined;
const loadKeyIdx = args.indexOf('--load-key');
const loadKey = loadKeyIdx >= 0 ? args[loadKeyIdx + 1] : undefined;
if (nonInteractive) {
(async () => {
try {
if (loadKey) {
if (engine.loadFromStorage) {
await engine.loadFromStorage(loadKey);
console.log(`✅ Loaded state from key '${loadKey}'`);
}
else {
console.warn('⚠️ No storage adapter attached; cannot load');
}
}
if (saveKey) {
if (engine.saveToStorage) {
await engine.saveToStorage(saveKey);
console.log(`✅ Saved state to key '${saveKey}'`);
}
else {
console.warn('⚠️ No storage adapter attached; cannot save');
}
}
const summary = {
currentNodeId: engine.getState().currentNodeId,
};
if (engine.listStorageKeys) {
try {
summary.storageKeys = await engine.listStorageKeys();
}
catch { }
}
console.log(JSON.stringify(summary));
// If telemetry report requested in non-interactive mode, print it here before exit
if (telemetry && telemetryReport) {
await telemetry.flush();
const s = telemetry.stats();
console.log('\nTelemetry Report');
console.log('----------------');
console.log(`queued : ${s.queued}`);
console.log(`sent : ${s.sent}`);
console.log(`dropped:${s.dropped}`);
if (typeof s.p50 === 'number' || typeof s.p95 === 'number') {
console.log(`p50 send latency: ${s.p50 ?? 'n/a'} ms`);
console.log(`p95 send latency: ${s.p95 ?? 'n/a'} ms`);
console.log('\nBatch send latency (ms):');
console.log(`p50: ${s.p50 ?? 'n/a'} | p95: ${s.p95 ?? 'n/a'}`);
}
}
process.exit(0);
}
catch (error) {
console.error('❌ Non-interactive run failed:', error?.message || error);
process.exit(1);
}
})();
return;
}
const rl = (0, readline_1.createInterface)({
input: process.stdin,
output: process.stdout
});
const session = { engine, rl, telemetry };
startInteractiveSession(session).catch((error) => {
console.error('❌ Session error:', error.message);
process.exit(1);
});
// Print telemetry report on exit
if (telemetry && telemetryReport) {
process.on('beforeExit', async () => {
await telemetry.flush();
const s = telemetry.stats();
// Minimal ASCII report
console.log('\nTelemetry Report');
console.log('----------------');
console.log(`queued : ${s.queued}`);
console.log(`sent : ${s.sent}`);
console.log(`dropped:${s.dropped}`);
if (typeof s.p50 === 'number' || typeof s.p95 === 'number') {
console.log(`p50 send latency: ${s.p50 ?? 'n/a'} ms`);
console.log(`p95 send latency: ${s.p95 ?? 'n/a'} ms`);
}
if (typeof s.p50 === 'number' || typeof s.p95 === 'number') {
console.log('\nBatch send latency (ms):');
console.log(`p50: ${s.p50 ?? 'n/a'} | p95: ${s.p95 ?? 'n/a'}`);
}
});
}
}
// Run if called directly
if (process.argv[1] && process.argv[1].endsWith('play.js')) {
main();
}
//# sourceMappingURL=play.js.map