/**
* Сценарий 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 }
);
}