Files
dlm/index.html
T
Alex CubeandClaude Opus 5.5 7bfe0eb0d7 ДЛМ: оцифрованное руководство 1987 года, эмулятор и проверка программ
Эмулятор в одном файле index.html, руководство в PDF (сканы, набранный
текст, расшифровка рисунков программ). check.mjs сверяет все программы
с руководством на всех входах, игру 4 решает перебором партий, игру 8
проверяет перебором раскладок карточек.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:56:24 +03:00

866 lines
65 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>