- tests/test.js: 148 -> 206 проверок (разделы 23 wrap/torus, 24 зоны, 25 бюджет, 26 размеры, 27 сейв v2/миграция, 28 рендер/resize 300x300, 29 HUD 381-400px); resetGlobals сбрасывает tool/reason/winner; sleep заменён на waitCond/settleViewport. - tools/old-ref/: независимый torus-эталон (nextStepCleanWrap, CLI --torus, gen-torus.js, reference-torus.json); diff-our.js сверяет stepN при setWrap(true). - index.html: медиазапрос HUD 380->400px (убрана микро-обрезка 381-388px); инлайн-комментарий про согласованность useWrap/setWrap. - AGENTS.md: полный список контракта дополнен; useWrap-заметка уточнена; покрытие приведено в соответствие. README: скорость — константа в коде, не UI. - .gitignore: tools/old-ref/.tmp-*.txt. - Прогоны: check: OK; суита 206/206 (3 повтора); паритет clean 273/273 + torus 273/273. - Ревью-гейт R2-3 (Oracle): PASS; minor/nit закрыты.
185 lines
8.2 KiB
JavaScript
185 lines
8.2 KiB
JavaScript
'use strict';
|
||
/* Дифтест: симуляция новой игры (index.html) против эталонов старого прототипа.
|
||
- reference.json — выходы ПРОТОТИПА (--proto, с багом parent[])
|
||
- reference-clean.json — выходы ЧИСТОГО правила (--clean), сгенерированы gen-clean.js
|
||
|
||
Для каждого кейса: setFieldASCII(ascii0, w), затем N шагов stepN(1); поле
|
||
считывается как ASCII (h строк по w, row-major, '.'/'1'/'2') и сравнивается
|
||
по шагам и с clean, и с proto.
|
||
|
||
Запуск: node diff-our.js
|
||
Env: CHROMIUM (путь к Chromium), по умолчанию '/usr/bin/chromium-browser'.
|
||
|
||
Код выхода: 0 — наш step == clean на ВСЕХ кейсах; 1 — есть расхождения с clean
|
||
(это баг нашей реализации); расхождения с proto ожидаемы (баг parent[]) и на
|
||
код выхода не влияют.
|
||
*/
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
let puppeteer;
|
||
try {
|
||
puppeteer = require('puppeteer-core');
|
||
} catch (e) {
|
||
puppeteer = require(__dirname + '/../../tests/node_modules/puppeteer-core');
|
||
}
|
||
|
||
const DIR = __dirname;
|
||
const GAME_URL = 'file://' + path.resolve(__dirname, '..', '..', 'index.html');
|
||
const refProto = JSON.parse(fs.readFileSync(path.join(DIR, 'reference.json'), 'utf8'));
|
||
const refClean = JSON.parse(fs.readFileSync(path.join(DIR, 'reference-clean.json'), 'utf8'));
|
||
// Опциональный эталон torus (генерируется gen-torus.js); нет — проверка пропускается.
|
||
const torusPath = path.join(DIR, 'reference-torus.json');
|
||
const refTorus = fs.existsSync(torusPath) ? JSON.parse(fs.readFileSync(torusPath, 'utf8')) : null;
|
||
|
||
const cleanByName = new Map(refClean.map(c => [c.name, c]));
|
||
|
||
// Построчная визуализация отличий двух ASCII-полей (для отчёта).
|
||
function diffLines(exp, act) {
|
||
const e = exp.replace(/\n$/, '').split('\n');
|
||
const a = act.replace(/\n$/, '').split('\n');
|
||
const rows = [];
|
||
for (let i = 0; i < Math.max(e.length, a.length); i++) {
|
||
const el = e[i] || '', al = a[i] || '';
|
||
const mark = el === al ? ' ' : '!';
|
||
let out = mark + ' ' + (el || '').padEnd(al.length > el.length ? al.length : el.length);
|
||
if (el !== al) out += ' | ожид=' + JSON.stringify(el) + ' факт=' + JSON.stringify(al);
|
||
rows.push(out);
|
||
}
|
||
return rows.join('\n');
|
||
}
|
||
|
||
(async () => {
|
||
const browser = await puppeteer.launch({
|
||
executablePath: process.env.CHROMIUM || '/usr/bin/chromium-browser',
|
||
headless: true,
|
||
args: ['--no-sandbox', '--disable-gpu', '--hide-scrollbars', '--mute-audio'],
|
||
defaultViewport: { width: 390, height: 844, deviceScaleFactor: 2 },
|
||
});
|
||
const page = await browser.newPage();
|
||
const pageErrors = [];
|
||
page.on('pageerror', e => pageErrors.push(String(e)));
|
||
await page.goto(GAME_URL, { waitUntil: 'load' });
|
||
await new Promise(r => setTimeout(r, 300));
|
||
|
||
let cleanTotal = 0, cleanMatch = 0;
|
||
let protoTotal = 0, protoMatch = 0;
|
||
let cleanBadCases = 0;
|
||
let protoDiffCases = 0;
|
||
const protoDiffList = [];
|
||
const shown = { clean: 0, proto: 0, torus: 0 };
|
||
const MAX_SHOW = 12;
|
||
|
||
for (const c of refProto) {
|
||
const init = c.steps[0];
|
||
const n = c.steps.length - 1;
|
||
const ourSeq = await page.evaluate((initAscii, w, steps) => {
|
||
function toAscii(f) {
|
||
let s = '';
|
||
for (let y = 0; y < fieldH; y++) {
|
||
for (let x = 0; x < fieldW; x++) {
|
||
const v = f[y * fieldW + x];
|
||
s += v === 0 ? '.' : String(v);
|
||
}
|
||
s += '\n';
|
||
}
|
||
return s;
|
||
}
|
||
forceState('MENU');
|
||
setFieldASCII(initAscii, w);
|
||
const seq = [toAscii(field)];
|
||
for (let k = 0; k < steps; k++) { stepN(1); seq.push(toAscii(field)); }
|
||
return seq;
|
||
}, init, c.w, n);
|
||
|
||
const clean = cleanByName.get(c.name);
|
||
let caseClean = 0, caseProto = 0;
|
||
for (let k = 1; k <= n; k++) {
|
||
cleanTotal++; protoTotal++;
|
||
const mine = ourSeq[k];
|
||
if (mine === clean.steps[k]) { cleanMatch++; caseClean++; }
|
||
else if (shown.clean < MAX_SHOW) {
|
||
shown.clean++;
|
||
console.log(`\n[РАСХОЖДЕНИЕ С CLEAN] case=${c.name} step=${k}`);
|
||
console.log(diffLines(clean.steps[k], mine));
|
||
}
|
||
if (mine === c.steps[k]) { protoMatch++; caseProto++; }
|
||
else if (shown.proto < MAX_SHOW) {
|
||
shown.proto++;
|
||
console.log(`\n[расхождение с PROTO — ожидаемо, баг parent[]] case=${c.name} step=${k}`);
|
||
console.log(diffLines(c.steps[k], mine));
|
||
}
|
||
}
|
||
if (caseClean !== n) cleanBadCases++;
|
||
if (caseProto !== n) { protoDiffCases++; protoDiffList.push(c.name); }
|
||
console.log(`${caseClean === n ? 'OK ' : 'FAIL'} clean ${caseClean}/${n} | ` +
|
||
`proto ${caseProto}/${n} | ${c.name}`);
|
||
}
|
||
|
||
// --- независимая проверка опции «Замкнутая карта» (torus) ---
|
||
let torusTotal = 0, torusMatch = 0, torusBadCases = 0;
|
||
if (refTorus) {
|
||
const torusByName = new Map(refTorus.map(c => [c.name, c]));
|
||
console.log('\n----- torus (wrap, чистое большинство) -----');
|
||
for (const c of refProto) {
|
||
const init = c.steps[0];
|
||
const n = c.steps.length - 1;
|
||
const ourSeq = await page.evaluate((initAscii, w, steps) => {
|
||
function toAscii(f) {
|
||
let s = '';
|
||
for (let y = 0; y < fieldH; y++) {
|
||
for (let x = 0; x < fieldW; x++) {
|
||
const v = f[y * fieldW + x];
|
||
s += v === 0 ? '.' : String(v);
|
||
}
|
||
s += '\n';
|
||
}
|
||
return s;
|
||
}
|
||
forceState('MENU');
|
||
setWrap(true); // пересобирает таблицы соседей под torus
|
||
setFieldASCII(initAscii, w);
|
||
const seq = [toAscii(field)];
|
||
for (let k = 0; k < steps; k++) { stepN(1); seq.push(toAscii(field)); }
|
||
return seq;
|
||
}, init, c.w, n);
|
||
const t = torusByName.get(c.name);
|
||
let caseT = 0;
|
||
for (let k = 1; k <= n; k++) {
|
||
torusTotal++;
|
||
if (ourSeq[k] === t.steps[k]) { torusMatch++; caseT++; }
|
||
else if (shown.torus < MAX_SHOW) {
|
||
shown.torus++;
|
||
console.log(`\n[РАСХОЖДЕНИЕ С TORUS] case=${c.name} step=${k}`);
|
||
console.log(diffLines(t.steps[k], ourSeq[k]));
|
||
}
|
||
}
|
||
if (caseT !== n) torusBadCases++;
|
||
console.log(`${caseT === n ? 'OK ' : 'FAIL'} torus ${caseT}/${n} | ${c.name}`);
|
||
}
|
||
await page.evaluate(() => setWrap(false));
|
||
}
|
||
|
||
await browser.close();
|
||
|
||
console.log('\n================ ИТОГ ДИФТЕСТА ================');
|
||
console.log(`случайных pageerror: ${pageErrors.length}${pageErrors.length ? ' -> ' + pageErrors.join(' | ') : ''}`);
|
||
console.log(`СОВПАДЕНИЕ С ЧИСТЫМ ПРАВИЛОМ (expected): ${cleanMatch}/${cleanTotal}` +
|
||
` (кейсов с расхождением: ${cleanBadCases})`);
|
||
console.log(`совпадение с прототипом (informational): ${protoMatch}/${protoTotal}` +
|
||
` (кейсов с расхождением: ${protoDiffCases})`);
|
||
console.log(`расхождений с proto (${protoDiffList.length} кейсов): ${protoDiffList.join(', ') || '(нет)'}`);
|
||
if (refTorus) {
|
||
console.log(`СОВПАДЕНИЕ С TORUS (wrap, expected): ${torusMatch}/${torusTotal}` +
|
||
` (кейсов с расхождением: ${torusBadCases})`);
|
||
} else {
|
||
console.log('TORUS: reference-torus.json не найден — проверка пропущена');
|
||
}
|
||
const torusOk = !refTorus || (torusMatch === torusTotal && torusBadCases === 0);
|
||
const oursEqualClean = cleanMatch === cleanTotal && torusOk && pageErrors.length === 0;
|
||
console.log(oursEqualClean
|
||
? 'ВЕРДИКТ: наш step == clean (и torus) на всех кейсах. OK'
|
||
: 'ВЕРДИКТ: наш step РАСХОДИТСЯ с clean/torus — это баг новой реализации!');
|
||
process.exit(oursEqualClean ? 0 : 1);
|
||
})().catch(e => { console.error('DIFF CRASH:', e && e.stack || e); process.exit(2); });
|