'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); });