// Checks every program in index.html against the booklet on all 8 inputs.
// The machine is simulated here independently of the page: a column fires when all three of its
// contacts are closed, a lamp lights when a jumper chain connects it to a firing column.
// Run: node check.mjs (exit code 1 if anything breaks, apart from the two known errata)
import { readFileSync } from 'node:fs';
const html = readFileSync(new URL('./index.html', import.meta.url), 'utf8');
const PROGRAMS = Function('return ' + html.match(/const PROGRAMS = (\[[\s\S]*?\n \]);/)[1])();
const P = Object.fromEntries(PROGRAMS.map(p => [p.id, p]));
// input n = А·4 + Б·2 + В·1; result = V·8 + X·4 + Y·2 + Z·1
function run(p, n) {
const inp = [n >> 2 & 1, n >> 1 & 1, n & 1];
const parent = {};
const find = a => (parent[a] ??= a) === a ? a : (parent[a] = find(parent[a]));
p.wires.forEach(([a, b]) => { parent[find(a)] = find(b); });
const fired = [];
for (let c = 0; c < 7; c++) {
const ok = p.rows.every((row, r) => { const ch = row[c]; return ch === 'B' || ch === (inp[r] ? 'L' : 'R'); });
if (ok) fired.push(find('C' + (c + 1)));
}
return [0, 1, 2, 3].reduce((s, k) => s + (fired.includes(find('L' + k)) ? 8 >> k : 0), 0);
}
const b3 = n => n.toString(2).padStart(3, '0'), b4 = n => n.toString(2).padStart(4, '0');
let failed = 0;
const errata = [];
function expect(id, n, want, why, erratum) {
const got = run(P[id], n);
if (got === want) return;
const line = `${id}: вход ${b3(n)} → ${b4(got)}, ожидалось ${b4(want)}. ${why}`;
if (erratum) errata.push(line); else { failed++; console.log('ОШИБКА ' + line); }
}
const all = f => { for (let n = 0; n < 8; n++) f(n); };
// Game 1: answers to the three questions → the number thought of. Game 9 uses the same figure.
const answers = x => (x % 2 === 0) * 4 + (x + x + 1 < 8) * 2 + [4, 5].includes((x + 10) % 6);
const numberOf = {};
all(x => { numberOf[answers(x)] = x; });
all(x => expect('game1', answers(x), x, `задумано ${x}`));
for (let x = 1; x <= 6; x++) expect('game3', answers(x), x + 7, `на кубике ${x}, плюс противоположные грани 7`);
// Game 2: everyone claims to be honest, so y is honest iff А = 1; z is honest iff В matches y.
all(n => {
const A = n >> 2 & 1, B = n >> 1 & 1, C = n & 1;
const y = A, z = +(C === y), x = z ? B : 1 - B;
expect('game2', n, x * 4 + y * 2 + z, `x=${x} y=${y} z=${z}`);
});
// Games 5 and 6: 24 matches; players 1, 2, 3 took 1, 2, 3 and then the owners of А, Б, В took 1×, 2×, 4× more.
const CODE = { А: 0, Б: 1, В: 2 };
const left = {};
for (const [a, b, c] of [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]) {
const owner = { [a]: 'А', [b]: 'Б', [c]: 'В' };
left[24 - 6 - (a + 2 * b + 4 * c)] = CODE[owner[1]] * 4 + CODE[owner[2]];
}
all(m => expect('game5', m, left[m] ?? 0, `осталось ${m} спичек`));
all(n => expect('game6', n, left[numberOf[n]] ?? 0, `осталось ${numberOf[n]} спичек`));
// Game 9: answer number → comment number, from the booklet's table of answers and comments.
const COMMENT = [0b101, 0b111, 0b011, 0b001, 0b100, 0b110, 0b010, 0b000];
all(n => expect('game9', n, COMMENT[n], 'таблица комментариев'));
// Game 11: Y — motor runs if there is any call, X — door unlocked when it does not,
// Z — up for an up call alone, keep direction А for both calls (no calls: Z does not matter).
all(n => {
const A = n >> 2 & 1, up = n >> 1 & 1, down = n & 1;
const y = up | down, z = up && down ? A : up;
const got = run(P.game11, n);
if (!up && !down) expect('game11', n, (got & 1) | 4, 'лифт стоит, дверь открыта');
else expect('game11', n, (1 - y) * 4 + y * 2 + z, 'лифт');
});
// Game 4: turn the die onto a side face and add it; the sum must not pass 12; whoever reaches
// exactly 12 wins; if nobody can, it is a draw. The machine gets the sum (minus 8 if over 7);
// if the advised face is not on a side, the player takes the nearest smaller side face.
// Solve the game outright (1 win, 0 draw, -1 loss for the player to move) and walk every game
// that can happen: the machine must never get less than the position allows.
const sides = t => [1, 2, 3, 4, 5, 6].filter(f => f !== t && f !== 7 - t);
const memo = {};
const after = (s, f) => s + f === 12 ? 1 : -value(s + f, f);
const value = (s, t) => memo[s + ',' + t] ??= Math.max(0, ...sides(t).filter(f => s + f <= 12).map(f => after(s, f)));
const machineMove = (s, t) => sides(t).filter(f => f <= run(P.game4, s % 8) && s + f <= 12).pop();
const RESULT = { 1: 'выигрыш', 0: 'ничья', '-1': 'проигрыш' };
const seen = new Set();
const machineTurn = (s, t) => {
if (seen.has(s + ',' + t)) return; seen.add(s + ',' + t);
const f = machineMove(s, t), got = f ? after(s, f) : 0;
if (got < value(s, t))
errata.push(`game4: сумма ${s}, сверху ${t}: машина советует ${run(P.game4, s % 8)}, игрок ставит ${f ?? '—'}, ` +
`и вместо «${RESULT[value(s, t)]}» выходит «${RESULT[got]}»`);
if (!f || s + f === 12) return;
for (const g of sides(f)) if (s + f + g < 12) machineTurn(s + f + g, g);
};
for (let t = 1; t <= 6; t++) machineTurn(t, t);
// Game 8: deal 4 birds, 4 beasts, 4 insects into three piles of four every possible way and play
// the booklet's patience. Questions: А — the bottom card at «Насекомые» is an insect,
// Б — the bottom card at «Звери» is a beast, В — one of these two cards is a bird.
// Lamps, as the booklet reads them: V X Y — full success; X — birds and insects; Y — birds and
// beasts; Z — birds, probably the rest too; none — birds only.
const outcomes = {};
(function deal(cards, left) {
if (!left.P && !left.Z && !left.N) {
const piles = { P: cards.slice(0, 4), Z: cards.slice(4, 8), N: cards.slice(8) };
const placed = { P: 0, Z: 0, N: 0 };
for (let cur = 'P'; piles[cur].length;) { const c = piles[cur].shift(); placed[c]++; cur = c; }
const done = 'PZN'.split('').filter(k => placed[k] === 4).join('');
const n = (cards[11] === 'N') * 4 + (cards[7] === 'Z') * 2 + (cards[11] === 'P' || cards[7] === 'P');
(outcomes[n] ??= new Set()).add(done);
return;
}
for (const k of 'PZN') if (left[k]) deal([...cards, k], { ...left, [k]: left[k] - 1 });
})([], { P: 4, Z: 4, N: 4 });
const LAMPS8 = { PZN: 0b1110, PN: 0b0100, PZ: 0b0010, P: 0 };
all(n => {
if (!outcomes[n]) return; // such answers cannot happen
const o = [...outcomes[n]], want = o.length === 1 ? LAMPS8[o[0]] : 0b0001;
expect('game8', n, want, `раскладка кончается так: ${o.join(' или ')}`, true);
});
// Game 7: seven matches into the pot one or two at a time, then back to the table; whoever puts down
// the seventh wins; the opponent starts and the turns alternate throughout. The machine gets the count
// taken (first half) or put back (second half, with 2 entered as 111 and 5 as 110) and shows the count
// to reach. Against every opponent the player following it must always win.
const bad7 = new Set();
const play7 = (half, table, myTurn, path) => {
if (!myTurn) {
for (const n of [1, 2]) {
const src = half === 1 ? table : 7 - table;
if (n > src) continue;
const t = half === 1 ? table - n : table + n;
if (half === 2 && t === 7) { bad7.add(path + ` противник ${n} и выигрывает`); continue; }
play7(half === 1 && t === 0 ? 2 : half, t, true, path + ` противник ${n};`);
}
return;
}
const count = half === 1 ? 7 - table : table;
const input = half === 2 && count === 2 ? 0b111 : half === 2 && count === 5 ? 0b110 : count;
const n = run(P.game7, input) - count;
const src = half === 1 ? table : 7 - table;
if (n < 1 || n > 2 || n > src) { bad7.add(path + ` машина советует ${run(P.game7, input)} при ${count}`); return; }
const t = half === 1 ? table - n : table + n;
if (half === 2 && t === 7) return; // the player puts down the seventh match
play7(half === 1 && t === 0 ? 2 : half, t, false, path + ` вы ${n};`);
};
play7(1, 7, false, '');
for (const s of bad7) { failed++; console.log('ОШИБКА game7:' + s); }
// Logic elements from the Habr article.
all(n => {
const A = n >> 2 & 1, B = n >> 1 & 1, C = n & 1;
expect('not', n, (1 - A) * 4 + (1 - B) * 2 + (1 - C), 'НЕ');
expect('nand', n, +!(A && B && C), 'И-НЕ');
expect('nor', n, +!(A || B || C), 'ИЛИ-НЕ');
expect('xor', n, A ^ B ^ C, 'XOR');
expect('adder', n, (A + B + C > 1) * 8 + ((A + B + C) & 1), 'сумматор');
expect('cipher', n, n ^ 0b100, 'шифр');
});
console.log(errata.length ? 'Ошибки в самом руководстве:\n ' + errata.join('\n ') : 'Ошибок в руководстве не найдено');
console.log(failed ? `Расхождений с руководством: ${failed}` : 'Все остальные проверки прошли');
process.exit(failed ? 1 : 0);