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

496 lines
22 KiB
C++
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.
// Автономный harness: ТОЧНЫЙ порт Life::nextStep() из old/life/life.cpp
// (Qt-контейнеры заменены на std::vector<unsigned char> с той же семантикой).
//
// Оси: field[i][j], i in [0,wid) = X (столбец), j in [0,hei) = Y (строка).
// Подтверждено painter.cpp: drawRect(i*sclx, j*scly) и mouseEvent: field[x*wid][y*hei].
// ASCII: строк текста = hei (Y), символов в строке = wid (X); ascii[j][i] = field[i][j].
//
// Сборка: g++ -std=c++17 -O2 -o old_ref old_ref.cpp
#include <bits/stdc++.h>
using namespace std;
struct Life {
int wid = 0, hei = 0;
vector<vector<unsigned char>> field, tmp;
Life() {}
Life(int w, int h) { setField(w, h); }
void setField(int w, int h) {
wid = w; hei = h;
field.assign(wid, vector<unsigned char>(hei, 0));
tmp = field;
}
// ---- ТОЧНЫЙ порт Life::nextStep() (баг parent[] сохранён как есть) ----
void nextStep() {
int num;
int parent[8];
for (int k = 0; k < 8; k++) parent[k] = 0;
for (int i = 0; i < wid; i++) {
for (int j = 0; j < hei; j++) {
num = 0;
if (i > 0) if (field[i-1][j] > 0) num++, parent[num-1] = field[i-1][j];
if (j > 0) if (field[i][j-1] > 0) num++, parent[num-1] = field[i][j-1];
if (i < wid-1) if (field[i+1][j] > 0) num++, parent[num-1] = field[i+1][j];
if (j < hei-1) if (field[i][j+1] > 0) num++, parent[num-1] = field[i][j+1];
if (i > 0 && j > 0) if (field[i-1][j-1] > 0) num++, parent[num-1] = field[i-1][j-1];
if (i > 0 && j < hei-1) if (field[i-1][j+1] > 0) num++, parent[num-1] = field[i-1][j+1];
if (i < wid-1 && j > 0) if (field[i+1][j-1] > 0) num++, parent[num-1] = field[i+1][j-1];
if (i < wid-1 && j < hei-1) if (field[i+1][j+1] > 0) num++, parent[num-1] = field[i+1][j+1];
if (field[i][j] == 0) {
if (num == 3) {
int firstp = 0, secondp = 0, p = 0;
for (int n = 0; n < 8; n++) {
if (parent[n] > 0) {
if (firstp == 0) firstp = parent[n];
else {
if (secondp == 0) secondp = parent[n];
else {
if (parent[n] == secondp) p = secondp;
else p = firstp;
}
}
}
}
tmp[i][j] = p;
}
} else {
if (num < 2 || num > 3) tmp[i][j] = 0;
else tmp[i][j] = field[i][j];
}
}
}
field = tmp;
}
// ---- ЭТАЛОН "чистое большинство" НА ТОРЕ (wrap) ----
// Соседи за краем берутся с противоположной стороны. Это НОВАЯ опция новой игры
// (в прототипе old заворотов не было), но правило рождения — то же чистое
// «большинство из 3 + tie-break наименьший id». Нужен для независимой проверки wrap.
void nextStepCleanWrap() {
vector<vector<unsigned char>> out = field;
for (int i = 0; i < wid; i++) {
for (int j = 0; j < hei; j++) {
int cnt[4] = {0,0,0,0};
int num = 0;
for (int di = -1; di <= 1; di++) for (int dj = -1; dj <= 1; dj++) {
if (!di && !dj) continue;
int x = ((i + di) % wid + wid) % wid;
int y = ((j + dj) % hei + hei) % hei;
if (field[x][y] > 0) { num++; if (field[x][y] < 4) cnt[field[x][y]]++; }
}
if (field[i][j] == 0) {
if (num == 3) {
int best = -1;
for (int c = 1; c <= 3; c++) if (cnt[c] >= 2) { best = c; break; }
if (best < 0) for (int c = 1; c <= 3; c++) if (cnt[c] == 1) { best = c; break; }
out[i][j] = (unsigned char)(best < 0 ? 0 : best);
} else out[i][j] = 0;
} else {
if (num < 2 || num > 3) out[i][j] = 0;
else out[i][j] = field[i][j];
}
}
}
field = out;
}
// Вариант ровно того же кода, но обход клеток j внешний, i внутренний.
// Нужен только для проверки зависимости от ПОРЯДКА ОБХОДА.
void nextStepOrderSwap() {
int num;
int parent[8];
for (int k = 0; k < 8; k++) parent[k] = 0;
for (int j = 0; j < hei; j++) {
for (int i = 0; i < wid; i++) {
num = 0;
if (i > 0) if (field[i-1][j] > 0) num++, parent[num-1] = field[i-1][j];
if (j > 0) if (field[i][j-1] > 0) num++, parent[num-1] = field[i][j-1];
if (i < wid-1) if (field[i+1][j] > 0) num++, parent[num-1] = field[i+1][j];
if (j < hei-1) if (field[i][j+1] > 0) num++, parent[num-1] = field[i][j+1];
if (i > 0 && j > 0) if (field[i-1][j-1] > 0) num++, parent[num-1] = field[i-1][j-1];
if (i > 0 && j < hei-1) if (field[i-1][j+1] > 0) num++, parent[num-1] = field[i-1][j+1];
if (i < wid-1 && j > 0) if (field[i+1][j-1] > 0) num++, parent[num-1] = field[i+1][j-1];
if (i < wid-1 && j < hei-1) if (field[i+1][j+1] > 0) num++, parent[num-1] = field[i+1][j+1];
if (field[i][j] == 0) {
if (num == 3) {
int firstp = 0, secondp = 0, p = 0;
for (int n = 0; n < 8; n++) {
if (parent[n] > 0) {
if (firstp == 0) firstp = parent[n];
else {
if (secondp == 0) secondp = parent[n];
else {
if (parent[n] == secondp) p = secondp;
else p = firstp;
}
}
}
}
tmp[i][j] = p;
}
} else {
if (num < 2 || num > 3) tmp[i][j] = 0;
else tmp[i][j] = field[i][j];
}
}
}
field = tmp;
}
// ---- ЭТАЛОН "чистое большинство из 3 живых соседей" ----
// Правило: клетка рождается ровно при 3 живых соседях, цвет = цвет, встретившийся >=2 раз;
// если все три разные (1,2,3) -> наименьший id (по AGENTS.md). Выживание/смерть как в Conway.
void nextStepClean() {
vector<vector<unsigned char>> out = field;
for (int i = 0; i < wid; i++) {
for (int j = 0; j < hei; j++) {
int cnt[4] = {0,0,0,0};
int num = 0;
for (int di = -1; di <= 1; di++) for (int dj = -1; dj <= 1; dj++) {
if (!di && !dj) continue;
int x = i + di, y = j + dj;
if (x < 0 || y < 0 || x >= wid || y >= hei) continue;
if (field[x][y] > 0) { num++; if (field[x][y] < 4) cnt[field[x][y]]++; }
}
if (field[i][j] == 0) {
if (num == 3) {
int best = -1;
for (int c = 1; c <= 3; c++) if (cnt[c] >= 2) { best = c; break; }
if (best < 0) for (int c = 1; c <= 3; c++) if (cnt[c] == 1) { best = c; break; } // tie-break: наименьший id
out[i][j] = (unsigned char)(best < 0 ? 0 : best);
} else out[i][j] = 0;
} else {
if (num < 2 || num > 3) out[i][j] = 0;
else out[i][j] = field[i][j];
}
}
}
field = out;
}
};
// ---------- утилиты ASCII ----------
static string fieldToAscii(const Life& L) {
string s;
for (int j = 0; j < L.hei; j++) {
for (int i = 0; i < L.wid; i++) {
unsigned char v = L.field[i][j];
s += (v == 0 ? '.' : (v == 1 ? '1' : (v == 2 ? '2' : (char)('0' + v))));
}
s += '\n';
}
return s;
}
static void loadAscii(Life& L, const vector<string>& lines) {
int h = (int)lines.size();
int w = 0;
for (auto& l : lines) w = max(w, (int)l.size());
L.setField(w, h);
for (int j = 0; j < h; j++) {
for (int i = 0; i < (int)lines[j].size(); i++) {
char c = lines[j][i];
if (c == '.' || c == ' ') L.field[i][j] = 0;
else L.field[i][j] = (unsigned char)(c - '0');
}
}
L.tmp = L.field;
}
static void trimNL(string& s) { while (!s.empty() && (s.back() == '\n')) s.pop_back(); }
// ---------- генератор LCG (детерминированный, без rand()) ----------
struct LCG {
uint64_t s;
LCG(uint64_t seed) : s(seed ? seed : 0x9E3779B97F4A7C15ULL) {}
uint32_t next() { s = s * 6364136223846793005ULL + 1442695040888963407ULL; return (uint32_t)(s >> 33); }
};
static vector<string> genRandom(int w, int h, uint64_t seed, int colors) {
LCG r(seed);
vector<string> lines(h, string(w, '.'));
for (int j = 0; j < h; j++)
for (int i = 0; i < w; i++) {
uint32_t t = r.next() % 100;
if (t < 50) lines[j][i] = '.';
else lines[j][i] = (char)('1' + (r.next() % colors));
}
return lines;
}
static vector<string> splitLines(const string& s) {
vector<string> out; string cur;
for (char c : s) { if (c == '\n') { out.push_back(cur); cur.clear(); } else cur += c; }
if (!cur.empty()) out.push_back(cur);
return out;
}
struct Case {
string name;
vector<string> lines;
int steps;
};
static string jsonEscape(const string& in) {
string o;
for (char c : in) {
if (c == '\n') o += "\\n";
else if (c == '\\') o += "\\\\";
else if (c == '"') o += "\\\"";
else o += c;
}
return o;
}
int main(int argc, char** argv) {
// CLI: old_ref <file.txt> <steps> [--proto|--clean|--swap|--torus]
if (argc >= 3 && argv[1][0] != '-') {
string mode = (argc >= 4) ? argv[3] : "--proto";
ifstream f(argv[1]);
string line; vector<string> lines;
while (getline(f, line)) { if (!line.empty()) lines.push_back(line); }
int steps = atoi(argv[2]);
Life L; loadAscii(L, lines);
cout << fieldToAscii(L);
for (int s = 0; s < steps; s++) {
if (mode == "--clean") L.nextStepClean();
else if (mode == "--torus") L.nextStepCleanWrap();
else if (mode == "--swap") L.nextStepOrderSwap();
else L.nextStep();
cout << "--- step " << (s+1) << " ---\n" << fieldToAscii(L);
}
return 0;
}
// ---------- Батарея тестов ----------
vector<Case> cases;
auto add = [&](string name, vector<string> lines, int steps) { cases.push_back({name, lines, steps}); };
add("empty5x5", splitLines(".....\n.....\n.....\n.....\n....."), 10);
add("block2x2_5x5", splitLines(".....\n.11..\n.11..\n.....\n....."), 10);
add("blinker_h_5x5", splitLines(".....\n.....\n.111.\n.....\n....."), 10);
add("blinker_v_5x5", splitLines(".....\n..1..\n..1..\n..1..\n....."), 10);
add("glider_8x8", splitLines("........\n..1.....\n...1....\n.111....\n........\n........\n........\n........"), 10);
add("single_cell_5x5", splitLines(".....\n.....\n..1..\n.....\n....."), 10);
add("single_cell_6x6", splitLines("......\n......\n...1..\n......\n......\n......"), 10);
// Смешанные цвета: ключевые случаи наследования
// P1 (цвет 1) в форме блока вокруг клетки рождения, один сосед цвета 2.
add("mix_2_1_5x5", splitLines(".....\n.111.\n.121.\n.111.\n....."), 10);
add("mix_row_121_5x5", splitLines(".....\n.....\n121..\n.....\n....."), 10);
// Наследование при рождении: изолированные 3 соседа вокруг пустой клетки (2,2)
// 112 -> 1; 122 -> 2; 121 -> 1; 221 -> 2
add("birth_112_5x5", splitLines(".....\n.11..\n.2...\n.....\n....."), 10);
add("birth_122_5x5", splitLines(".....\n.12..\n.2...\n.....\n....."), 10);
add("birth_121_5x5", splitLines(".....\n.12..\n.1...\n.....\n....."), 10);
add("birth_221_5x5", splitLines(".....\n.22..\n.1...\n.....\n....."), 10);
// Минимальный контрпример бага parent[] (3x3, расходится с чистым большинством на шаге 1)
add("cc_3x3_stale", splitLines("..2\n2.1\n1.."), 3);
// Несколько рождений за один шаг в разных местах (проверка порядка обхода)
add("multi_births_6x6", splitLines("......\n.1.1..\n..1...\n...1.1\n....1.\n......"), 10);
// Спец-случай: сначала клетка с 4+ соседями (заполнит parent[]), затем рождение
add("stale_split_6x6", splitLines("......\n.1111.\n.1.1..\n...1..\n......\n......"), 10);
// Детерминированные "плотные" случайные поля разных размеров/соотношений
for (int sz : {5, 6, 8, 10, 12}) {
add("rnd_" + to_string(sz) + "x" + to_string(sz) + "_s1", genRandom(sz, sz, 0x1234u + sz, 2), 10);
add("rnd_" + to_string(sz) + "x" + to_string(sz) + "_s2", genRandom(sz, sz, 0xABCDu + sz, 2), 10);
}
// Только цвет 1 (проверка чистого Conway без цвета)
add("rnd_8x8_onecolor", genRandom(8, 8, 0x55u, 1), 10);
// Только цвет 2
{
auto g = genRandom(8, 8, 0x77u, 1);
for (auto& l : g) for (auto& c : l) if (c == '1') c = '2';
add("rnd_8x8_all2", g, 10);
}
// ---------- Прогон + запись эталона ----------
// Файлы пишутся в текущий каталог (запуск, как правило, из tools/old-ref).
string json = "[\n";
bool firstCase = true;
// для анализа
struct Div { string name; int step; string got, want; };
vector<Div> divsClean, divsSwap;
for (auto& c : cases) {
Life P; loadAscii(P, c.lines);
Life Cl; loadAscii(Cl, c.lines);
Life Sw; loadAscii(Sw, c.lines);
vector<string> protoSeq, cleanSeq, swapSeq;
protoSeq.push_back(fieldToAscii(P));
cleanSeq.push_back(fieldToAscii(Cl));
swapSeq.push_back(fieldToAscii(Sw));
for (int s = 0; s < c.steps; s++) {
P.nextStep();
Cl.nextStepClean();
Sw.nextStepOrderSwap();
protoSeq.push_back(fieldToAscii(P));
cleanSeq.push_back(fieldToAscii(Cl));
swapSeq.push_back(fieldToAscii(Sw));
}
// ref-файл (все шаги, включая шаг 0)
{
ofstream rf("ref-" + c.name + ".txt");
for (size_t s = 0; s < protoSeq.size(); s++) {
rf << "=== step " << s << " ===\n" << protoSeq[s];
}
}
// сравнение prototype vs clean / swap
{
Life PC; loadAscii(PC, c.lines);
Life PS; loadAscii(PS, c.lines);
for (size_t s = 1; s < protoSeq.size(); s++) {
if (divsClean.empty() || true) {
// отмечаем первое расхождение на каждом шаге отдельно
}
if (cleanSeq[s] != protoSeq[s]) {
divsClean.push_back({c.name, (int)s, protoSeq[s], cleanSeq[s]});
}
if (swapSeq[s] != protoSeq[s]) {
divsSwap.push_back({c.name, (int)s, protoSeq[s], swapSeq[s]});
}
}
}
// JSON
json += string(firstCase ? "" : ",\n") + " {\"name\":\"" + c.name + "\",\"w\":" + to_string(P.wid) +
",\"h\":" + to_string(P.hei) + ",\"steps\":[";
for (size_t s = 0; s < protoSeq.size(); s++) {
json += (s ? "," : "") + string("\"") + jsonEscape(protoSeq[s]) + "\"";
}
json += "]}";
firstCase = false;
}
json += "\n]\n";
{
ofstream jf("reference.json");
jf << json;
}
// ---------- Поиск минимального контрпримера (prototype != clean) ----------
// Перебор всех полей 3x3 с цветами {0,1,2}: 3^9 = 19683.
string bestCounter;
int bestLive = 99;
for (int code = 0; code < 19683; code++) {
int t = code;
vector<string> lines(3, string(3, '.'));
for (int k = 0; k < 9; k++) {
int v = t % 3; t /= 3;
lines[k / 3][k % 3] = (v == 0 ? '.' : (char)('0' + v));
}
Life A; loadAscii(A, lines);
Life B; loadAscii(B, lines);
A.nextStep(); B.nextStepClean();
if (fieldToAscii(A) != fieldToAscii(B)) {
int live = 0;
for (auto& l : lines) for (char c : l) if (c != '.') live++;
if (live < bestLive) {
bestLive = live;
bestCounter = lines[0] + "\n" + lines[1] + "\n" + lines[2] + "\n";
}
}
}
// Также перебор 4x4? 3^16 = 43M — дорого, пропускаем; случайный поиск 4x4/5x5.
string rndCounter;
{
LCG r(0xC0FFEEu);
for (int iter = 0; iter < 300000; iter++) {
int w = 4 + (r.next() % 3), h = 4 + (r.next() % 3);
vector<string> lines(h, string(w, '.'));
for (int j = 0; j < h; j++) for (int i = 0; i < w; i++) {
uint32_t t = r.next() % 100;
lines[j][i] = (t < 55 ? '.' : (char)('1' + r.next() % 2));
}
Life A; loadAscii(A, lines);
Life B; loadAscii(B, lines);
A.nextStep(); B.nextStepClean();
if (fieldToAscii(A) != fieldToAscii(B)) {
int live = 0; string s;
for (auto& l : lines) { s += l + "\n"; for (char c : l) if (c != '.') live++; }
if (rndCounter.empty()) rndCounter = s + "// live=" + to_string(live) + "\n";
}
}
}
// ---------- Проверка "протечки цвета": рождённый цвет всегда среди соседей? ----------
long long leakCount = 0, birthCount = 0;
{
LCG lk(0xBEEF1234u);
for (int iter = 0; iter < 300000; iter++) {
int w = 4 + (lk.next() % 9), h = 4 + (lk.next() % 9);
vector<string> lines(h, string(w, '.'));
for (int j = 0; j < h; j++) for (int i = 0; i < w; i++) {
uint32_t t = lk.next() % 100;
lines[j][i] = (t < 50 ? '.' : (char)('1' + lk.next() % 2));
}
Life A; loadAscii(A, lines);
auto before = A.field;
A.nextStep();
for (int i = 0; i < w; i++) for (int j = 0; j < h; j++) {
if (before[i][j] == 0 && A.field[i][j] != 0) {
birthCount++;
bool among = false;
for (int di = -1; di <= 1; di++) for (int dj = -1; dj <= 1; dj++) {
if (!di && !dj) continue;
int x = i + di, y = j + dj;
if (x < 0 || y < 0 || x >= w || y >= h) continue;
if (before[x][y] == A.field[i][j]) among = true;
}
if (!among) leakCount++;
}
}
}
}
// ---------- Отчёт в stdout ----------
cout << "===== HARNESS: эталон записан =====\n";
cout << "Cases: " << cases.size() << "\n";
cout << "reference.json: ./reference.json\n";
cout << "ref-*.txt: " << cases.size() << " файлов\n\n";
cout << "===== prototype vs CLEAN majority (расхождения) =====\n";
if (divsClean.empty()) cout << "нет расхождений ни на одном шаге\n";
else {
cout << "всего расхождений (шаг/кейс): " << divsClean.size() << "\n";
for (size_t i = 0; i < divsClean.size() && i < 40; i++) {
cout << "[" << i << "] case=" << divsClean[i].name << " step=" << divsClean[i].step << "\n";
cout << " proto:\n" << divsClean[i].got;
cout << " clean:\n" << divsClean[i].want;
}
}
cout << "\n===== prototype vs ORDER-SWAPPED (расхождения) =====\n";
if (divsSwap.empty()) cout << "нет расхождений — результат не зависит от порядка обхода на этой батарее\n";
else {
cout << "всего расхождений: " << divsSwap.size() << "\n";
for (size_t i = 0; i < divsSwap.size() && i < 20; i++) {
cout << "[" << i << "] case=" << divsSwap[i].name << " step=" << divsSwap[i].step << "\n";
cout << " x-major(proto):\n" << divsSwap[i].got;
cout << " y-major(swap):\n" << divsSwap[i].want;
}
}
cout << "\n===== Минимальный контрпример (proto != clean) в 3x3 =====\n";
if (bestCounter.empty()) cout << "в 3x3 не найдено\n";
else cout << "live=" << bestLive << "\n" << bestCounter;
cout << "\n===== Проверка протечки цвета (300000 случайных полей) =====\n";
cout << "рождений всего: " << birthCount << ", цвет НЕ среди соседей: " << leakCount << "\n";
cout << "\n===== Первый случайный контрпример 4..6 =====\n";
if (rndCounter.empty()) cout << "не найдено\n";
else cout << rndCounter;
return 0;
}