Newer
Older
rpg / apps / game / tools / checks / synth.mjs
/**
 * Сценарий synth — спек-синтез: агент без слуха описывает звуки параметрами
 * (scene:synthesize → playSpec), факты запуска читаем из window.__gameAudioLog.
 * 1) удар (hit): запуск в логе с заданным ключом и громкостью;
 * 2) звон (chime): другой ключ, кэш — компиляция не мешает повтору;
 * 3) детерминизм: тот же спек дважды — оба запуска в логе;
 * 4) негодный спек (нет dur / неизвестный kind) — команда возвращает null;
 * 5) гул (hum) не глушит амбиент области (разные голоса).
 * Запуск: node tools/agent.mjs run tools/checks/synth.mjs
 */
import { startDevServer, openGame, Checks } from '../lib.mjs';

/** Прочитать DEV-лог аудио из страницы. */
const readLog = (page) => page.evaluate(() => window.__gameAudioLog ?? null);

/** Команда моста с ожиданием появления ключа в логе (синтез асинхронный). */
async function synthAndWait({ page, agent }, spec, { key = 'agent/synth', volume, tries = 12 } = {}) {
    const before = (await readLog(page)).filter((e) => e.key === key).length;
    const res = await agent.command('scene:synthesize', { spec, key, ...(volume !== undefined ? { volume } : {}) });
    for (let i = 0; i < tries; i++) {
        const log = await readLog(page);
        if (log.filter((e) => e.key === key).length > before) return { res, log };
        await agent.step(30);
    }
    return { res, log: await readLog(page) };
}

export default async function ({ pretty }) {
    const c = new Checks('synth');
    const server = await startDevServer();
    let ctx;
    try {
        await c.run('новая игра: спек-синтез недоступен в прод-сборке с внятной ошибкой', async () => {
            ctx = await openGame({ url: server.url, newGame: true });
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
            c.expect(Array.isArray(await readLog(ctx.page)), 'DEV-лог аудио недоступен (прод-сборка?)');
            return null;
        });
        await c.run('удар по спеку: запуск в логе с заданным ключом и громкостью', async () => {
            const { res, log } = await synthAndWait(
                ctx,
                { kind: 'hit', dur: 0.3, low: 600, high: 3000, seed: 7 },
                { key: 'agent/hit-test', volume: 0.5 }
            );
            c.expect(res === true, 'scene:synthesize не принят мостом', res);
            const hits = log.filter((e) => e.key === 'agent/hit-test');
            c.expect(hits.length > 0, 'удар по спеку не сыграл', log);
            c.expect(Math.abs(hits.at(-1).volume - 0.5) < 0.01, 'громкость не дошла до голоса', hits.at(-1));
            return null;
        });
        await c.run('звон по спеку: другой ключ играет параллельно', async () => {
            const { res, log } = await synthAndWait(
                ctx,
                { kind: 'chime', dur: 0.6, freq: 880 },
                { key: 'agent/chime-test' }
            );
            c.expect(res === true, 'scene:synthesize не принят мостом', res);
            c.expect(log.some((e) => e.key === 'agent/chime-test'), 'звон по спеку не сыграл', log);
            c.expect(
                log.some((e) => e.key === 'agent/hit-test'),
                'предыдущий голос пропал из лога (лог мал?)',
                log
            );
            return null;
        });
        await c.run('детерминизм: тот же спек дважды — оба запуска в логе', async () => {
            const spec = { kind: 'scrape', dur: 0.5, low: 80, high: 400, tone: 60, seed: 3 };
            const first = await synthAndWait(ctx, spec, { key: 'agent/scrape-test' });
            const second = await synthAndWait(ctx, spec, { key: 'agent/scrape-test' });
            c.expect(first.res === true && second.res === true, 'команда не принята', [first.res, second.res]);
            const plays = second.log.filter((e) => e.key === 'agent/scrape-test');
            c.expect(plays.length >= 2, 'повторный синтез не сыграл', plays);
            return null;
        });
        await c.run('негодный спек: без dur и с неизвестным kind — null', async () => {
            const noDur = await ctx.agent.command('scene:synthesize', { spec: { kind: 'hit' } });
            const badKind = await ctx.agent.command('scene:synthesize', { spec: { kind: 'thunder', dur: 1 } });
            const noSpec = await ctx.agent.command('scene:synthesize', {});
            c.expect(noDur === null, 'спек без dur должен вернуть null', noDur);
            c.expect(badKind === null, 'неизвестный kind должен вернуть null', badKind);
            c.expect(noSpec === null, 'вызов без спека должен вернуть null', noSpec);
            return null;
        });
        await c.run('гул (hum) не глушит амбиент области', async () => {
            const { res, log } = await synthAndWait(
                ctx,
                { kind: 'hum', dur: 2, tone: 55, loop: true },
                { key: 'agent/hum-test' }
            );
            c.expect(res === true, 'scene:synthesize не принят мостом', res);
            c.expect(log.some((e) => e.key === 'agent/hum-test'), 'гул по спеку не сыграл', log);
            c.expect(log.some((e) => e.key === 'ambience/meadows'), 'амбиент области пропал', log);
            return null;
        });
    } finally {
        await ctx?.browser?.close();
        server.stop();
    }
    return c.finish({ pretty }).ok ? 0 : 1;
}