Newer
Older
rpg / tools / checks / ai.mjs
/**
 * Сценарий ai — ИИ врагов (батч 4).
 * 1) тяжеловес с патрулём стартует бодрым в patrol и реально ходит;
 * 2) тихий шум (0.35) при далёком герое — настороженность (wary);
 * 3) не увидел героя — wary гаснет, возврат к дому и снова patrol;
 * 4) герой рядом с LOS — погоня (chase);
 * 5) герой далеко — потеря погони (return -> patrol);
 * 6) мало hp — отступление бегом (flee), дистанция растёт;
 * 7) инварианты (enemy-state-legal, enemy-in-bounds) чисты.
 * Запуск: node tools/agent.mjs run tools/checks/ai.mjs
 */
import { startDevServer, openGame, Checks } from '../agent-lib.mjs';

const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
/** Тяжеловес из снапшота. */
const heavy = (s) => (s.enemies ?? []).find((e) => e.kind === 'heavy');

export default async function ({ pretty }) {
    const c = new Checks('ai');
    const server = await startDevServer();
    let ctx;
    try {
        await c.run('патруль: тяжеловес бодр и ходит по маршруту', async () => {
            ctx = await openGame({ url: server.url, newGame: true });
            const s0 = await ctx.agent.snapshot();
            const h0 = heavy(s0);
            c.expect(h0?.state === 'patrol', 'тяжеловес не в patrol на старте', h0);
            // Спавн (6,22), патруль к (4,20): позиция меняется без участия героя.
            const w = await ctx.agent.waitFor(
                '(s.enemies.find((e) => e.kind === "heavy")?.pos.x ?? 9) < 6',
                { timeoutTicks: 600 }
            );
            c.expect(w.ok, 'тяжеловес не сдвинулся с места в патруле', w.snapshot.enemies);
            return null;
        });

        await c.run('тихий шум настораживает патрульного', async () => {
            // Герой далеко (спавн 14,14), шум у маршрута тяжеловеса.
            await ctx.agent.command('scene:noise', { x: 5, y: 21, level: 0.35 });
            const w = await ctx.agent.waitFor(
                's.enemies.find((e) => e.kind === "heavy")?.state === "wary"',
                { timeoutTicks: 300 }
            );
            c.expect(w.ok, 'тяжеловес не насторожился от шума', w.snapshot.enemies);
            return null;
        });

        await c.run('настороженность гаснет: wary -> return -> patrol', async () => {
            const w = await ctx.agent.waitFor(
                's.enemies.find((e) => e.kind === "heavy")?.state === "patrol"',
                { timeoutTicks: 900 }
            );
            c.expect(w.ok, 'тяжеловес не вернулся к патрулю', w.snapshot.enemies);
            return null;
        });

        await c.run('герой рядом — погоня', async () => {
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 300 });
            await ctx.agent.command('scene:teleport', { x: 5, y: 22 });
            const w = await ctx.agent.waitFor(
                's.enemies.find((e) => e.kind === "heavy")?.state === "chase"',
                { timeoutTicks: 300 }
            );
            c.expect(w.ok, 'тяжеловес не погнался за героем', w.snapshot.enemies);
            return null;
        });

        await c.run('герой далеко — погоня гаснет и враг уходит домой', async () => {
            await ctx.agent.command('scene:teleport', { x: 14, y: 14 });
            const w = await ctx.agent.waitFor(
                's.enemies.find((e) => e.kind === "heavy")?.state === "patrol"',
                { timeoutTicks: 900 }
            );
            c.expect(w.ok, 'тяжеловес не отступил к дому', w.snapshot.enemies);
            return null;
        });

        await c.run('мало hp — отступление бегом', async () => {
            await ctx.agent.command('scene:teleport', { x: 6, y: 21 });
            const dmg = await ctx.agent.command('scene:damageEnemy', { id: 'heavy', value: 6 });
            c.expect(dmg === true, 'scene:damageEnemy не нашла тяжеловеса');
            const w = await ctx.agent.waitFor(
                's.enemies.find((e) => e.kind === "heavy")?.state === "flee"',
                { timeoutTicks: 300 }
            );
            c.expect(w.ok, 'тяжеловес не отступает при низком hp', w.snapshot.enemies);
            // Держится подальше: дистанция до неподвижного героя растёт.
            const s1 = await ctx.agent.snapshot();
            const d1 = dist(heavy(s1).pos, s1.hero.pos);
            await ctx.agent.waitFor('false', { timeoutTicks: 60 }); // 1 сек наблюдения
            const s2 = await ctx.agent.snapshot();
            const d2 = dist(heavy(s2).pos, s2.hero.pos);
            c.expect(d2 > d1, 'отступающий не отдаляется', { d1, d2 });
            return null;
        });

        await c.run('инварианты ИИ чисты', async () => {
            const inv = await ctx.agent.invariants();
            const errors = (inv ?? []).filter((i) => i.severity === 'error');
            c.expect(errors.length === 0, 'инварианты нарушены', errors);
            return null;
        });
    } finally {
        await ctx?.browser?.close().catch(() => {});
        await server.stop();
    }
    return c.finish({ pretty }).ok ? 0 : 1;
}