Files
andrey 36f0fa2623 R2-3: полное покрытие новых фич, паритет torus, полировка
- 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 закрыты.
2026-10-11 11:02:23 +03:00

185 lines
8.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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); });