ДЛМ: оцифрованное руководство 1987 года, эмулятор и проверка программ

Эмулятор в одном файле index.html, руководство в PDF (сканы, набранный
текст, расшифровка рисунков программ). check.mjs сверяет все программы
с руководством на всех входах, игру 4 решает перебором партий, игру 8
проверяет перебором раскладок карточек.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alex Cube
2026-10-27 18:56:24 +03:00
co-authored by Claude Opus 5.5
commit 7bfe0eb0d7
4 changed files with 1067 additions and 0 deletions
Binary file not shown.
+135
View File
@@ -0,0 +1,135 @@
// 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);
});
// 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);
+865
View File
@@ -0,0 +1,865 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Детская логическая машина</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Rubik:wght@500;700;800&family=PT+Serif:ital,wght@0,400;0,700;1,400&family=PT+Sans:wght@400;700&family=PT+Mono&display=swap">
<style>
/* Layout: the machine as drawn in the 1987 booklet (left) and its insides as a live circuit (right); programs and answer table below; the booklet's games at the end. */
:root {
--bg: #e8ecf2;
--surface: #f7f8fb;
--ink: #18223b;
--muted: #5a6479;
--line: #c9d0dc;
--accent: #c8222c;
--accent-ink: #ffffff;
--blue: #23409a;
--hl: #fde9a8;
--font-display: "Rubik", "Arial Rounded MT Bold", system-ui, sans-serif;
--font-body: "PT Sans", "Segoe UI", system-ui, sans-serif;
--font-book: "PT Serif", Georgia, "Times New Roman", serif;
--font-mono: "PT Mono", ui-monospace, Consolas, monospace;
/* the inside view is a fixed blueprint, the same in both themes */
--bp-bg: #0f2348; --bp-grid: #1b3463; --bp-ink: #9fb6e6; --bp-dim: #50678f; --bp-live: #ffd166; --bp-flow: #ff8c42;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0f1730; --surface: #17223f; --ink: #e6eaf4; --muted: #9ba6c0; --line: #2c3a5e;
--accent: #ff5a5f; --accent-ink: #1a0b0c; --blue: #8fa8ff; --hl: #4a3f17;
color-scheme: dark;
}
}
:root[data-theme="dark"] {
--bg: #0f1730; --surface: #17223f; --ink: #e6eaf4; --muted: #9ba6c0; --line: #2c3a5e;
--accent: #ff5a5f; --accent-ink: #1a0b0c; --blue: #8fa8ff; --hl: #4a3f17;
color-scheme: dark;
}
* { box-sizing: border-box; }
html { padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); }
body { margin: 0; }
img { max-width: 100%; }
[hidden] { display: none !important; }
body {
background: var(--bg); color: var(--ink); font-family: var(--font-body); font-size: 16px; line-height: 1.5;
padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px));
padding-block: 28px 64px;
}
.wrap { max-width: 1400px; margin: 0 auto; display: grid; gap: 36px; }
header { display: flex; flex-wrap: wrap; align-items: center; gap: 16px 22px; }
.logo { width: 64px; height: 64px; border-radius: 50%; background: var(--accent); color: #fff; display: grid; place-items: center;
font-family: var(--font-display); font-weight: 800; font-size: 22px; box-shadow: 0 0 0 4px var(--bg), 0 0 0 7px var(--accent); flex: none; }
h1 { font-family: var(--font-display); font-weight: 800; font-size: clamp(28px, 4.4vw, 46px); line-height: 1.02; margin: 0; text-transform: lowercase; color: var(--blue); text-wrap: balance; }
.sub { margin: 4px 0 0; color: var(--muted); max-width: 70ch; }
.bench { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; align-items: start; }
@media (max-width: 1100px) { .bench { grid-template-columns: minmax(0, 1fr); } }
.pane { display: grid; gap: 10px; min-width: 0; }
.pane-head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 6px 16px; }
.pane h2, .lower h2, .book h2 { font-family: var(--font-display); font-weight: 700; font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin: 0; }
svg.view { width: 100%; height: auto; display: block; user-select: none; -webkit-user-select: none; touch-action: manipulation; }
svg#dlm { filter: drop-shadow(0 14px 22px rgba(10, 20, 50, .22)); }
svg#inside { border-radius: 14px; background: var(--bp-bg); }
svg .hit { cursor: pointer; }
svg .hit:focus { outline: none; }
svg .hit:focus-visible .focusring { stroke: var(--blue); stroke-width: 4; }
.status { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center; font-size: 15px; min-height: 2.4em; }
.readout { font-family: var(--font-mono); font-size: 17px; padding: 6px 12px; border-radius: 8px; background: var(--surface); border: 1px solid var(--line); font-variant-numeric: tabular-nums; }
.readout b { color: var(--accent); }
.hint { color: var(--muted); }
.warn { color: var(--accent); font-weight: 700; }
.controls { display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; font-size: 15px; }
.controls label { display: inline-flex; gap: 8px; align-items: center; cursor: pointer; }
.chip { font-family: var(--font-mono); font-size: 14px; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--line); background: var(--surface); font-variant-numeric: tabular-nums; }
.chip.over { border-color: var(--accent); color: var(--accent); }
.ghost { font: inherit; font-size: 14px; padding: 4px 12px; border-radius: 999px; border: 1px solid var(--line); background: var(--surface); color: var(--ink); cursor: pointer; }
.ghost:hover { border-color: var(--blue); }
.ghost:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
.legend { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 14px; color: var(--muted); }
.legend span { display: inline-flex; align-items: center; gap: 8px; }
.legend i { display: inline-block; width: 26px; height: 0; border-top: 4px solid; }
.legend .l-flow { border-color: var(--bp-flow); border-top-style: dashed; }
.legend .l-live { border-color: var(--bp-live); border-top-width: 2px; }
.legend .l-dead { border-color: var(--bp-dim); border-top-width: 2px; }
.explain { font-size: 14px; color: var(--muted); margin: 0; max-width: 75ch; }
#inside .w { fill: none; stroke-linecap: round; stroke-linejoin: round; }
#inside .dead { stroke: var(--bp-dim); stroke-width: 2; }
#inside .live { stroke: var(--bp-live); stroke-width: 2.5; }
#inside .flow { stroke: var(--bp-flow); stroke-width: 4.5; stroke-dasharray: 11 7; animation: run .55s linear infinite; }
@keyframes run { to { stroke-dashoffset: -18; } }
@media (prefers-reduced-motion: reduce) { #inside .flow { animation: none; stroke-dasharray: none; } * { transition: none !important; } }
.lower { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) minmax(0, 1fr); gap: 24px; align-items: start; }
@media (max-width: 1100px) { .lower { grid-template-columns: minmax(0, 1fr); } }
.lower > div { display: grid; gap: 10px; min-width: 0; }
.programs { display: grid; gap: 6px; }
.prog { display: grid; grid-template-columns: 1fr auto; gap: 2px 10px; text-align: left; width: 100%; padding: 9px 12px; border-radius: 10px;
border: 1px solid var(--line); background: var(--surface); color: var(--ink); font: inherit; cursor: pointer; }
.prog:hover { border-color: var(--blue); }
.prog:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
.prog[aria-pressed="true"] { border-color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }
.prog .t { font-weight: 700; }
.prog .src { font-size: 12px; color: var(--muted); align-self: center; white-space: nowrap; }
.prog .d { grid-column: 1 / -1; font-size: 13px; color: var(--muted); }
.group-label { font-size: 12px; color: var(--muted); margin: 8px 0 2px; }
.tt-wrap { overflow-x: auto; }
table.tt { border-collapse: collapse; font-family: var(--font-mono); font-size: 14px; width: 100%; font-variant-numeric: tabular-nums; }
.tt th, .tt td { padding: 4px 8px; text-align: center; border-bottom: 1px solid var(--line); }
.tt th { font-weight: 400; color: var(--muted); font-size: 12px; }
.tt tr.cur td { background: var(--hl); }
.tt tbody tr { cursor: pointer; }
.tt .sep { border-left: 1px solid var(--line); }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; border: 1.5px solid var(--muted); vertical-align: -1px; }
.dot.on.red { background: #ff4b3e; border-color: #ff4b3e; box-shadow: 0 0 6px #ff4b3e; }
.dot.on.green { background: #3fdc6a; border-color: #2bb552; box-shadow: 0 0 6px #3fdc6a; }
.exprs { list-style: none; margin: 0; padding: 0; font-family: var(--font-mono); font-size: 14px; display: grid; gap: 3px; }
.exprs li { display: grid; grid-template-columns: 2.2em minmax(0, 1fr) auto; gap: 8px; }
.exprs .n, .exprs .to { color: var(--muted); }
.exprs .dead { color: var(--muted); font-style: italic; font-family: var(--font-body); }
.book { background: var(--surface); border: 1px solid var(--line); border-radius: 6px; padding-block: 36px 40px; padding-inline: clamp(18px, 5vw, 64px); display: grid; gap: 34px; }
.book-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px 24px; align-items: baseline; }
.book-head p { margin: 0; color: var(--muted); max-width: 60ch; }
.page { font-family: var(--font-book); max-width: 68ch; display: grid; gap: 12px; }
.page h3 { font-family: var(--font-book); font-size: 20px; margin: 0; letter-spacing: .04em; }
.page h3 .gn { letter-spacing: .3em; font-weight: 400; }
.page p { margin: 0; text-indent: 1.6em; hyphens: auto; }
.page p.noind { text-indent: 0; }
.note { font-family: var(--font-body); font-size: 14px; color: var(--muted); border-left: 3px solid var(--line); padding-left: 12px; max-width: 68ch; margin: 0; }
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; border-radius: 999px; background: var(--accent); color: var(--accent-ink); border: 0; font: 700 14px var(--font-body); cursor: pointer; justify-self: start; }
.btn:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
.sources { font-size: 14px; color: var(--muted); }
.sources a { color: var(--blue); }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="logo" aria-hidden="true">длм</div>
<div>
<h1>детская логическая машина</h1>
<p class="sub">Советская программируемая игрушка 1984–1989 годов, в которой нет ни транзистора, ни реле: только планки, штеккеры, пружинные контакты и лампочки. Справа видно, как поворот переключателя двигает планку, штеккеры нажимают контакты и куда идёт ток. Все 11 игр из руководства 1987 года набираются одной кнопкой.</p>
</div>
</header>
<section class="bench">
<div class="pane">
<div class="pane-head"><h2>Пульт</h2><span id="pincount" class="chip">штеккеров: 0 / 30</span></div>
<svg id="dlm" class="view" viewBox="0 0 1000 700" role="application" aria-label="Пульт Детской логической машины"></svg>
<div class="status" aria-live="polite">
<span id="readout" class="readout" hidden></span>
<span id="hint" class="hint"></span>
</div>
<div class="controls">
<label><input type="checkbox" id="latch"> Держать кнопку нажатой</label>
<button class="ghost" type="button" id="mid">Переключатели в среднее положение</button>
<button class="ghost" type="button" id="clear">Снять всё</button>
</div>
<p class="explain">Гнездо: вставить или вынуть штеккер. Пружина, затем вторая пружина: поставить перемычку. Перемычка: снять. Переключатель: щелчок по ручке или по «0» и «1».</p>
</div>
<div class="pane">
<div class="pane-head"><h2>Вид изнутри</h2>
<div class="legend"><span><i class="l-flow"></i>ток</span><span><i class="l-live"></i>напряжение, тока нет</span><span><i class="l-dead"></i>обесточено</span></div>
</div>
<svg id="inside" class="view" viewBox="0 0 1000 720" role="img" aria-label="Электрическая схема ДЛМ с текущим состоянием контактов"></svg>
<p class="explain">Переключатель — это кривошип: поворот сдвигает планку. Под каждым столбцом, под каждой планкой, стоит пружинящая пластина, по бокам от неё два неподвижных контакта. При «1» планка уходит вправо, и штеккер из левого гнезда отгибает пластину на правый контакт. При «0» штеккер из правого гнезда отгибает её на левый. В среднем положении пластины никто не касается. Каждый столбец — три такие пластины подряд, по одной под каждой планкой, то есть логическое И. Перемычки собирают несколько столбцов на одну лампу — это ИЛИ. Вот и весь «процессор»: программа — это расстановка штеккеров и перемычек. <em>Механизм под крышкой — реконструкция: в брошюре его нет, фотографий тоже не нашлось.</em></p>
</div>
</section>
<section class="lower">
<div>
<h2>Программы</h2>
<div class="programs" id="programs"></div>
</div>
<div>
<h2>Что ответит машина</h2>
<div class="tt-wrap"><table class="tt" id="tt"></table></div>
</div>
<div>
<h2>Столбцы программирования</h2>
<ul class="exprs" id="exprs"></ul>
</div>
</section>
<article class="book" id="book">
<div class="book-head">
<h2>Игры из руководства 1987 года</h2>
<p>Правила сокращены по оригинальной брошюре. Кнопка под каждой игрой набирает программу с её рисунка: штеккеры и перемычки ставятся точно как на рисунке.</p>
</div>
<section class="page">
<h3>Как читать ответ</h3>
<p>Ввод — переключатели А (4), Б (2), В (1): числа от 0 до 7. Вывод — лампы V (8), X (4), Y (2), Z (1): горящая лампа означает «1», «Да», «Правильно». Сумма чисел над горящими лампами даёт ответ от 0 до 15.</p>
<p>Штеккер в левом гнезде замыкает звено столбца, когда переключатель стоит на «1», в правом — на «0». Столбец срабатывает, если замкнуты все три его звена. Перемычки соединяют столбцы с лампами.</p>
</section>
<section class="page" id="game1">
<h3><span class="gn">Игра 1.</span> Какое число задумано?</h3>
<p>Играющий задумывает число от 0 до 7 и отвечает на три вопроса. «Да» — переключатель в «1», «Нет» — в «0».</p>
<p class="noind">А: Задуманное число чётное? (Ноль — чётное.)<br>Б: Сумма задуманного числа и следующего за ним меньше восьми?<br>В: К задуманному числу прибавьте 10 и разделите на 6. Остаток 4 или 5?</p>
<p>Лампы X, Y, Z показывают задуманное число. Пример: для 5 все ответы «Нет», горят X и Z: 101 = 5.</p>
<button class="btn" type="button" data-load="game1">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game2">
<h3><span class="gn">Игра 2.</span> Честные и лгуны</h3>
<p>В стране живут честные (всегда говорят правду) и лгуны (всегда лгут). На вопрос «Вы честный или лгун?» житель x отвечает на непонятном языке.</p>
<p class="noind">А: житель y: «Он сказал, что он честный» — «1», «…что он лгун» — «0».<br>Б: житель z о жителе x: «честный» — «1», «лгун» — «0».<br>В: житель z о жителе y: «честный» — «1», «лгун» — «0».</p>
<p>Горящие X, Y, Z означают, что x, y, z честные. Пример: Дима (x) честный, Витя (y) и Коля (z) лгуны. Ответы 0, 0, 1 — горит только X.</p>
<button class="btn" type="button" data-load="game2">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game3">
<h3><span class="gn">Игра 3.</span> Сумма трёх чисел</h3>
<p>Играющий тайком бросает кубик и запоминает число. Его ответы на вопросы игры 1 фиксируются планками А, Б, В. Затем он бросает ещё раз, запоминает верхнее число и число на противоположной грани и складывает все три.</p>
<p>Нажмите кнопку: лампы покажут сумму. Пример: 5, затем 4 и 3, сумма 12. Ответы «Нет, Нет, Нет», горят V и X: 1100 = 12.</p>
<button class="btn" type="button" data-load="game3">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game4">
<h3><span class="gn">Игра 4.</span> Двенадцать — выигрыш!</h3>
<p>Противник бросает кубик первым. Дальше игроки по очереди поворачивают кубик на одну из боковых граней и прибавляют её число к сумме. Кто первым наберёт ровно 12, тот выиграл.</p>
<p>Введите текущую сумму (если она больше 7, вычтите 8). Лампы X, Y, Z покажут число, которое нужно поставить наверх. Если такой грани сбоку нет, берите ближайшее меньшее. Пример: выпало 2, машина говорит 2, но двойка сверху, ставим 1 (сумма 3). Противник делает 5, машина 4 (сумма 9). Противник делает 10, вводим 2, машина 2, сумма 12.</p>
<button class="btn" type="button" data-load="game4">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game5">
<h3><span class="gn">Игра 5.</span> Какой предмет выбран?</h3>
<p>Возьмите 24 спички. Игроки № 1, 2, 3 берут 1, 2 и 3 спички и тайком делят фишки А, Б, В. Затем владелец А добирает столько же, сколько взял, владелец Б — вдвое больше, владелец В — вчетверо.</p>
<p>Введите число оставшихся спичек. Пара V X — игрок № 1, пара Y Z — игрок № 2. Код: 00 — фишка А, 01 — Б, 10 — В. Если все лампы погашены, правила нарушены. Пример: осталось 5, горят X и Y. Игрок № 1 — Б, игрок № 2 — В, игроку № 3 досталась А.</p>
<button class="btn" type="button" data-load="game5">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game6">
<h3><span class="gn">Игра 6.</span> ДЛМ превосходит себя!</h3>
<p>Всё как в игре 5, но оператор не узнаёт, сколько спичек осталось. Он задаёт одному из игроков вопросы игры 1 про это число и читает ответ по коду игры 5.</p>
<button class="btn" type="button" data-load="game6">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game7">
<h3><span class="gn">Игра 7.</span> Семь спичек</h3>
<p>Двое по очереди берут со стола одну или две спички в «общий котёл», пока стол не опустеет. Потом в том же порядке возвращают по одной-две. Выигрывает тот, кто положит седьмую. Противник ходит первым.</p>
<p>Вводите число спичек, уже взятых (во второй половине — уже положенных). Лампы покажут, сколько их должно стать после вашего хода. Во второй половине вместо 2 вводите 111, вместо 5 — 110.</p>
<button class="btn" type="button" data-load="game7">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game8">
<h3><span class="gn">Игра 8.</span> Зоологические карточки</h3>
<p>Двенадцать карточек (по 4 птицы, зверя, насекомых) лежат тремя стопками рубашкой вверх, возле стопок в любом порядке — надписи. Берите верхнюю карточку стопки «Птицы» и кладите к нужной надписи. Затем берите карточку из той стопки, куда положили, и так далее.</p>
<p class="noind">А: нижняя карточка у надписи «Насекомые» — насекомое?<br>Б: нижняя карточка у надписи «Звери» — зверь?<br>В: среди двух просмотренных карточек есть птица?</p>
<p>V, X, Y — возможен полный успех. Только X — сойдутся птицы или насекомые. Только Y — птицы или звери. Только Z — птицы, вероятно, и остальные. Ничего не горит — сойдутся только птицы.</p>
<button class="btn" type="button" data-load="game8">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game9">
<h3><span class="gn">Игра 9.</span> ДЛМ — экзаменатор</h3>
<p>Введите номер выбранного ответа. Лампы X, Y, Z покажут номер комментария.</p>
<p class="noind"><b>1.</b> Основные блоки ВМ? 000 — пульт, считыватель перфокарт, магнитофон, печатающее устройство; 001 — центральный блок (память и обработка), входные и выходные устройства.<br>
<b>2.</b> Как ВМ обрабатывает информацию? 010 — нужна программа вычислений; 011 — центральный блок сам решает любую задачу.<br>
<b>3.</b> В каком виде идёт обработка? 100 — в двоичном коде; 101 — на языке программирования.<br>
<b>4.</b> Почему ВМ и считает, и выполняет логические операции? 110 — она обрабатывает аналоговые данные; 111 — она построена на логических схемах, имитирующих алгебру Буля.</p>
<p class="noind">Комментарии: 000 — Да (алгебра Буля); 001 — Нет (нужна программа); 010 — Нет (обработка цифровая); 011 — Да (ВМ работает по программе); 100 — Да (двоичный код); 101 — Нет (это внешние устройства); 110 — Нет (язык программирования нужен для общения с машиной); 111 — Да (центральный блок и внешние устройства).</p>
<button class="btn" type="button" data-load="game9">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game10">
<h3><span class="gn">Игра 10.</span> Ваш характер</h3>
<p>Четыре группы по три вопроса. В каждой группе ответьте «Да» или «Нет» переключателями А, Б, В и нажмите кнопку. Если горит Z, вставьте штеккер в правое гнездо планки А в столбце с номером группы (1–4). Когда все группы пройдены, вставьте штеккеры в правые гнёзда столбцов 1–4 планки Б, поставьте переключатели на 0 и нажмите кнопку.</p>
<p class="noind">I. Можешь ли ты говорить неправду? Миришься ли ты со своими недостатками? Легко ли ты изменяешь своё мнение?<br>
II. Часто ли ты говоришь о недостатках других людей? Свободно ли ты чувствуешь себя в незнакомой компании? Доверяешь ли ты людям?<br>
III. Нравится ли тебе, когда тебя хвалят? Доволен ли ты собой? Любишь ли ты говорить о себе?<br>
IV. Доводишь ли ты начатое дело до конца? Настойчиво ли ты добиваешься своей цели? Проявляешь ли ты инициативу в работе?</p>
<p class="note">Шестнадцать характеристик, от «0000 — замкнутый, скромный, целеустремлённый, но ленивый» до «1111 — деятельный, откровенный, но беспринципный и себялюбивый», приведены в полном тексте руководства.</p>
<button class="btn" type="button" data-load="game10">Набрать программу на машине ↑</button>
</section>
<section class="page" id="game11">
<h3><span class="gn">Игра 11.</span> Автоматическое управление лифтом</h3>
<p>А — направление до вызова (1 — вверх). Б — вызов вверх. В — вызов вниз.</p>
<p>X — дверь разблокирована. Y — мотор работает. Z — лифт поедет вверх. При двух вызовах сразу лифт сохраняет прежнее направление.</p>
<button class="btn" type="button" data-load="game11">Набрать программу на машине ↑</button>
</section>
<section class="sources">
<div>Источник: «Игрушка Детская логическая машина ДЛМ. Руководство по эксплуатации». ВНИИ «Электронстандарт», Гатчина, 1987. Логические элементы взяты из статьи <a href="https://habr.com/ru/articles/807187/" target="_blank" rel="noopener">«Криптография и другие вычисления на детских логических машинах»</a> (Хабр).</div>
</section>
</article>
</div>
<script>
(() => {
const NS = 'http://www.w3.org/2000/svg';
const ROWS = ['А', 'Б', 'В'];
const LAMPS = [
{ name: 'V', w: 8, color: 'red' }, { name: 'X', w: 4, color: 'green' },
{ name: 'Y', w: 2, color: 'green' }, { name: 'Z', w: 1, color: 'green' }
];
const MAX_PINS = 30;
// Programs. Rows А, Б, В; per column: L = left hole (closes at «1»), R = right hole (closes at «0»), B = both, . = none.
// Wires: L0..L3 are lamp springs V, X, Y, Z; C1..C7 are column springs.
const PROGRAMS = [
{ id: 'game1', group: 'Игры из инструкции', t: 'Игра 1. Какое число задумано?', src: 'рис. 3',
d: "Три вопроса — и машина называет задуманное число 0…7.",
rows: [".B.BBR.", ".R.LRB.", ".B.RLB."], wires: [["L1", "C2"], ["L2", "C4"], ["L2", "C5"], ["L3", "C6"]] },
{ id: 'game2', group: 'Игры из инструкции', t: 'Игра 2. Честные и лгуны', src: 'рис. 4',
d: "Кто из троих жителей честный, а кто лгун. X, Y, Z = x, y, z.",
rows: ["RRLLLRL", "RLRLBBB", "LRRLBRL"], wires: [["C1", "C2"], ["C2", "L1"], ["L1", "C3"], ["L1", "C4"], ["L2", "C5"], ["C6", "L3"], ["L3", "C7"]] },
{ id: 'game3', group: 'Игры из инструкции', t: 'Игра 3. Сумма трех чисел', src: 'рис. 5',
d: "Сумма трёх чисел на кубике, 8…13.",
rows: ["BRLRLLL", "BRRLRBR", "BRLRRRB"], wires: [["L0", "C1"], ["C2", "L1"], ["L1", "C3"], ["C4", "L2"], ["L2", "C5"], ["C6", "L3"], ["L3", "C7"]] },
{ id: 'game4', group: 'Игры из инструкции', t: 'Игра 4. Двенадцать — выигрыш!', src: 'рис. 6',
d: "Подсказывает ход, чтобы первым набрать 12.",
rows: ["RLRBLRB", "RBRLBBL", "RLLRRLL"], wires: [["C1", "C2"], ["C2", "L1"], ["C3", "C4"], ["C4", "L2"], ["C5", "C6"], ["C6", "L3"], ["L3", "C7"]] },
{ id: 'game5', group: 'Игры из инструкции', t: 'Игра 5. Какой предмет выбран?', src: 'рис. 7',
d: "Угадывает, кому из трёх игроков досталась какая фишка (по спичкам).",
rows: ["LRLRLRL", "LLRLRRL", "BRLLLLL"], wires: [["L0", "C1"], ["C2", "L1"], ["L1", "C3"], ["C4", "L2"], ["L2", "C5"], ["C6", "L3"], ["L3", "C7"]] },
{ id: 'game6', group: 'Игры из инструкции', t: 'Игра 6. ДЛМ превосходит себя!', src: 'рис. 8',
d: "То же, что игра 5, но по ответам на вопросы игры 1.",
rows: ["BLR.RR.", "RLR.BB.", "LRR.RL."], wires: [["L0", "C1"], ["C2", "L1"], ["L1", "C3"], ["L2", "C5"], ["C6", "L3"]] },
{ id: 'game7', group: 'Игры из инструкции', t: 'Игра 7. Семь спичек', src: 'рис. 9',
d: "Беспроигрышная стратегия в игре «Семь спичек».",
rows: ["LBBLBRB", "BLRBLRL", "BLLRRBR"], wires: [["C1", "C2"], ["C2", "L1"], ["C3", "C4"], ["C4", "L2"], ["L2", "C5"], ["C6", "L3"], ["L3", "C7"]] },
{ id: 'game8', group: 'Игры из инструкции', t: 'Игра 8. Зоологические карточки', src: 'рис. 10',
d: "Предсказывает исход раскладки зоологических карточек.",
rows: ["RR.R.RL", "RB.R.BR", "LL.L.RR"], wires: [["L0", "C1"], ["C2", "L1"], ["C4", "L2"], ["C6", "L3"], ["L3", "C7"]] },
{ id: 'game9', group: 'Игры из инструкции', t: 'Игра 9. ДЛМ — экзаменатор', src: 'рис. 11',
d: "Экзамен по вычислительной технике: номер ответа → номер комментария.",
rows: [".B.BBR.", ".R.LRB.", ".B.RLB."], wires: [["C2", "L1"], ["C4", "L2"], ["L2", "C5"], ["C6", "L3"]] },
{ id: 'game10', group: 'Игры из инструкции', t: 'Игра 10. Ваш характер', src: 'рис. 12',
d: "Тест «Ваш характер»: 12 вопросов, ответ — код 0000…1111.",
rows: ["....RLL", "....LLB", "RRRRLRL"], wires: [["L0", "C1"], ["C2", "L1"], ["C3", "L2"], ["C4", "C5"], ["C5", "C6"], ["C6", "L3"], ["L3", "C7"]] },
{ id: 'game11', group: 'Игры из инструкции', t: 'Игра 11. Автоматическое управление лифтом', src: 'рис. 13',
d: "Логика управления лифтом: дверь, мотор, направление.",
rows: [".BBBLBL", ".RLBBLL", ".RBLRRB"], wires: [["C2", "L1"], ["C3", "C4"], ["C4", "L2"], ["C5", "C6"], ["C6", "L3"], ["L3", "C7"]] },
{ id: 'not', group: 'Логические элементы (из статьи на Хабре)', t: '3 × НЕ', src: 'статья',
d: 'X = ¬А, Y = ¬Б, Z = ¬В.',
rows: ['..R.B.B', '..B.R.B', '..B.B.R'], wires: [['L1','C3'], ['L2','C5'], ['L3','C7']] },
{ id: 'nand', group: 'Логические элементы (из статьи на Хабре)', t: '3И-НЕ', src: 'статья',
d: 'Z = ¬(А·Б·В) = ¬А + ¬Б + ¬В.',
rows: ['....RBB', '....BRB', '....BBR'], wires: [['L3','C7'], ['C7','C6'], ['C6','C5']] },
{ id: 'nor', group: 'Логические элементы (из статьи на Хабре)', t: '3ИЛИ-НЕ', src: 'статья',
d: 'Z = ¬(А + Б + В) = ¬А·¬Б·¬В, один столбец.',
rows: ['......R', '......R', '......R'], wires: [['L3','C7']] },
{ id: 'xor', group: 'Логические элементы (из статьи на Хабре)', t: 'Исключающее ИЛИ', src: 'статья',
d: 'Z = А ⊕ Б ⊕ В: горит при нечётном числе единиц.',
rows: ['...LRRL', '...LRLR', '...LLRR'], wires: [['L3','C7'], ['C7','C6'], ['C6','C5'], ['C5','C4']] },
{ id: 'adder', group: 'Логические элементы (из статьи на Хабре)', t: 'Полный сумматор', src: 'статья',
d: 'Z — бит суммы, V — перенос. Складывает три бита.',
rows: ['LLBLRRL', 'LBLLRLR', 'BLLLLRR'],
wires: [['L0','C1'], ['C1','C2'], ['C2','C3'], ['L3','C7'], ['C7','C6'], ['C6','C5'], ['C5','C4']] },
{ id: 'cipher', group: 'Логические элементы (из статьи на Хабре)', t: 'XOR-шифр, ключ 100', src: 'статья',
d: 'X, Y, Z = А, Б, В, зашифрованные ключом 100.',
rows: ['..R.B.B', '..B.L.B', '..B.B.L'], wires: [['L1','C3'], ['L2','C5'], ['L3','C7']] },
{ id: 'empty', group: 'Своя программа', t: 'Пустая машина', src: '',
d: 'Все штеккеры и перемычки сняты, переключатели в среднем положении.', rows: ['.......', '.......', '.......'], wires: [] }
];
// inp[r]: 1, 0 or null (middle position: every contact of that plank is open)
const S = { pins: [], wires: [], inp: [null, null, null], pressed: false, latch: false, sel: null, prog: null };
function loadProgram(id) {
const p = PROGRAMS.find(x => x.id === id);
S.pins = p.rows.map(r => [...r].map(ch => [ch === 'L' || ch === 'B', ch === 'R' || ch === 'B']));
S.wires = p.wires.map(w => [...w]);
S.inp = [null, null, null]; // the booklet: set the switches to the middle before placing pins
S.prog = id; S.sel = null;
update(); save();
}
// ---- circuit model ----
const pinCount = () => S.pins.flat().reduce((n, [l, r]) => n + l + r, 0);
const contactClosed = (r, c, inp = S.inp) => {
const v = inp[r], [l, rr] = S.pins[r][c];
return v !== null && ((l && v === 1) || (rr && v === 0));
};
const colTrue = (c, inp) => [0, 1, 2].every(r => contactClosed(r, c, inp));
function unionFind(edges) {
const parent = {};
const find = a => { if (!(a in parent)) parent[a] = a; return parent[a] === a ? a : (parent[a] = find(parent[a])); };
edges.forEach(([a, b]) => { parent[find(a)] = find(b); });
return find;
}
const NODES = ['L0', 'L1', 'L2', 'L3', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7'];
function solve(inp, power) {
const find = unionFind(S.wires);
const src = new Set();
for (let c = 0; c < 7; c++) if (power && colTrue(c, inp)) src.add('C' + (c + 1));
const hasSrc = n => NODES.some(m => src.has(m) && find(m) === find(n));
const hasLamp = n => NODES.some(m => m[0] === 'L' && find(m) === find(n));
const lit = LAMPS.map((_, k) => hasSrc('L' + k));
// a jumper carries current when it lies between a conducting column and a lamp
const wireState = S.wires.map(([a, b], i) => {
if (!hasSrc(a)) return 'dead';
if (!hasLamp(a)) return 'live';
const f2 = unionFind(S.wires.filter((_, j) => j !== i));
if (f2(a) === f2(b)) return 'flow';
const side = n => NODES.filter(m => f2(m) === f2(n));
const sa = side(a), sb = side(b);
const s = arr => arr.some(m => src.has(m)), l = arr => arr.some(m => m[0] === 'L');
return (s(sa) && l(sb)) || (l(sa) && s(sb)) ? 'flow' : 'live';
});
const colFlow = [...Array(7)].map((_, c) => src.has('C' + (c + 1)) && hasLamp('C' + (c + 1)));
return { lit, wireState, colFlow, src, hasSrc };
}
const lampsFor = inp => solve(inp, true).lit;
const el = (svg, tag, attrs = {}, parent = svg) => {
const e = document.createElementNS(NS, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
parent.appendChild(e); return e;
};
// =============== FRONT PANEL (after Fig. 1 of the booklet) ===============
const svg = document.getElementById('dlm');
const E = (tag, attrs, parent) => el(svg, tag, attrs, parent);
const colX = c => 95 + c * 105;
const lampX = k => colX(k * 2);
const ROW_Y = [330, 440, 550], PLANK_H = 76;
const LSPR_Y = 182, CSPR_LOW = 268, CSPR_HIGH = 226;
const colSprY = c => c % 2 ? CSPR_HIGH : CSPR_LOW; // springs of columns 2, 4, 6 sit higher
const SW_Y = ROW_Y.map(y => y + PLANK_H / 2);
const INK = '#2a2d33';
E('defs').innerHTML = `
<filter id="glow" x="-80%" y="-80%" width="260%" height="260%"><feGaussianBlur stdDeviation="9"/></filter>
<radialGradient id="pinG" cx="35%" cy="30%" r="70%"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#d9d6cc"/></radialGradient>
<radialGradient id="btnG" cx="40%" cy="35%" r="70%"><stop offset="0" stop-color="#4a4d55"/><stop offset="1" stop-color="#101114"/></radialGradient>
<linearGradient id="bodyG" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fbfaf6"/><stop offset="1" stop-color="#ebe9e1"/></linearGradient>`;
// body, rails and the side panel, as in Fig. 1
E('rect', { x: 6, y: 6, width: 988, height: 688, rx: 28, fill: 'url(#bodyG)', stroke: '#bcb8ac', 'stroke-width': 3 });
E('rect', { x: 26, y: 20, width: 772, height: 672, fill: 'none', stroke: '#cfccc1', 'stroke-width': 2 });
E('line', { x1: 26, y1: 62, x2: 798, y2: 62, stroke: '#cfccc1', 'stroke-width': 2 });
E('line', { x1: 26, y1: 140, x2: 798, y2: 140, stroke: '#cfccc1', 'stroke-width': 2 });
E('line', { x1: 812, y1: 62, x2: 980, y2: 62, stroke: '#cfccc1', 'stroke-width': 2 });
E('line', { x1: 812, y1: 140, x2: 980, y2: 140, stroke: '#cfccc1', 'stroke-width': 2 });
// lamps with oval labels V-8 … Z-1
const lampEls = LAMPS.map((L, k) => {
const x = lampX(k);
E('rect', { x: x - 36, y: 26, width: 72, height: 30, rx: 15, fill: 'none', stroke: '#9c988c', 'stroke-width': 2 });
E('text', { x, y: 48, 'text-anchor': 'middle', 'font-family': 'PT Sans, sans-serif', 'font-style': 'italic', 'font-size': 20, fill: '#5d5a52' }).textContent = `${L.name}-${L.w}`;
E('rect', { x: x - 48, y: 72, width: 96, height: 58, rx: 3, fill: '#dedbd1', stroke: '#b7b3a7', 'stroke-width': 2 });
const glow = E('rect', { x: x - 40, y: 76, width: 80, height: 50, rx: 6, fill: L.color === 'red' ? '#ff5a3c' : '#53ff7e', filter: 'url(#glow)', opacity: 0 });
const lens = E('rect', { x: x - 36, y: 80, width: 72, height: 42, rx: 2 });
E('rect', { x: x - 30, y: 84, width: 60, height: 10, rx: 2, fill: '#ffffff', opacity: .18 });
E('line', { x1: x, y1: 130, x2: x, y2: LSPR_Y - 16, stroke: '#b7b3a7', 'stroke-width': 2 });
return { glow, lens, color: L.color };
});
// side panel: emblem, button
E('ellipse', { cx: 896, cy: 101, rx: 62, ry: 30, fill: '#c8222c' });
E('ellipse', { cx: 896, cy: 101, rx: 55, ry: 24, fill: 'none', stroke: '#fff', 'stroke-width': 2.5 });
E('text', { x: 896, y: 110, 'text-anchor': 'middle', 'font-family': 'Rubik, sans-serif', 'font-size': 25, 'font-weight': 800, fill: '#fff' }).textContent = 'длм';
const btnG = E('g', { class: 'hit', tabindex: 0, role: 'button', 'aria-label': 'Кнопка: держать, чтобы получить ответ' });
E('circle', { cx: 896, cy: 220, r: 44, fill: '#efede6', stroke: '#b7b3a7', 'stroke-width': 3 }, btnG);
E('circle', { cx: 896, cy: 220, r: 34, fill: 'none', stroke: '#cfccc1', 'stroke-width': 2 }, btnG);
const btnCap = E('circle', { cx: 896, cy: 220, r: 27, fill: 'url(#btnG)', class: 'focusring', stroke: 'transparent' }, btnG);
// contact springs
const springEls = {};
function spring(id, x, y, label) {
const g = E('g', { class: 'hit', tabindex: 0, role: 'button', 'aria-label': label });
E('circle', { cx: x, cy: y + 3, r: 17, fill: '#000', opacity: .12 }, g);
const ring = E('circle', { cx: x, cy: y, r: 16, fill: '#c9cdd3', stroke: '#7d838c', 'stroke-width': 2.5, class: 'focusring' }, g);
E('circle', { cx: x, cy: y, r: 11, fill: 'none', stroke: '#8a9098', 'stroke-width': 2 }, g);
E('circle', { cx: x, cy: y, r: 6, fill: '#55595f' }, g);
springEls[id] = { ring, x, y };
g.addEventListener('click', () => pickSpring(id));
}
LAMPS.forEach((L, k) => spring('L' + k, lampX(k), LSPR_Y, `Пружина лампы ${L.name}`));
for (let c = 0; c < 7; c++) {
E('line', { x1: colX(c), y1: colSprY(c) + 16, x2: colX(c), y2: ROW_Y[0] - 22, stroke: '#9c988c', 'stroke-width': 2.5 });
E('text', { x: colX(c), y: ROW_Y[0] - 6, 'text-anchor': 'middle', 'font-family': 'PT Sans, sans-serif', 'font-style': 'italic', 'font-size': 20, fill: '#5d5a52' }).textContent = c + 1;
spring('C' + (c + 1), colX(c), colSprY(c), `Пружина столбца ${c + 1}`);
}
// planks: 14 holes each; the column's divider runs between its two holes
const plankGs = [], holeEls = [];
ROW_Y.forEach((y, r) => {
E('rect', { x: 30, y: y - 6, width: 764, height: PLANK_H + 12, fill: '#e2dfd5' });
const g = E('g', { style: 'transition: transform .18s ease' });
plankGs.push(g);
E('rect', { x: 34, y, width: 756, height: PLANK_H, rx: 3, fill: '#d42a35' }, g);
E('rect', { x: 34, y: y + PLANK_H - 8, width: 756, height: 8, rx: 2, fill: '#a51c26' }, g);
holeEls[r] = [];
for (let c = 0; c < 7; c++) {
E('line', { x1: colX(c), y1: y + 4, x2: colX(c), y2: y + PLANK_H - 10, stroke: '#8f141d', 'stroke-width': 2.5 }, g);
holeEls[r][c] = [0, 1].map(side => {
const hx = colX(c) + (side ? 26 : -26), hy = y + PLANK_H / 2 - 3;
const hg = E('g', { class: 'hit', tabindex: 0, role: 'button' }, g);
E('circle', { cx: hx, cy: hy, r: 20, fill: 'transparent' }, hg);
E('circle', { cx: hx, cy: hy, r: 10, fill: '#3a0b10', class: 'focusring', stroke: 'transparent' }, hg);
const pin = E('g', { opacity: 0 }, hg);
E('ellipse', { cx: hx + 3, cy: hy + 5, rx: 12, ry: 9, fill: '#000', opacity: .25 }, pin);
E('circle', { cx: hx, cy: hy, r: 12, fill: 'url(#pinG)', stroke: '#bdb8aa', 'stroke-width': 1.5 }, pin);
for (let a = 0; a < 4; a++) {
const ang = a * Math.PI / 4;
E('line', { x1: hx - 7 * Math.cos(ang), y1: hy - 7 * Math.sin(ang), x2: hx + 7 * Math.cos(ang), y2: hy + 7 * Math.sin(ang), stroke: '#c9c4b6', 'stroke-width': 1.2 }, pin);
}
hg.addEventListener('click', () => togglePin(r, c, side));
return { hg, pin, label: `Столбец ${c + 1}, планка ${ROWS[r]}, ${side ? 'правое' : 'левое'} гнездо` };
});
}
});
// rotary switches А-4, Б-2, В-1 with positions 0 · middle · 1
const swEls = ROWS.map((name, r) => {
const cy = SW_Y[r];
const mark = (v, x) => {
const g = E('g', { class: 'hit', tabindex: 0, role: 'button', 'aria-label': `Переключатель ${name} в положение ${v}` });
E('circle', { cx: x, cy: cy + 34, r: 15, fill: 'transparent' }, g);
E('circle', { cx: x, cy: cy + 34, r: 11, fill: 'none', stroke: '#9c988c', 'stroke-width': 1.8 }, g);
E('text', { x, y: cy + 40, 'text-anchor': 'middle', 'font-family': 'PT Sans, sans-serif', 'font-size': 16, fill: '#5d5a52' }, g).textContent = v;
g.addEventListener('click', () => setSwitch(r, v));
};
mark(0, 836); mark(1, 956);
const g = E('g', { class: 'hit', tabindex: 0, role: 'switch' });
E('circle', { cx: 896, cy, r: 40, fill: '#efede6', stroke: '#b7b3a7', 'stroke-width': 3, class: 'focusring' }, g);
const knob = E('g', { style: `transition: transform .18s ease; transform-origin: 896px ${cy}px` }, g);
E('rect', { x: 881, y: cy - 40, width: 30, height: 80, rx: 4, fill: '#f8f7f2', stroke: INK, 'stroke-width': 2 }, knob);
E('line', { x1: 896, y1: cy - 12, x2: 896, y2: cy + 12, stroke: INK, 'stroke-width': 2 }, knob);
E('text', { x: 896, y: cy - 18, 'text-anchor': 'middle', 'font-family': 'PT Sans, sans-serif', 'font-style': 'italic', 'font-size': 18, fill: INK }, knob).textContent = name;
E('text', { x: 896, y: cy + 33, 'text-anchor': 'middle', 'font-family': 'PT Sans, sans-serif', 'font-style': 'italic', 'font-size': 18, fill: INK }, knob).textContent = [4, 2, 1][r];
g.addEventListener('click', () => setSwitch(r, S.inp[r] === null ? 1 : 1 - S.inp[r]));
return { g, knob };
});
const wireLayer = E('g');
svg.addEventListener('keydown', e => {
const g = e.target.closest('.hit');
if (!g || g === btnG) return;
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); g.dispatchEvent(new MouseEvent('click', { bubbles: true })); }
});
// =============== INSIDE VIEW: the mechanism, reconstructed ===============
// No photo of the mechanism under the cover is known; this follows the booklet (circuits open in
// the middle position) and the memory of an owner. Under every column, below each plank, an upright
// spring leaf stands between the column's two holes, with a fixed contact on either side. Turning
// a switch slides its plank: at «1» the peg in the left hole bends the leaf right onto its contact,
// at «0» the peg in the right hole bends it left; in the middle nothing touches it. The three leaves
// of a column connect the + bus at the bottom to the column spring; lamps sit on top, as on the panel.
const ins = document.getElementById('inside');
const I = (tag, attrs, parent) => el(ins, tag, attrs, parent);
const IX = c => 300 + c * 95; // column x: the leaf stands right on it
const HOFF = 22, TRAVEL = 27; // hole offset from the column line; plank travel brings a peg onto the leaf
const NEG_Y = 36, LAMP_Y = 84, LNODE_Y = 134, NODE_Y = 196, PL_Y = [250, 380, 510], STRIP_H = 30, BUS_Y = 690;
const TIP_Y = y => y + STRIP_H + 20, ANCH_Y = y => y + STRIP_H + 64, PEG_END = y => y + STRIP_H + 22;
const ILX = k => IX(k * 2);
const BP = { ink: '#9fb6e6', dim: '#50678f', txt: '#c9d6f2', strip: '#c8222c', stripDark: '#7d1218', pin: '#f1efe8' };
for (let x = 0; x <= 1000; x += 40) I('line', { x1: x, y1: 0, x2: x, y2: 720, stroke: '#1b3463', 'stroke-width': 1 });
for (let y = 0; y <= 720; y += 40) I('line', { x1: 0, y1: y, x2: 1000, y2: y, stroke: '#1b3463', 'stroke-width': 1 });
const wpath = (d, parent) => I('path', { d, class: 'w dead' }, parent);
const T = (x, y, s, attrs = {}, parent) => { const t = I('text', Object.assign({ x, y, fill: BP.txt, 'font-family': 'PT Sans, sans-serif', 'font-size': 15 }, attrs), parent); t.textContent = s; return t; };
// battery 3 × А-332 on the left: − to the lamps' bus on top, + through the button to the bus at the bottom
const segBatNeg = wpath(`M44 156 V${NEG_Y} H${ILX(3)}`);
const segBatPos = wpath(`M44 226 V${BUS_Y} H96`);
const segBus = wpath(`M150 ${BUS_Y} H${IX(6)}`);
[0, 1, 2].forEach(i => {
const y = 160 + i * 26;
I('line', { x1: 31, y1: y, x2: 57, y2: y, stroke: BP.txt, 'stroke-width': 6 });
I('line', { x1: 20, y1: y + 10, x2: 68, y2: y + 10, stroke: BP.txt, 'stroke-width': 3 });
});
T(64, 186, '3 × А-332', { 'font-size': 12 });
T(64, 201, '4,5 В', { 'font-size': 12 });
I('circle', { cx: 96, cy: BUS_Y, r: 4, fill: BP.txt });
I('circle', { cx: 150, cy: BUS_Y, r: 4, fill: BP.txt });
const btnBlade = I('line', { x1: 96, y1: BUS_Y, x2: 146, y2: BUS_Y - 24, stroke: BP.txt, 'stroke-width': 3, 'stroke-linecap': 'round' });
T(123, BUS_Y - 28, 'кнопка', { 'text-anchor': 'middle', 'font-size': 12 });
T(IX(6) + 8, BUS_Y + 5, 'шина +', { 'font-size': 12 });
T(ILX(3) + 8, NEG_Y + 5, 'шина −', { 'font-size': 12 });
// columns: from the + bus up through the leaves of planks В, Б, А to the column spring.
// Each leaf's two fixed contacts are joined around it and lead to the next leaf.
const colEls = [];
for (let c = 0; c < 7; c++) {
const x = IX(c);
I('circle', { cx: x, cy: BUS_Y, r: 4, fill: BP.txt });
const feed = wpath(`M${x} ${BUS_Y} V${ANCH_Y(PL_Y[2])}`);
const stages = [2, 1, 0].map(r => {
const tip = TIP_Y(PL_Y[r]), join = r ? ANCH_Y(PL_Y[r - 1]) : NODE_Y + 30;
const sides = [-1, 1].map(s => {
I('rect', { x: x + s * 13 - 2, y: tip - 10, width: 4, height: 16, fill: BP.txt });
return wpath(`M${x + s * 13} ${tip} H${x + s * 36} V${join} H${x}${r ? '' : ` V${NODE_Y}`}`);
});
I('circle', { cx: x, cy: ANCH_Y(PL_Y[r]), r: 4, fill: BP.txt });
const leaf = I('line', { x1: x, y1: ANCH_Y(PL_Y[r]), x2: x, y2: tip - 6, stroke: BP.txt, 'stroke-width': 3.5, 'stroke-linecap': 'round', style: 'transition: all .2s ease' });
return { r, sides, leaf };
});
T(x + 12, NODE_Y - 10, String(c + 1), { 'font-style': 'italic', 'font-size': 16 });
colEls.push({ x, feed, stages });
}
T(IX(0) - 26, NODE_Y + 5, 'пружины столбцов', { 'font-size': 12, 'text-anchor': 'end' });
T(ILX(0) - 26, LNODE_Y + 5, 'пружины ламп', { 'font-size': 12, 'text-anchor': 'end' });
// planks: a dial (the switch) cranks the strip left or right; pegs stick out below it, past the leaves
const insPlanks = PL_Y.map((y, r) => {
const cy = y + STRIP_H / 2;
T(108, y - 8, `переключатель ${ROWS[r]}`, { 'font-size': 12, 'text-anchor': 'middle' });
I('circle', { cx: 108, cy, r: 24, fill: '#0f2348', stroke: BP.txt, 'stroke-width': 2 });
const knob = I('line', { x1: 108, y1: cy + 18, x2: 108, y2: cy - 18, stroke: BP.txt, 'stroke-width': 5, 'stroke-linecap': 'round', style: `transition: transform .25s ease; transform-origin: 108px ${cy}px` });
T(84, cy + 40, '0', { 'font-size': 12, 'text-anchor': 'middle' }); T(132, cy + 40, '1', { 'font-size': 12, 'text-anchor': 'middle' });
const rod = I('line', { x1: 132, y1: cy, x2: 196, y2: cy, stroke: BP.txt, 'stroke-width': 3, style: 'transition: transform .25s ease' });
const g = I('g', { style: 'transition: transform .25s ease' });
I('rect', { x: 196, y, width: IX(6) + 70 - 196, height: STRIP_H, rx: 3, fill: BP.strip, stroke: BP.stripDark, 'stroke-width': 2 }, g);
T(206, y + 20, ROWS[r], { 'font-size': 15, fill: '#ffffff', 'font-weight': 700 }, g);
const holes = [];
for (let c = 0; c < 7; c++) {
holes[c] = [0, 1].map(side => {
const hx = IX(c) + (side ? HOFF : -HOFF);
I('circle', { cx: hx, cy, r: 7, fill: BP.stripDark }, g);
return I('rect', { x: hx - 5, y: cy - 9, width: 10, height: PEG_END(y) - cy + 9, rx: 3, fill: BP.pin, stroke: '#9c988c', 'stroke-width': 1 }, g);
});
}
return { knob, rod, g, holes };
});
const plankState = PL_Y.map(y => T(108, y + STRIP_H + 62, '', { 'font-size': 12, 'text-anchor': 'middle', fill: '#ffd166' }));
// lamps on top: from the lamp spring up through the bulb to the − bus
const insLamps = LAMPS.map((L, k) => {
const x = ILX(k);
const up = wpath(`M${x} ${LNODE_Y} V${LAMP_Y + 18}`);
const down = wpath(`M${x} ${LAMP_Y - 18} V${NEG_Y}`);
const bulb = I('circle', { cx: x, cy: LAMP_Y, r: 18, fill: '#0f2348', stroke: BP.txt, 'stroke-width': 2.5 });
I('line', { x1: x - 12, y1: LAMP_Y - 12, x2: x + 12, y2: LAMP_Y + 12, stroke: BP.txt, 'stroke-width': 2 });
I('line', { x1: x + 12, y1: LAMP_Y - 12, x2: x - 12, y2: LAMP_Y + 12, stroke: BP.txt, 'stroke-width': 2 });
T(x + 26, LAMP_Y + 5, `${L.name}-${L.w}`, { 'font-style': 'italic' });
return { up, down, bulb };
});
const insWires = I('g');
// draw node circles above jumpers again, for clarity
const nodeCaps = I('g');
const insPos = {};
LAMPS.forEach((_, k) => { insPos['L' + k] = [ILX(k), LNODE_Y]; });
for (let c = 0; c < 7; c++) insPos['C' + (c + 1)] = [IX(c), NODE_Y];
function insWirePath(a, b) {
const [x1, y1] = insPos[a], [x2, y2] = insPos[b];
// jumpers between two springs of one row bow into the gap between the rows
if (y1 === y2) { const bow = (28 + Math.abs(x1 - x2) * 0.08) * (y1 === NODE_Y ? -1 : 1); return `M${x1} ${y1} Q${(x1 + x2) / 2} ${y1 + bow} ${x2} ${y2}`; }
return `M${x1} ${y1} C${x1} ${(y1 + y2) / 2 + 10} ${x2} ${(y1 + y2) / 2 - 10} ${x2} ${y2}`;
}
// =============== interactions ===============
const hintEl = document.getElementById('hint');
let msg = null;
function flash(text) { msg = text; update(); clearTimeout(flash.t); flash.t = setTimeout(() => { msg = null; update(); }, 3600); }
function togglePin(r, c, side) {
const on = S.pins[r][c][side];
if (!on && pinCount() >= MAX_PINS) { flash(`В комплекте всего ${MAX_PINS} штеккеров. Выньте лишний, чтобы поставить новый.`); return; }
S.pins[r][c][side] = !on; S.prog = null;
if (S.inp[r] !== null) flash(`Руководство велит перед установкой штеккеров ставить переключатель ${ROWS[r]} в среднее положение, иначе можно сломать машину.`);
else update();
save();
}
function setSwitch(r, v) { S.inp[r] = v; update(); save(); }
function pickSpring(id) {
if (!S.sel) { S.sel = id; update(); return; }
if (S.sel === id) { S.sel = null; update(); return; }
const a = S.sel, b = id;
if (!S.wires.some(([x, y]) => (x === a && y === b) || (x === b && y === a))) {
if (S.wires.length >= 7) { S.sel = null; flash('В комплекте всего 7 перемычек. Снимите лишнюю.'); return; }
S.wires.push([a, b]);
}
S.sel = null; S.prog = null; update(); save();
}
function setPressed(v) { if (S.pressed !== v) { S.pressed = v; update(); } }
btnG.addEventListener('pointerdown', e => { e.preventDefault(); btnG.setPointerCapture?.(e.pointerId); setPressed(true); });
['pointerup', 'pointercancel', 'lostpointercapture'].forEach(t => btnG.addEventListener(t, () => setPressed(false)));
btnG.addEventListener('keydown', e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setPressed(true); } });
btnG.addEventListener('keyup', e => { if (e.key === ' ' || e.key === 'Enter') setPressed(false); });
btnG.addEventListener('blur', () => setPressed(false));
document.getElementById('latch').addEventListener('change', e => { S.latch = e.target.checked; update(); });
document.getElementById('mid').addEventListener('click', () => { S.inp = [null, null, null]; update(); save(); });
document.getElementById('clear').addEventListener('click', () => loadProgram('empty'));
// ---- lower panels ----
const progBox = document.getElementById('programs');
let lastGroup = null;
const progBtns = PROGRAMS.map(p => {
if (p.group !== lastGroup) { lastGroup = p.group; const d = document.createElement('div'); d.className = 'group-label'; d.textContent = p.group; progBox.appendChild(d); }
const b = document.createElement('button');
b.type = 'button'; b.className = 'prog'; b.id = 'prog-' + p.id;
b.innerHTML = `<span class="t"></span><span class="src"></span><span class="d"></span>`;
b.querySelector('.t').textContent = p.t; b.querySelector('.src').textContent = p.src; b.querySelector('.d').textContent = p.d;
b.addEventListener('click', () => loadProgram(p.id));
progBox.appendChild(b);
return [p.id, b];
});
document.querySelectorAll('[data-load]').forEach(b => b.addEventListener('click', () => {
loadProgram(b.dataset.load); svg.scrollIntoView({ behavior: 'smooth', block: 'center' });
}));
const tt = document.getElementById('tt');
function renderTable() {
let h = '<thead><tr><th>А</th><th>Б</th><th>В</th>' + LAMPS.map((L, i) => `<th${i ? '' : ' class="sep"'}>${L.name}</th>`).join('') + '<th class="sep">число</th></tr></thead><tbody>';
for (let n = 0; n < 8; n++) {
const inp = [n >> 2 & 1, n >> 1 & 1, n & 1];
const lit = lampsFor(inp);
const val = lit.reduce((s, on, k) => s + (on ? LAMPS[k].w : 0), 0);
const cur = inp.every((v, i) => v === S.inp[i]);
h += `<tr data-n="${n}"${cur ? ' class="cur"' : ''}><td>${inp[0]}</td><td>${inp[1]}</td><td>${inp[2]}</td>` +
lit.map((on, k) => `<td${k ? '' : ' class="sep"'}><span class="dot ${LAMPS[k].color}${on ? ' on' : ''}"></span></td>`).join('') +
`<td class="sep">${val}</td></tr>`;
}
tt.innerHTML = h + '</tbody>';
}
tt.addEventListener('click', e => {
const tr = e.target.closest('tr[data-n]'); if (!tr) return;
const n = +tr.dataset.n; S.inp = [n >> 2 & 1, n >> 1 & 1, n & 1]; update(); save();
});
const exprs = document.getElementById('exprs');
function renderExprs() {
const find = unionFind(S.wires);
exprs.innerHTML = '';
for (let c = 0; c < 7; c++) {
const terms = [];
let dead = false, used = false;
S.pins.forEach((row, r) => {
const [l, rr] = row[c];
if (l || rr) used = true;
if (l && rr) return;
if (l) terms.push(ROWS[r]); else if (rr) terms.push('¬' + ROWS[r]); else dead = true;
});
const lamps = LAMPS.filter((_, k) => find('L' + k) === find('C' + (c + 1))).map(L => L.name);
const li = document.createElement('li');
const expr = !used ? '<span class="dead">пусто</span>' : dead ? '<span class="dead">цепь разомкнута</span>' : (terms.length ? terms.join('·') : '1');
li.innerHTML = `<span class="n">${c + 1}</span><span>${expr}</span><span class="to">${lamps.length ? '→ ' + lamps.join(', ') : used ? '<span class="dead">без перемычки</span>' : ''}</span>`;
exprs.appendChild(li);
}
}
// ---- render ----
function frontWirePath(a, b) {
const A = springEls[a], B = springEls[b];
const sag = 22 + Math.abs(A.x - B.x) * 0.1;
return `M${A.x} ${A.y} Q${(A.x + B.x) / 2} ${(A.y + B.y) / 2 + sag} ${B.x} ${B.y}`;
}
// wavy line, the way the booklet draws a jumper
function wavy(a, b) {
const A = springEls[a], B = springEls[b];
const dx = B.x - A.x, dy = B.y - A.y, len = Math.hypot(dx, dy), n = Math.max(4, Math.round(len / 14));
const ux = dx / len, uy = dy / len, px = -uy, py = ux;
let d = `M${A.x + ux * 16} ${A.y + uy * 16}`;
for (let i = 1; i <= n; i++) {
const t = 16 + (len - 32) * i / n, s = (i % 2 ? 5 : -5);
const tm = 16 + (len - 32) * (i - .5) / n;
d += ` Q${A.x + ux * tm + px * s} ${A.y + uy * tm + py * s} ${A.x + ux * t} ${A.y + uy * t}`;
}
return d;
}
function renderFrontWires() {
wireLayer.innerHTML = '';
S.wires.forEach(([a, b], i) => {
const g = E('g', { class: 'hit', tabindex: 0, role: 'button', 'aria-label': `Перемычка ${a}–${b}, снять` }, wireLayer);
const d = wavy(a, b);
E('path', { d, fill: 'none', stroke: 'transparent', 'stroke-width': 18 }, g);
E('path', { d, fill: 'none', stroke: '#1b1d22', 'stroke-width': 3.5, 'stroke-linecap': 'round', class: 'focusring' }, g);
g.addEventListener('click', () => { S.wires.splice(i, 1); S.prog = null; update(); save(); });
g.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); g.dispatchEvent(new MouseEvent('click')); } });
});
}
const setCls = (node, state) => node.setAttribute('class', 'w ' + state);
function update() {
const power = S.pressed || S.latch;
const sol = solve(S.inp, power);
const anyFlow = sol.lit.some(Boolean);
// front panel
lampEls.forEach((L, k) => {
const on = sol.lit[k];
L.glow.setAttribute('opacity', on ? .95 : 0);
L.lens.setAttribute('fill', L.color === 'red' ? (on ? '#ff8a6a' : '#9c2a24') : (on ? '#b9ffcb' : '#1f6b45'));
});
btnCap.setAttribute('r', S.pressed ? 24 : 27);
S.pins.forEach((row, r) => row.forEach((pr, c) => pr.forEach((v, side) => {
const h = holeEls[r][c][side];
h.pin.setAttribute('opacity', v ? 1 : 0);
h.hg.setAttribute('aria-label', h.label + (v ? ': штеккер стоит' : ': пусто'));
})));
S.inp.forEach((v, r) => {
plankGs[r].style.transform = `translateX(${v === null ? 0 : v ? 8 : -8}px)`;
swEls[r].knob.style.transform = `rotate(${v === null ? 0 : v ? 90 : -90}deg)`;
swEls[r].g.setAttribute('aria-checked', v === 1 ? 'true' : v === 0 ? 'false' : 'mixed');
swEls[r].g.setAttribute('aria-label', `Переключатель ${ROWS[r]}: ${v === null ? 'среднее положение' : v}`);
});
Object.entries(springEls).forEach(([id, s]) => {
s.ring.setAttribute('stroke', S.sel === id ? '#23409a' : '#7d838c');
s.ring.setAttribute('stroke-width', S.sel === id ? 6 : 2.5);
});
renderFrontWires();
// inside view
btnBlade.setAttribute('x2', power ? 150 : 146);
btnBlade.setAttribute('y2', power ? BUS_Y : BUS_Y - 24);
const supply = !power ? 'dead' : anyFlow ? 'flow' : 'live';
setCls(segBatPos, supply); setCls(segBus, supply);
setCls(segBatNeg, anyFlow ? 'flow' : 'dead');
insPlanks.forEach((P, r) => {
const v = S.inp[r], shift = v === null ? 0 : v ? TRAVEL : -TRAVEL;
P.g.style.transform = `translateX(${shift}px)`;
P.rod.style.transform = `translateX(${shift}px)`;
P.knob.style.transform = `rotate(${v === null ? 0 : v ? 50 : -50}deg)`;
for (let c = 0; c < 7; c++) P.holes[c].forEach((peg, side) => peg.setAttribute('opacity', S.pins[r][c][side] ? 1 : 0));
});
plankState.forEach((t, r) => { const v = S.inp[r]; t.textContent = v === null ? 'середина' : v ? '«1» · вправо' : '«0» · влево'; });
colEls.forEach((col, c) => {
let reach = power; // energized from the bus up to the first open leaf
setCls(col.feed, !reach ? 'dead' : sol.colFlow[c] ? 'flow' : 'live');
col.stages.forEach(st => {
const side = contactClosed(st.r, c) ? (S.inp[st.r] ? 1 : -1) : 0; // the contact the leaf is bent onto
st.leaf.setAttribute('x2', col.x + side * 11);
reach = reach && side !== 0;
st.sides.forEach((seg, i) => setCls(seg, !reach ? 'dead' : sol.colFlow[c] && side === (i ? 1 : -1) ? 'flow' : 'live'));
});
});
insLamps.forEach((L, k) => {
const on = sol.lit[k];
setCls(L.up, on ? 'flow' : sol.hasSrc('L' + k) ? 'live' : 'dead');
setCls(L.down, on ? 'flow' : 'dead');
L.bulb.setAttribute('fill', on ? (LAMPS[k].color === 'red' ? '#ff5a3c' : '#53ff7e') : '#0f2348');
});
insWires.innerHTML = '';
S.wires.forEach(([a, b], i) => I('path', { d: insWirePath(a, b), class: 'w ' + sol.wireState[i] }, insWires));
nodeCaps.innerHTML = '';
Object.entries(insPos).forEach(([id, [x, y]]) => I('circle', { cx: x, cy: y, r: 7, fill: '#0f2348', stroke: '#c9d6f2', 'stroke-width': 2.5 }, nodeCaps));
renderTable();
renderExprs();
progBtns.forEach(([id, b]) => b.setAttribute('aria-pressed', S.prog === id ? 'true' : 'false'));
const n = pinCount(), chip = document.getElementById('pincount');
chip.textContent = `штеккеров: ${n} / ${MAX_PINS} · перемычек: ${S.wires.length} / 7`; chip.classList.toggle('over', n >= MAX_PINS);
const ro = document.getElementById('readout');
if (power) {
const bits = sol.lit.map(b => b ? 1 : 0).join('');
const val = sol.lit.reduce((s, b, k) => s + (b ? LAMPS[k].w : 0), 0);
const names = LAMPS.filter((_, k) => sol.lit[k]).map(L => L.name).join(' ') || 'ничего';
ro.innerHTML = `горят: <b>${names}</b> · ${bits}₂ = <b>${val}</b>`;
ro.hidden = false;
} else ro.hidden = true;
hintEl.className = msg ? 'warn' : 'hint';
hintEl.textContent = msg ?? (S.sel ? 'Теперь щёлкните вторую пружину, чтобы поставить перемычку.'
: S.inp.includes(null) && !power ? 'Переключатели в среднем положении: планки отведены, все контакты разомкнуты.'
: power ? '' : 'Нажмите и держите кнопку, чтобы получить ответ.');
}
// ---- persistence ----
const KEY = 'dlm-state-v3';
function save() { try { localStorage.setItem(KEY, JSON.stringify({ pins: S.pins, wires: S.wires, inp: S.inp, prog: S.prog })); } catch (e) {} }
function restore() {
try {
const d = JSON.parse(localStorage.getItem(KEY));
if (d && d.pins?.length === 3 && Array.isArray(d.wires) && Array.isArray(d.inp)) { Object.assign(S, d); update(); return true; }
} catch (e) {}
return false;
}
if (!restore()) loadProgram('game1');
})();
</script>
</body>
</html>
+67
View File
@@ -0,0 +1,67 @@
# Компьютер без электроники: оцифровано руководство к «Детской логической машине» 1987 года
В конце 1980-х в советских магазинах продавалась игрушка «Детская логическая машина», ДЛМ. Выпускал её ВНИИ «Электронстандарт» в Гатчине. Это программируемое устройство, в котором нет ни транзистора, ни реле, ни микросхемы: только пластмассовые планки, штеккеры, пружинные контакты, три батарейки и четыре лампочки. И всё же машина отгадывает задуманное число, вычисляет честных и лгунов, играет со спичками и даже экзаменует по вычислительной технике.
Руководство к ней в сети найти было невозможно. Были только описания игрушки, фото коробки и несколько снимков страниц в статье на Хабре. Полный экземпляр нашёлся у меня дома: сама машина давно потерялась, а брошюра уцелела. Теперь она отсканирована, набрана заново, и по ней сделан работающий эмулятор.
**Скачать руководство (PDF, 69 стр.):** [ссылка на PDF]
**Эмулятор в браузере:** [ссылка на эмулятор]
## Что это за машина
На передней панели сверху четыре лампы: V, X, Y, Z. Над каждой стоит её «вес» в двоичной системе: 8, 4, 2, 1. Ниже три красные планки, по 14 гнёзд в каждой, и семь столбцов программирования. Справа кнопка и три поворотных переключателя А, Б, В с «весами» 4, 2, 1.
В комплект входили 30 штеккеров, 7 перемычек, игральный кубик, три фишки, 12 карточек с птицами, зверями и насекомыми и три карточки с надписями. Питание — три элемента А-332 (4,5 В), потребляемый ток 200 мА, от одного комплекта батарей машина работала не меньше 80 часов. На коробке указан возраст: 13–15 лет.
## Как она «думает»
Самое интересное в ДЛМ — что вся её логика механическая.
1. **Ввод.** Переключатель сдвигает свою планку на полшага гнезда: в положении «1» вправо, в «0» влево. В среднем положении планка отведена, и все её контакты разомкнуты.
2. **Программа.** Под каждым столбцом, под каждой планкой, внутри корпуса стоит пружинящая пластина, а по бокам от неё два неподвижных контакта. Когда планка уходит вправо (положение «1»), штеккер из левого гнезда отгибает пластину на правый контакт. Когда влево («0»), штеккер из правого гнезда отгибает её на левый. Нет штеккера в нужном гнезде — нет и контакта. Штеккер в левом гнезде значит «А», в правом — «не А», в обоих — «А не важно», ни одного — «столбец не работает никогда».
3. **И.** В каждом столбце три контакта соединены подряд, по одному на каждую планку. Ток проходит через столбец, только если замкнуты все три.
4. **ИЛИ.** Перемычками столбцы соединяют с лампами. Если к одной лампе подведено несколько столбцов, она загорается, когда сработал хотя бы один из них.
5. **Вывод.** Нажимаете кнопку — горят лампы. Ответ читается как двоичное число от 0 до 15.
То есть машина одновременно вычисляет семь выражений вида «А и не Б и В» и складывает их по ИЛИ на четыре выхода. Говоря современным языком, это программируемая логическая матрица с тремя входами и четырьмя выходами, собранная из пластмассы и пружинок.
В эмуляторе всё это видно: справа нарисован разрез машины. Честно оговорюсь: это реконструкция. В брошюре механизма нет, снимков вскрытой машины я не нашёл, так что рисунок сделан по брошюре («цепи разомкнуты в среднем положении планок») и по моей памяти. Повернёте переключатель — планка поедет, штеккеры нажмут контакты, а при нажатой кнопке видно, по каким столбцам и перемычкам идёт ток.
## Одиннадцать игр
1. **Какое число задумано?** Три вопроса — и машина называет число от 0 до 7. Вопросы хитрые, например: «К задуманному числу прибавьте 10 и разделите на 6. Остаток 4 или 5?»
2. **Честные и лгуны.** Классическая логическая задача про страну, где одни всегда говорят правду, а другие всегда лгут.
3. **Сумма трёх чисел.** Фокус с кубиком: машина «угадывает» сумму, потому что противоположные грани кубика в сумме всегда дают 7.
4. **Двенадцать — выигрыш!** Игра с кубиком против машины: кто первым наберёт ровно 12.
5. **Какой предмет выбран?** Угадывание, кому из трёх игроков досталась какая фишка, по числу оставшихся спичек.
6. **ДЛМ превосходит себя!** То же самое, но число спичек машине не сообщают.
7. **Семь спичек.** Беспроигрышная стратегия, если противник ходит первым.
8. **Зоологические карточки.** Предсказание того, как разложатся карточки.
9. **ДЛМ — экзаменатор.** Вопросы о вычислительной технике с комментариями машины. Там есть ответы в духе эпохи: «пульт управления, устройство считывания с перфокарт, магнитофон, печатающее устройство».
10. **Ваш характер.** Психологический тест из двенадцати вопросов. Здесь машину перепрограммируют прямо по ходу игры, по результатам каждой группы вопросов.
11. **Автоматическое управление лифтом.** Логика двери, мотора и направления движения.
## Ошибки в оригинале
Все 11 программ я перенёс в эмулятор точно по рисункам и проверил на всех комбинациях входов, а игру 4 ещё и решил полным перебором партий. Проверка лежит в репозитории, её можно запустить самому: `node check.mjs`. Игры 7 и 10 так проверить нельзя: в тексте мало примеров и нет полной таблицы ответов. Остальные работают ровно так, как написано в тексте, включая все примеры. А в двух нашлось кое-что любопытное.
Рисунки при этом сверены со сканами гнездо за гнездом. Есть и независимая проверка: на фотографии настоящей ДЛМ из статьи на Хабре набран сумматор, все три переключателя стоят на «1», горят V и Z. Эмулятор на той же программе зажигает те же лампы.
- **Игра 4.** Авторы честно предупреждают: «при достаточной практике Вы можете обыграть ДЛМ». Перебор всех партий показывает, где именно. Таких позиций четыре, и виновата не таблица ходов, а правило «если такой грани сбоку нет, берите меньшую, наиболее близкую». Самая обидная: сумма 6, сверху тройка. Машина советует 3, тройки сбоку нет, правило велит поставить 2, и выигрыш уходит противнику. А ведь сбоку лежит шестёрка, и она сразу даёт 12.
- **Игра 8.** Здесь ошибка в самой программе. Я перебрал все 34 650 раскладок карточек и сравнил, чем кончается игра, с тем, что показывает машина. В двух случаях из семи возможных она ошибается. Если на вопросы ответить «Да, Нет, Да», карточки всегда сходятся у птиц и зверей, то есть должна гореть Y, а не горит ничего. Если на все вопросы ответить «Нет», сойдутся только птицы, а машина зажигает Z и обещает, что «вероятно», сойдутся и остальные. Кстати, ответить «Да» на все три вопроса нельзя вообще: если внизу у «Насекомых» насекомое, а у «Зверей» зверь, птицы среди этих двух карточек быть не может.
Кто знает, сколько советских школьников в 1988 году ломали голову, почему машина в «Зоологических карточках» иногда ошибается.
Есть и опечатки попроще: на схеме панели третий переключатель подписан «Б-1» вместо «В-1», а в примере к игре 5 фишка достаётся «игроку № 8», хотя игроков всего трое.
## Об экземпляре
Брошюра 32 страницы, сдана в набор 17 февраля 1987 года, распространялась бесплатно, в комплекте с игрушкой. Ответственный редактор — Г. С. Стерин. Мой экземпляр сопровождал машину с заводским номером 005957, принятую ОТК в декабре 1987 года и проданную 23 февраля 1988-го.
В PDF есть и сканы всех страниц, и набранный текст. Отдельным приложением приведена расшифровка всех рисунков программ, чтобы машину можно было запрограммировать, не разглядывая мелкие кружочки.
## Благодарности
Устройство машины изнутри впервые подробно разобрано в статье «Криптография и другие вычисления на детских логических машинах» на Хабре: https://habr.com/ru/articles/807187/. Оттуда же в эмулятор попали логические элементы: НЕ, И-НЕ, ИЛИ-НЕ, исключающее ИЛИ, сумматор и XOR-шифр. Руководство подтвердило выводы автора: левое гнездо действительно срабатывает при «1».
Если у вас сохранилась своя ДЛМ, особенно с карточками, или есть фото её внутренностей, напишите. Хочется сделать эмулятор ещё точнее.