Newer
Older
rpg / apps / game / 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 { withChecks } from '../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 }) {
    return withChecks(
        'ai',
        async (t) => {
            const { c } = t;
            await c.run('патруль: тяжеловес бодр и ходит по маршруту', async () => {
                await t.boot();
                const s0 = await t.ctx.agent.snapshot();
                const h0 = heavy(s0);
                c.expect(h0?.state === 'patrol', 'тяжеловес не в patrol на старте', h0);
                // Спавн (6,22), патруль к (4,20): позиция меняется без участия героя.
                const w = await t.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 t.ctx.agent.command('scene:noise', { x: 5, y: 21, level: 0.35 });
                const w = await t.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 t.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 t.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 300 });
                await t.ctx.agent.command('scene:teleport', { x: 5, y: 22 });
                const w = await t.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 t.ctx.agent.command('scene:teleport', { x: 14, y: 14 });
                const w = await t.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 t.ctx.agent.command('scene:teleport', { x: 6, y: 21 });
                const dmg = await t.ctx.agent.command('scene:damageEnemy', { id: 'heavy', value: 6 });
                c.expect(dmg === true, 'scene:damageEnemy не нашла тяжеловеса');
                const w = await t.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 t.ctx.agent.snapshot();
                const d1 = dist(heavy(s1).pos, s1.hero.pos);
                await t.ctx.agent.waitFor('false', { timeoutTicks: 60 }); // 1 сек наблюдения
                const s2 = await t.ctx.agent.snapshot();
                const d2 = dist(heavy(s2).pos, s2.hero.pos);
                c.expect(d2 > d1, 'отступающий не отдаляется', { d1, d2 });
                return null;
            });

            await c.run('инварианты ИИ чисты', async () => {
                await t.expectInvariantsClean('в ИИ-сценарии');
                return null;
            });
        },
        { pretty }
    );
}