// 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: skill 2 berjenis area tanpa `ahead` (jangkauannya lebih pendek dari pukulan dasar) dulu tidak pernah terpilih karena pukulan dasar didahulukan. // Sejak cpuAreaKit/cpuAreaFits (game.js, RENCANA 6.5) CPU memakainya bila lawan masuk lingkarannya, tetapi hanya di sebagian ronde (bot simulator menjauh tepat saat CPU bebas), jadi // tetap dicatat sebagai peringatan (tidak menggagalkan). Lihat docs/balance-report.md. Bila CPU sudah memakainya di lebih dari separuh ronde, 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'); };