// Автономный harness: ТОЧНЫЙ порт Life::nextStep() из old/life/life.cpp // (Qt-контейнеры заменены на std::vector с той же семантикой). // // Оси: 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 using namespace std; struct Life { int wid = 0, hei = 0; vector> 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(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> 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> 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& 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 genRandom(int w, int h, uint64_t seed, int colors) { LCG r(seed); vector 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 splitLines(const string& s) { vector 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 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 [--proto|--clean|--swap|--torus] if (argc >= 3 && argv[1][0] != '-') { string mode = (argc >= 4) ? argv[3] : "--proto"; ifstream f(argv[1]); string line; vector 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 cases; auto add = [&](string name, vector 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
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 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 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 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 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; }