Newer
Older
rpg / apps / game / tools / dialogues / probe.ts
/**
 * Проверка произвольного графа (для сервера редактора, D8). Читает stdin:
 *   { "op": "validate", "graph": {...} }              — Invariants + анализ
 *   { "op": "dry-run",  "graph": {...}, "preset": "fresh" } — ещё и прогон раннера
 * Печатает JSON:
 *   { errors, warnings, orphans, cycles, silentEnds, run? }
 * Запуск: vite-node apps/game/tools/dialogues/probe.ts
 */
import { GameState, DialogueRunner } from '@rpg/engine';
import type { DialogueGraph } from '@rpg/engine';
import { analyzeGraph, checkGraph, type GraphRefs } from '../../src/data/dialogueRules';
import { FLAGS, VARS } from '../../src/data/ids';
import { ITEMS } from '../../src/data/items';
import { DIALOGUE_CUSTOM } from '../../src/data/effects';

/** Реестры игры (те же множества, что в validate.ts и dry-run.ts). */
function refs(): GraphRefs {
    return {
        flags: new Set(Object.keys(FLAGS)),
        vars: new Set(Object.keys(VARS)),
        items: new Set(Object.keys(ITEMS)),
        customs: new Set(Object.keys(DIALOGUE_CUSTOM)),
        strings: new Set()
    };
}

/** Пресеты состояния — как в dry-run.ts (общая семантика с CLI). */
const PRESETS: { name: string; setup: (s: GameState) => void }[] = [
    { name: 'fresh', setup: () => {} },
    {
        name: 'quest-taken+flowers',
        setup: (s) => {
            s.setFlag('quest_bells_taken');
            s.setFlag('met_elder');
            s.setVar('flowers', 3);
        }
    },
    {
        name: 'quest-done',
        setup: (s) => {
            s.setFlag('quest_bells_taken');
            s.setFlag('met_elder');
            s.setFlag('quest_bells_done');
        }
    }
];

/** Один прогон раннера: реплики до первого выбора (или до конца). */
function walk(graph: DialogueGraph, preset: string): string {
    const state = new GameState();
    (PRESETS.find((p) => p.name === preset) ?? PRESETS[0]!).setup(state);
    const runner = new DialogueRunner(state);
    const shown: string[] = [];
    runner.setView({
        show: (n) =>
            void shown.push(
                `${n.speaker ? n.speaker + ': ' : ''}${n.text}` +
                    (n.choices.length > 0 ? ` [${n.choices.map((c) => c.text).join(' | ')}]` : '')
            ),
        hide: () => {}
    });
    runner.start(graph);
    while (runner.active && !runner.waitingForChoice) runner.advance();
    return shown.join(' → ') || '(молча)';
}

const main = (): void => {
    const chunks: string[] = [];
    process.stdin.on('data', (c) => chunks.push(c));
    process.stdin.on('end', () => {
        const req = JSON.parse(chunks.join('')) as {
            op?: string;
            graph: DialogueGraph;
            preset?: string;
        };
        const graph = req.graph;
        const a = analyzeGraph(graph);
        const inv = checkGraph('editor', graph, refs());
        const out: Record<string, unknown> = {
            errors: inv.filter((i) => i.severity === 'error'),
            warnings: inv.filter((i) => i.severity === 'warn'),
            orphans: a.orphans,
            cycles: a.textlessCycles,
            silentEnds: a.silentEnds
        };
        if (req.op === 'dry-run') {
            out.run = walk(graph, req.preset ?? 'fresh');
        }
        console.log(JSON.stringify(out));
    });
};

main();