import { describe, it, expect } from 'vitest';
import {
evalConditions, hasConditions, hasText, validateDialogue,
type DialogueGraph, type DialogueState, type DialogueWorld,
} from '../graph';
/** Простое состояние-заглушка: флаги и переменные в Map. */
function makeState(): DialogueState & { flags: Set<string>; vars: Map<string, number | string | boolean> } {
const flags = new Set<string>();
const vars = new Map<string, number | string | boolean>();
return {
flags,
vars,
hasFlag: (f) => flags.has(f),
setFlag: (f) => flags.add(f),
clearFlag: (f) => flags.delete(f),
getVar: (k) => vars.get(k),
setVar: (k, v) => vars.set(k, v),
};
}
const world: DialogueWorld = { hasItem: (id) => id === 'herb' };
describe('dialogue/graph — условия', () => {
it('hasConditions/hasText различают реплику, действие и условие', () => {
expect(hasText({ text: 'Привет' })).toBe(true);
expect(hasText({})).toBe(false);
expect(hasConditions({ when: ['a'] })).toBe(true);
expect(hasConditions({ whenVar: { key: 'x', op: 'gt', value: 1 } })).toBe(true);
expect(hasConditions({})).toBe(false);
});
it('when/whenNot — AND по списку флагов', () => {
const st = makeState();
st.setFlag('met');
expect(evalConditions({ when: ['met'] }, st, world)).toBe(true);
expect(evalConditions({ when: ['met', 'other'] }, st, world)).toBe(false);
expect(evalConditions({ whenNot: ['other'] }, st, world)).toBe(true);
expect(evalConditions({ whenNot: ['met'] }, st, world)).toBe(false);
});
it('whenVar: eq/ne работают с любыми значениями, gt/lt — только с числами', () => {
const st = makeState();
st.setVar('gold', 5);
st.setVar('name', 'Ив');
expect(evalConditions({ whenVar: { key: 'gold', op: 'eq', value: 5 } }, st, world)).toBe(true);
expect(evalConditions({ whenVar: { key: 'gold', op: 'ne', value: 5 } }, st, world)).toBe(false);
expect(evalConditions({ whenVar: { key: 'gold', op: 'gt', value: 4 } }, st, world)).toBe(true);
expect(evalConditions({ whenVar: { key: 'gold', op: 'le', value: 5 } }, st, world)).toBe(true);
expect(evalConditions({ whenVar: { key: 'gold', op: 'lt', value: 5 } }, st, world)).toBe(false);
expect(evalConditions({ whenVar: { key: 'name', op: 'gt', value: 1 } }, st, world)).toBe(false);
// нет такой переменной: eq false, ne true, сравнение ложно
expect(evalConditions({ whenVar: { key: 'nope', op: 'eq', value: 0 } }, st, world)).toBe(false);
expect(evalConditions({ whenVar: { key: 'nope', op: 'ne', value: 0 } }, st, world)).toBe(true);
expect(evalConditions({ whenVar: { key: 'nope', op: 'gt', value: 0 } }, st, world)).toBe(false);
});
it('hasItem требует DialogueWorld; все группы — AND', () => {
const st = makeState();
expect(evalConditions({ hasItem: ['herb'] }, st, world)).toBe(true);
expect(evalConditions({ hasItem: ['herb', 'sword'] }, st, world)).toBe(false);
expect(evalConditions({ hasItem: ['herb'] }, st, undefined)).toBe(false);
expect(evalConditions({ when: ['met'], hasItem: ['herb'] }, st, world)).toBe(false);
});
});
describe('dialogue/graph — валидация', () => {
it('чистый граф без ошибок', () => {
const g: DialogueGraph = {
start: 'a',
nodes: {
a: { text: 'Привет', next: 'b' },
b: { text: 'Пока', end: true },
},
};
expect(validateDialogue(g)).toEqual([]);
});
it('ловит битые ссылки, тупики и выборы без текста', () => {
const g: DialogueGraph = {
start: 'a',
nodes: {
a: { text: 'Привет', next: 'ghost' },
ghostNode: { text: 'не ссылается никто' },
dead: { do: [] },
bad: {
text: 'Хм',
choices: [{ text: '', next: 'a' }, { text: 'Ок', next: 'nope' }],
},
},
};
const errs = validateDialogue(g).join('\n');
expect(errs).toContain('a: next → «ghost» не существует');
expect(errs).toContain('dead: узел без текста, next, choices и end — тупик');
expect(errs).toContain('bad: выбор без текста');
expect(errs).toContain('bad: choice.next → «nope» не существует');
});
it('узел-действие без текста с next — корректен', () => {
const g: DialogueGraph = {
start: 'act',
nodes: { act: { setFlags: ['met'], next: 'talk' }, talk: { text: 'Привет', end: true } },
};
expect(validateDialogue(g)).toEqual([]);
});
});