Кубик показывает верх, низ и боковые грани; в игре 4 щелчок по боковой грани поворачивает кубик и ведёт счёт до 12 с ничьей. Карточки: раскладка по трём стопкам, просмотр нижних карточек, пошаговая игра и итог, с которым можно сверить ответ машины. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1041 lines
77 KiB
HTML
1041 lines
77 KiB
HTML
<!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 { 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:disabled { opacity: .45; cursor: default; border-color: var(--line); }
|
||
.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); grid-template-rows: auto 1fr; grid-template-areas: "progs tt cols" "progs book book"; gap: 24px; align-items: start; }
|
||
.progs-box { grid-area: progs; } .tt-box { grid-area: tt; } .cols-box { grid-area: cols; } .lower > .book { grid-area: book; }
|
||
@media (max-width: 1100px) { .lower { grid-template-columns: minmax(0, 1fr); grid-template-areas: "book" "progs" "tt" "cols"; } }
|
||
.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); }
|
||
.prog-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; align-items: stretch; }
|
||
.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: 24px clamp(16px, 3vw, 36px); display: grid; gap: 16px; }
|
||
.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; }
|
||
.sources { font-size: 14px; color: var(--muted); }
|
||
.sources a { color: var(--blue); }
|
||
|
||
/* the table: the die and the zoological cards, drawn after the photos of the kit */
|
||
.desk { display: grid; gap: 12px; border-top: 1px solid var(--line); padding-top: 16px; }
|
||
.desk h3 { font-family: var(--font-display); font-weight: 700; font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin: 0; }
|
||
.desk-body { display: grid; gap: 12px; justify-items: start; }
|
||
.desk-note { margin: 0; font-size: 14px; }
|
||
.desk-btns { display: flex; flex-wrap: wrap; gap: 8px; }
|
||
.die-row { display: flex; flex-wrap: wrap; gap: 20px; align-items: end; }
|
||
.die-row > div { display: grid; gap: 4px; justify-items: center; }
|
||
.face { width: 36px; height: 36px; display: block; }
|
||
.face.big { width: 64px; height: 64px; }
|
||
.face rect { fill: #fff; stroke: #2a2d33; stroke-width: 1.5; }
|
||
.face circle { fill: #2a2d33; }
|
||
.sides { display: flex; gap: 6px; }
|
||
.side { padding: 0; border: 0; background: none; cursor: pointer; border-radius: 7px; }
|
||
.side:disabled { cursor: default; opacity: .4; }
|
||
.side:not(:disabled):hover .face rect { stroke: var(--blue); stroke-width: 2.5; }
|
||
.side:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
|
||
.cap { font-size: 12px; color: var(--muted); }
|
||
.piles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; width: 100%; }
|
||
.pile { display: grid; gap: 8px; align-content: start; justify-items: start; padding: 8px; border-radius: 8px; border: 2px dashed transparent; min-width: 0; }
|
||
.pile.cur { border-color: var(--accent); }
|
||
.cards { display: flex; flex-wrap: wrap; gap: 4px; min-height: 56px; }
|
||
.cards.down .card + .card { margin-left: -58px; }
|
||
.card { width: 64px; height: 56px; background: #fff; border: 1px solid #b9c3d6; border-radius: 4px; display: grid; place-items: center; align-content: center; color: #23409a; font: 700 10px var(--font-body); box-shadow: 0 1px 2px rgba(0, 0, 0, .18); flex: none; }
|
||
.card svg { width: 42px; height: 30px; fill: none; stroke: #23409a; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
||
.card.label { width: auto; height: 30px; padding: 0 8px; font-size: 11px; letter-spacing: .05em; }
|
||
.card.back { background: repeating-linear-gradient(45deg, #23409a 0 4px, #3552ad 4px 8px); border-color: #1b2f73; }
|
||
</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>
|
||
<button class="ghost" type="button" id="save">Сохранить программу</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 class="progs-box">
|
||
<h2>Программы</h2>
|
||
<div class="programs" id="programs"></div>
|
||
</div>
|
||
<div class="tt-box">
|
||
<h2>Что ответит машина</h2>
|
||
<div class="tt-wrap"><table class="tt" id="tt"></table></div>
|
||
</div>
|
||
<div class="cols-box">
|
||
<h2>Столбцы программирования</h2>
|
||
<ul class="exprs" id="exprs"></ul>
|
||
</div>
|
||
<article class="book" id="book">
|
||
<h2 id="book-h">О программе</h2>
|
||
<section class="page" id="ch-other">
|
||
<h3 id="ch-title"></h3>
|
||
<p class="noind" id="ch-desc"></p>
|
||
<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>
|
||
</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>
|
||
</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>
|
||
</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>
|
||
</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>
|
||
</section>
|
||
<section class="page" id="game6">
|
||
<h3><span class="gn">Игра 6.</span> ДЛМ превосходит себя!</h3>
|
||
<p>Всё как в игре 5, но оператор не узнаёт, сколько спичек осталось. Он задаёт одному из игроков вопросы игры 1 про это число и читает ответ по коду игры 5.</p>
|
||
</section>
|
||
<section class="page" id="game7">
|
||
<h3><span class="gn">Игра 7.</span> Семь спичек</h3>
|
||
<p>Двое по очереди берут со стола одну или две спички в «общий котёл», пока стол не опустеет. Потом в том же порядке возвращают по одной-две. Выигрывает тот, кто положит седьмую. Противник ходит первым.</p>
|
||
<p>Вводите число спичек, уже взятых (во второй половине — уже положенных). Лампы покажут, сколько их должно стать после вашего хода. Во второй половине вместо 2 вводите 111, вместо 5 — 110.</p>
|
||
</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>
|
||
</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>
|
||
</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>
|
||
</section>
|
||
<section class="page" id="game11">
|
||
<h3><span class="gn">Игра 11.</span> Автоматическое управление лифтом</h3>
|
||
<p>А — направление до вызова (1 — вверх). Б — вызов вверх. В — вызов вниз.</p>
|
||
<p>X — дверь разблокирована. Y — мотор работает. Z — лифт поедет вверх. При двух вызовах сразу лифт сохраняет прежнее направление.</p>
|
||
</section>
|
||
<section class="desk" id="desk" hidden>
|
||
<h3>На столе</h3>
|
||
<div class="desk-body" id="desk-body"></div>
|
||
</section>
|
||
<section class="sources">
|
||
<div>Источник: «Игрушка Детская логическая машина ДЛМ. Руководство по эксплуатации». ВНИИ «Электронстандарт», Гатчина, 1987 — <a href="DLM_rukovodstvo_1987.pdf">скачать PDF</a> (сканы, набранный текст и расшифровка рисунков программ). Логические элементы взяты из статьи <a href="https://habr.com/ru/articles/807187/" target="_blank" rel="noopener">«Криптография и другие вычисления на детских логических машинах»</a> (Хабр).</div>
|
||
</section>
|
||
</article>
|
||
</section>
|
||
</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 };
|
||
|
||
// own programs: same shape as PROGRAMS, no group
|
||
const UKEY = 'dlm-user-programs';
|
||
let userProgs = [];
|
||
try { userProgs = JSON.parse(localStorage.getItem(UKEY)) || []; } catch (e) {}
|
||
const saveUser = () => { try { localStorage.setItem(UKEY, JSON.stringify(userProgs)); } catch (e) {} };
|
||
const allProgs = () => PROGRAMS.concat(userProgs);
|
||
|
||
function loadProgram(id) {
|
||
const p = allProgs().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);
|
||
|
||
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: '#2a2d33', 'stroke-width': 2 }, knob);
|
||
E('line', { x1: 896, y1: cy - 6, x2: 896, y2: cy + 12, stroke: '#2a2d33', 'stroke-width': 2 }, knob);
|
||
// the letter sits in a red dot, so the turn of the knob reads at a glance
|
||
E('circle', { cx: 896, cy: cy - 23, r: 12, fill: '#c8222c' }, knob);
|
||
E('text', { x: 896, y: cy - 17, 'text-anchor': 'middle', 'font-family': 'PT Sans, sans-serif', 'font-weight': 700, 'font-size': 17, fill: '#fff' }, 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: '#2a2d33' }, 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 });
|
||
// push button, normally open: a bridge over the two contacts, a stem and a pusher; pressing drops the bridge
|
||
const btnBlade = I('g', { style: 'transition: transform .12s ease' });
|
||
I('line', { x1: 90, y1: BUS_Y - 10, x2: 156, y2: BUS_Y - 10, stroke: BP.txt, 'stroke-width': 3, 'stroke-linecap': 'round' }, btnBlade);
|
||
I('line', { x1: 123, y1: BUS_Y - 10, x2: 123, y2: BUS_Y - 28, stroke: BP.txt, 'stroke-width': 2 }, btnBlade);
|
||
I('line', { x1: 113, y1: BUS_Y - 28, x2: 133, y2: BUS_Y - 28, stroke: BP.txt, 'stroke-width': 3, 'stroke-linecap': 'round' }, btnBlade);
|
||
T(123, BUS_Y - 36, 'кнопка', { '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'));
|
||
|
||
// ---- own programs, kept in this browser ----
|
||
document.getElementById('save').addEventListener('click', () => {
|
||
const cur = userProgs.find(p => p.id === S.prog);
|
||
const name = prompt('Название программы', cur ? cur.t : 'Моя программа')?.trim();
|
||
if (!name) return;
|
||
const p = {
|
||
id: 'u' + Date.now(), t: name, src: '', d: `Штеккеров: ${pinCount()}, перемычек: ${S.wires.length}.`,
|
||
rows: S.pins.map(row => row.map(([l, r]) => l && r ? 'B' : l ? 'L' : r ? 'R' : '.').join('')), wires: S.wires.map(w => [...w])
|
||
};
|
||
userProgs = userProgs.filter(x => x.t !== name).concat(p); // the same name overwrites
|
||
saveUser(); S.prog = p.id; renderPrograms(); update(); save();
|
||
});
|
||
function deleteUser(p) {
|
||
if (!confirm(`Удалить программу «${p.t}»?`)) return;
|
||
userProgs = userProgs.filter(x => x !== p);
|
||
if (S.prog === p.id) S.prog = null;
|
||
saveUser(); renderPrograms(); update(); save();
|
||
}
|
||
|
||
// ---- lower panels ----
|
||
const progBox = document.getElementById('programs');
|
||
let progBtns = [];
|
||
function renderPrograms() {
|
||
progBox.innerHTML = '';
|
||
let lastGroup = null;
|
||
progBtns = allProgs().map(p => {
|
||
const group = p.group || 'Мои программы';
|
||
if (group !== lastGroup) { lastGroup = group; const d = document.createElement('div'); d.className = 'group-label'; d.textContent = group; progBox.appendChild(d); }
|
||
const b = document.createElement('button');
|
||
b.type = 'button'; b.className = 'prog';
|
||
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));
|
||
if (p.group) progBox.appendChild(b);
|
||
else {
|
||
const row = document.createElement('div'), del = document.createElement('button');
|
||
row.className = 'prog-row';
|
||
del.type = 'button'; del.className = 'ghost'; del.textContent = '×'; del.title = 'Удалить';
|
||
del.setAttribute('aria-label', `Удалить программу «${p.t}»`);
|
||
del.addEventListener('click', () => deleteUser(p));
|
||
row.append(b, del); progBox.appendChild(row);
|
||
}
|
||
return [p.id, b];
|
||
});
|
||
}
|
||
renderPrograms();
|
||
|
||
// the chapter of the booklet for the chosen program, or a note on it
|
||
const book = document.getElementById('book'), chOther = document.getElementById('ch-other');
|
||
const OWN_HINT = 'Программа набрана вручную. Поставьте переключатели в среднее положение и щёлкайте по гнёздам, чтобы ставить и снимать штеккеры, и по двум пружинам подряд, чтобы соединить их перемычкой. Готовую программу сохраните кнопкой «Сохранить программу»: она останется в этом браузере.';
|
||
function renderChapter() {
|
||
const prog = allProgs().find(p => p.id === S.prog);
|
||
const chap = prog && book.querySelector('section.page#' + CSS.escape(prog.id));
|
||
book.querySelectorAll('section.page').forEach(s => { s.hidden = s !== (chap || chOther); });
|
||
document.getElementById('book-h').textContent = chap ? 'Из руководства 1987 года' : 'О программе';
|
||
if (chap) return;
|
||
document.getElementById('ch-title').textContent = prog ? prog.t : 'Своя программа';
|
||
document.getElementById('ch-desc').textContent = prog ? prog.d : OWN_HINT;
|
||
}
|
||
|
||
// ---- the table: the die for games 3 and 4, the cards for game 8 ----
|
||
// Only props: the player looks at them and sets the switches, as with the real machine.
|
||
const desk = document.getElementById('desk'), deskBody = document.getElementById('desk-body');
|
||
let deskFor;
|
||
const rnd = n => Math.floor(Math.random() * n);
|
||
const PIPS = { 1: [[1, 1]], 2: [[0, 0], [2, 2]], 3: [[0, 0], [1, 1], [2, 2]], 4: [[0, 0], [2, 0], [0, 2], [2, 2]],
|
||
5: [[0, 0], [2, 0], [1, 1], [0, 2], [2, 2]], 6: [[0, 0], [2, 0], [0, 1], [2, 1], [0, 2], [2, 2]] };
|
||
const face = (n, big) => `<svg class="face${big ? ' big' : ''}" viewBox="0 0 30 30" role="img" aria-label="${n}"><rect x="1" y="1" width="28" height="28" rx="6"/>` +
|
||
PIPS[n].map(([x, y]) => `<circle cx="${8 + x * 7}" cy="${8 + y * 7}" r="2.6"/>`).join('') + '</svg>';
|
||
|
||
function dieDesk(game4) {
|
||
let top = 1 + rnd(6), sum = top, log = [top];
|
||
const draw = () => {
|
||
const sides = [1, 2, 3, 4, 5, 6].filter(f => f !== top && f !== 7 - top);
|
||
const can = f => game4 && sum < 12 && sum + f <= 12;
|
||
const stuck = game4 && sum < 12 && !sides.some(can);
|
||
deskBody.innerHTML = `<div class="die-row">
|
||
<div>${face(top, true)}<span class="cap">сверху</span></div>
|
||
<div>${face(7 - top)}<span class="cap">снизу</span></div>
|
||
<div><div class="sides">${sides.map(f => `<button type="button" class="side" data-f="${f}" aria-label="Повернуть на ${f}"${can(f) ? '' : ' disabled'}>${face(f)}</button>`).join('')}</div>
|
||
<span class="cap">сбоку${game4 ? ': щелчок поворачивает кубик этой гранью вверх' : ''}</span></div>
|
||
</div>` +
|
||
(game4 ? `<p class="desk-note">Сумма: <b>${sum}</b>${sum > 7 && sum < 12 ? ` (в машину вводится ${sum - 8})` : ''}${sum === 12 ? '. Набрано 12!' : stuck ? '. Ходов нет: ничья.' : ''}<br><span class="cap">${log.join(' → ')}</span></p>` : '') +
|
||
`<button type="button" class="ghost" data-act="throw">${game4 ? 'Новая партия: бросить кубик' : 'Бросить кубик'}</button>`;
|
||
};
|
||
deskBody.onclick = e => {
|
||
const b = e.target.closest('button');
|
||
if (!b) return;
|
||
if (b.dataset.act === 'throw') { top = 1 + rnd(6); sum = top; log = [top]; }
|
||
else { top = +b.dataset.f; sum += top; log.push('+' + top + ' = ' + sum); }
|
||
draw();
|
||
};
|
||
draw();
|
||
}
|
||
|
||
// Game 8: four birds, four beasts, four insects; the labels lie by the piles in any order.
|
||
const KINDS = {
|
||
P: { label: 'ПТИЦЫ', names: ['гусь', 'утка', 'сова', 'воробей'],
|
||
icon: '<ellipse cx="18" cy="17" rx="10" ry="6"/><circle cx="29" cy="10" r="3.5"/><path d="M26 13 24 15M32 9.5 37 11 32 12.5M10 15Q18 10 24 16M8 17 3 13 4 20ZM16 23 15 28M21 23 22 28"/>' },
|
||
Z: { label: 'ЗВЕРИ', names: ['заяц', 'лиса', 'медведь', 'ёж'],
|
||
icon: '<ellipse cx="17" cy="18" rx="10" ry="6"/><circle cx="29" cy="13" r="4"/><path d="M28 9 26 2M31 9 34 2M11 23 9 28M23 23 25 28"/><circle cx="6" cy="17" r="2"/>' },
|
||
N: { label: 'НАСЕКОМЫЕ', names: ['жук', 'бабочка', 'стрекоза', 'муравей'],
|
||
icon: '<path d="M20 8V25M20 12C11 2 4 9 10 15 4 20 12 26 20 18M20 12C29 2 36 9 30 15 36 20 28 26 20 18M20 8 16 3M20 8 24 3"/>' }
|
||
};
|
||
const cardHtml = c => `<div class="card" title="${c.name}"><svg viewBox="0 0 40 30" aria-hidden="true">${KINDS[c.k].icon}</svg>${c.name}</div>`;
|
||
|
||
function cardsDesk() {
|
||
let order, piles, placed, cur, shown, done;
|
||
const deal = () => {
|
||
const deck = Object.keys(KINDS).flatMap(k => KINDS[k].names.map(name => ({ k, name })));
|
||
for (let i = deck.length - 1; i > 0; i--) { const j = rnd(i + 1); [deck[i], deck[j]] = [deck[j], deck[i]]; }
|
||
order = Object.keys(KINDS).sort(() => Math.random() - .5);
|
||
piles = { P: deck.slice(0, 4), Z: deck.slice(4, 8), N: deck.slice(8) }; // index 0 is the top card
|
||
placed = { P: [], Z: [], N: [] }; cur = 'P'; shown = false; done = false;
|
||
};
|
||
const step = () => {
|
||
const c = piles[cur].shift();
|
||
placed[c.k].push(c); cur = c.k;
|
||
done = !piles[cur].length;
|
||
};
|
||
const draw = () => {
|
||
const full = Object.keys(KINDS).filter(k => placed[k].length === 4).map(k => KINDS[k].label.toLowerCase());
|
||
deskBody.innerHTML = `<div class="piles">${order.map(k => {
|
||
const down = piles[k], bottom = down[down.length - 1];
|
||
return `<div class="pile${!done && cur === k ? ' cur' : ''}">
|
||
<div class="card label">${KINDS[k].label}</div>
|
||
<div class="cards down" aria-label="Рубашкой вверх: ${down.length}">${down.map((c, i) => shown && i === down.length - 1 && (k !== 'P') ? cardHtml(c) : '<div class="card back"></div>').join('')}</div>
|
||
<span class="cap">${down.length ? `рубашкой вверх: ${down.length}${shown && k !== 'P' ? `, нижняя — ${bottom.name}` : ''}` : 'пусто'}</span>
|
||
<div class="cards">${placed[k].map(cardHtml).join('')}</div>
|
||
</div>`;
|
||
}).join('')}</div>
|
||
<p class="desk-note">${done ? `Раскладка окончена. У своих надписей собрались все четыре карточки: ${full.join(', ')}.` :
|
||
Object.values(placed).some(x => x.length) ? 'Следующую карточку берите из стопки, обведённой красным.' :
|
||
shown ? 'Нижние карточки у «Насекомых» и «Зверей» открыты: ответьте по ним на вопросы А, Б, В и нажмите кнопку машины. Потом раскладывайте.' :
|
||
'Сначала посмотрите нижние карточки у надписей «Насекомые» и «Звери».'}</p>
|
||
<div class="desk-btns">
|
||
<button type="button" class="ghost" data-act="deal">Перемешать и разложить</button>
|
||
<button type="button" class="ghost" data-act="show"${shown ? ' disabled' : ''}>Посмотреть нижние карточки</button>
|
||
<button type="button" class="ghost" data-act="step"${done ? ' disabled' : ''}>Переложить карточку</button>
|
||
<button type="button" class="ghost" data-act="all"${done ? ' disabled' : ''}>Доиграть</button>
|
||
</div>`;
|
||
};
|
||
deskBody.onclick = e => {
|
||
const act = e.target.closest('button')?.dataset.act;
|
||
if (act === 'deal') deal();
|
||
else if (act === 'show') shown = true;
|
||
else if (act === 'step') step();
|
||
else if (act === 'all') while (!done) step();
|
||
else return;
|
||
draw();
|
||
};
|
||
deal(); draw();
|
||
}
|
||
|
||
function renderDesk() {
|
||
if (deskFor === S.prog) return;
|
||
deskFor = S.prog;
|
||
const make = { game3: () => dieDesk(false), game4: () => dieDesk(true), game8: cardsDesk }[S.prog];
|
||
desk.hidden = !make;
|
||
if (make) make(); else { deskBody.innerHTML = ''; deskBody.onclick = null; }
|
||
}
|
||
|
||
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.style.transform = `translateY(${power ? 7 : 0}px)`;
|
||
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();
|
||
renderChapter();
|
||
renderDesk();
|
||
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>
|