fighting-konoha/tests/pairs.mjs

60 lines
5.3 KiB
JavaScript

// Uji pasangan (RENCANA 5.4): semua 49 pasangan arah (7 x 7, termasuk cermin) dimainkan singkat oleh bot melawan CPU.
// node tests/harness.mjs tests/pairs.mjs env: PAIRS_SECONDS = detik waktu permainan per pasangan (bawaan 24, cukup untuk ultimate dan mode)
// Memeriksa: galat halaman/konsol (ditangkap harness), startMatch berhasil, angka tidak wajar (NaN, HP di luar 0..200), tiap karakter memakai skill 1,
// skill 2, dan ultimate sedikitnya sekali di sisi bot DAN sisi CPU, data peringkat content/tiers.js, dan bagian peringkat di menu Koleksi.
import { installBalance } from './bot.mjs';
// Celah CPU yang sudah diketahui: AI memilih pukulan dasar lebih dulu, dan skill 2 berjenis area tanpa `ahead` (jangkauannya lebih pendek dari pukulan dasar) tidak pernah terpilih.
// Lihat docs/balance-report.md. Bila datanya disetel supaya CPU memakainya, peringatan berubah jadi catatan agar daftar ini dibersihkan.
const KNOWN_CPU_GAPS = { ethanolman: ['skill 2'], soloman: ['skill 2'] };
export default async ({ page, game, characters }) => {
await page.evaluate(installBalance);
const secs = Number(process.env.PAIRS_SECONDS) || 24, cap = Math.round(secs * 30), problems = [], seen = { bot: {}, cpu: {} }, t0 = Date.now(), per = characters.length; // seen[sisi][id] = [ronde memakai skill 1, skill 2, ultimate]
let rounds = 0, ended = 0;
for (const a of characters) for (const b of characters) {
const r = await game(args => window.BalSim.round({ a: args.a, b: args.b, level: 'hard', rseed: 5, cap: args.cap }), { a, b, cap });
rounds++;
if (r.error) { problems.push(r.error); continue; }
if (r.w !== 'S') ended++;
if (r.nan || ![r.hh, r.ch, r.t].every(Number.isFinite) || r.hh < 0 || r.hh > 200 || r.ch < 0 || r.ch > 200) problems.push(`angka tidak wajar: ${a} (bot) vs ${b} (CPU) -> ${JSON.stringify({ hh: r.hh, ch: r.ch, t: r.t, nan: r.nan })}`);
for (const [side, id, bits] of [['bot', a, r.hs], ['cpu', b, r.cs]]) { const c = seen[side][id] || (seen[side][id] = [0, 0, 0]); for (let k = 0; k < 3; k++) if (bits & (1 << k)) c[k]++; }
}
// tiap karakter harus memakai skill 1, skill 2, dan ultimate di kedua sisi (sedikitnya satu dari 7 ronde); celah CPU yang sudah diketahui hanya diperingatkan
const warnings = [], NAMES = ['skill 1', 'skill 2', 'ultimate'];
for (const [side, map] of Object.entries(seen)) for (const id of characters) {
const c = map[id] || [0, 0, 0], known = side === 'cpu' ? KNOWN_CPU_GAPS[id] || [] : [];
const bad = NAMES.filter((n, k) => !c[k] && !known.includes(n));
if (bad.length) problems.push(`${id} di sisi ${side} tidak pernah memakai: ${bad.join(', ')}`);
for (const n of known) { const k = NAMES.indexOf(n); warnings.push(c[k] > per / 2 ? `${id} (CPU) kini sering memakai ${n} (${c[k]} dari ${per} ronde): hapus dari KNOWN_CPU_GAPS di tests/pairs.mjs` : `${id} (CPU) jarang memakai ${n} (${c[k]} dari ${per} ronde): celah AI yang sudah diketahui`); }
}
// Koleksi: buka tab (bagian peringkat memuat content/tiers.js secara malas), tunggu sampai tampil, lalu periksa tampilan dan datanya
const shown = await page.evaluate(async () => {
window.Meta.open('book');
for (let i = 0; i < 50 && !document.querySelector('#meta-overlay .tier-chip'); i++) await new Promise(r => setTimeout(r, 100));
const text = document.querySelector('#meta-overlay .meta-body')?.textContent || '';
const out = { chips: document.querySelectorAll('#meta-overlay .tier-chip').length, title: text.includes('Peringkat karakter (versi simulator)'), note: text.includes('bukan kebenaran mutlak') };
window.Meta.close(); return out;
});
if (shown.chips !== characters.length || !shown.title || !shown.note) problems.push('Koleksi tidak menampilkan peringkat karakter dengan benar: ' + JSON.stringify(shown));
const tiers = await page.evaluate(() => window.TIERS || null);
if (!tiers) problems.push('content/tiers.js tidak termuat (window.TIERS kosong)');
else {
const ids = tiers.order || [];
if (ids.length !== characters.length || new Set(ids).size !== ids.length || !ids.every(id => characters.includes(id))) problems.push('TIERS.order harus memuat tepat ke-' + characters.length + ' karakter tanpa ganda: ' + JSON.stringify(ids));
for (const id of characters) {
const r = tiers.rate?.[id], t = tiers.tier?.[id];
if (!Number.isFinite(r) || r < 0 || r > 100) problems.push(`TIERS.rate.${id} tidak wajar: ${r}`);
if (!['S', 'A', 'B', 'C'].includes(t)) problems.push(`TIERS.tier.${id} tidak wajar: ${t}`);
}
for (let i = 1; i < ids.length; i++) if ((tiers.rate?.[ids[i]] ?? 0) > (tiers.rate?.[ids[i - 1]] ?? 0)) problems.push('TIERS.order tidak urut menurun menurut rate');
if (typeof tiers.updated !== 'string') problems.push('TIERS.updated harus teks (tanggal)');
}
const lines = [`Pasangan: ${rounds} ronde singkat (${secs} dtk permainan), ${ended} selesai K.O., ${((Date.now() - t0) / 1000).toFixed(1)} dtk nyata.`, `Peringkat: ${tiers ? tiers.order.join(' > ') + ' (' + tiers.updated + ')' : 'tidak ada'}; chip di Koleksi: ${shown.chips}.`];
if (warnings.length) lines.push('Peringatan (tidak menggagalkan):', ...warnings.map(w => ' - ' + w));
if (problems.length) { lines.push('MASALAH:', ...problems.map(p => ' - ' + p)); throw new Error(lines.join('\n')); }
lines.push('semua pasangan lolos');
return lines.join('\n');
};