<!-- Enigma I emulator: rotor settings, plugboard, lampboard, signal path and a crib-dragging tool. -->
<p class="hint">A working <b>Enigma I</b>: three rotors chosen from I–V, reflector B, plugboard and ring settings, with the real double-step. Click the keys (or type on your keyboard) and watch a lamp light up — never the same letter you pressed.</p>
<div class="box">
<div class="lbl">Rotors (left · middle · right), ring settings and start position</div>
<div class="rotors" id="rotors"></div>
<label class="plugrow">Plugboard pairs
<input id="plugs" type="text" autocomplete="off" spellcheck="false" placeholder="e.g. AV BS CG DL">
</label>
<div class="btns">
<button id="presetTest" type="button">Preset: I-II-III · AAA</button>
<button id="presetBarb" type="button">Preset: Barbarossa 1941</button>
<button id="selftest" type="button" class="ghost">Run test vectors</button>
</div>
</div>
<div class="box">
<div class="lbl">Lampboard</div>
<div class="board" id="lamps"></div>
<div class="lbl">Keyboard (click or type)</div>
<div class="board" id="keys"></div>
<div class="status" id="status">Ready. Press a key: a different lamp will light.</div>
<div class="lbl">Signal path of the last key</div>
<div class="path" id="path"></div>
<div class="tape"><span class="tl">In</span><code id="tin"></code></div>
<div class="tape"><span class="tl">Out</span><code id="tout"></code></div>
<div class="btns">
<button id="reset" type="button" class="ghost">Back to start position</button>
<button id="clear" type="button" class="ghost">Clear tape</button>
</div>
<div class="lbl">Encrypt or decrypt a whole text</div>
<textarea id="txt" rows="3" placeholder="Paste letters here (spaces and punctuation are ignored)"></textarea>
<div class="btns"><button id="runText" type="button">Run through the machine</button></div>
</div>
<div class="box">
<div class="lbl">Crib dragging: where can a guessed word NOT go?</div>
<p class="small">Because Enigma never encrypts a letter to itself, a guessed plaintext word (a crib) cannot sit at any offset where one of its letters equals the ciphertext letter above it. Green offsets survive; red ones are ruled out.</p>
<label class="col">Ciphertext<textarea id="cribCt" rows="2" spellcheck="false"></textarea></label>
<label class="col">Crib (guessed plaintext)<input id="cribWord" type="text" autocomplete="off" spellcheck="false"></label>
<div class="btns"><button id="useOut" type="button" class="ghost">Use the tape output</button></div>
<div class="status" id="cribSum"></div>
<div class="cells" id="cribCells"></div>
<div class="status" id="cribDetail"></div>
<div class="align" id="cribAlign"></div>
</div>
/* Base layout and panels */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #1f2933; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.small { font-size: .82rem; color: #52606d; margin: .2rem 0 .5rem; line-height: 1.4; }
.box { border: 1px solid #d5dde5; border-radius: 10px; padding: .6rem; margin-bottom: .7rem; background: #f8fafc; }
.lbl { font: 700 .78rem system-ui, sans-serif; text-transform: uppercase; letter-spacing: .04em; color: #52606d; margin: .35rem 0 .3rem; }
/* Rotor slots with their visible window */
.rotors { display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem; }
.slot { display: flex; flex-direction: column; align-items: center; gap: 3px; background: #fff; border: 1px solid #d5dde5; border-radius: 8px; padding: .35rem .2rem; }
.slot-name { font-size: .75rem; color: #52606d; font-weight: 600; }
.win { width: 44px; border: 2px solid #1d3557; border-radius: 6px; background: #fffdf5; text-align: center; font: 700 15px ui-monospace, monospace; line-height: 1.15; padding: 2px 0; }
.win .cur { display: block; font-size: 22px; color: #1d3557; }
.win .side { display: block; color: #9aa5b1; font-size: 12px; }
.ring-l { font-size: .7rem; color: #52606d; }
select, input, textarea { font: 600 14px ui-monospace, monospace; border: 1px solid #b8c4d0; border-radius: 6px; padding: .25rem .35rem; background: #fff; color: #1f2933; max-width: 100%; }
textarea, .plugrow input, .col input { width: 100%; }
.plugrow, .col { display: block; font-size: .82rem; font-weight: 600; color: #3e4c59; margin: .5rem 0 .2rem; }
.arrow { font: 600 12px system-ui, sans-serif; padding: 0 .7rem; line-height: 1.4; }
/* Lampboard and keyboard in the German QWERTZ layout */
.board { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.row { display: flex; gap: 4px; }
.lamp, .key { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; font: 700 13px ui-monospace, monospace; user-select: none; }
.lamp { border-radius: 50%; background: #2b2f36; color: #6b7280; border: 1px solid #111; transition: background .15s, color .15s, box-shadow .15s; }
.lamp.on { background: #ffd23f; color: #3a2e00; box-shadow: 0 0 10px 3px rgba(255, 210, 63, .8); }
.key { border-radius: 50%; background: #1d3557; color: #fff; border: 2px solid #0e1d30; cursor: pointer; padding: 0; }
.key:active, .key.down { transform: translateY(1px); background: #457b9d; }
/* Signal-path chips, tapes and buttons */
.path { display: flex; flex-wrap: wrap; gap: 3px; align-items: center; min-height: 2.2em; }
.chip { display: inline-flex; flex-direction: column; align-items: center; min-width: 34px; padding: 2px 4px; border-radius: 6px; background: #e4e7eb; border: 1px solid #cbd2d9; opacity: .35; transition: opacity .15s, background .15s; }
.chip.lit { opacity: 1; background: #fff3c4; border-color: #e0b400; }
.chip.refl.lit { background: #ffd6d6; border-color: #c92f3c; }
.chip b { font: 700 15px ui-monospace, monospace; }
.chip small { font-size: 10px; color: #52606d; white-space: nowrap; }
.sep { color: #9aa5b1; font-size: 12px; }
.tape { display: flex; gap: .4rem; align-items: baseline; margin: .25rem 0; }
.tl { font-size: .75rem; font-weight: 700; color: #52606d; min-width: 2.6em; }
.tape code { font: 600 14px ui-monospace, monospace; word-break: break-all; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; margin: .45rem 0; min-height: 1.3em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin: .35rem 0; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .75rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Crib-dragging offsets and alignment */
.cells { display: flex; flex-wrap: wrap; gap: 3px; }
.cell { min-width: 28px; height: 26px; font: 600 11px ui-monospace, monospace; border-radius: 5px; border: 1px solid transparent; cursor: pointer; padding: 0 3px; }
.cell.ok { background: #d3f5df; color: #0a5c27; border-color: #8fd8a8; }
.cell.bad { background: #fde0e0; color: #9b1c25; border-color: #f2a5a5; }
.cell.sel { outline: 2px solid #1d3557; }
.align { overflow-x: auto; }
.arow { display: flex; gap: 2px; margin: 2px 0; }
.ac { width: 22px; height: 24px; display: flex; align-items: center; justify-content: center; font: 700 13px ui-monospace, monospace; border-radius: 4px; background: #e4e7eb; flex: 0 0 auto; }
.ac.crib { background: #dbe9f6; }
.ac.clash { background: #c92f3c; color: #fff; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Historical wirings of rotors I–V and the letter shown in the window when each one kicks the next rotor (turnover).
const AZ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const ROTORS = {
I: { w: 'EKMFLGDQVZNTOWYHXUSPAIBRCJ', n: 'Q' },
II: { w: 'AJDKSIRUXBLHWTMCQGZNPYFVOE', n: 'E' },
III: { w: 'BDFHJLCPRTXVZNYEIWGAKMUSQO', n: 'V' },
IV: { w: 'ESOVPZJAYQUIRHXLNFTGKDCMWB', n: 'J' },
V: { w: 'VZBRGITYUPSDNHLXAWMJQOFECK', n: 'Z' }
};
const NAMES = ['I', 'II', 'III', 'IV', 'V'];
// Reflector B (UKW-B): a fixed pairing of the 26 letters with no letter left alone.
const UKW_B = 'YRUHQSLDPXNGOKMIEBFZCWVJAT';
// Key and lamp rows of the real machine (QWERTZ).
const ROWS = ['QWERTZUIO', 'ASDFGHJK', 'PYXCVBNML'];
const SLOTS = ["Left", "Middle", "Right"];
// Part 1 of a German Army message from 7 July 1941, a classic published test case.
const BARB_CT = 'EDPUDNRGYSZRCXNUYTPOMRMBOFKTBZREZKMLXLVEFGUEYSIOZVEQMIKUBPMMYLKLTTDEISMDICAGYKUACTCDOMOHWXMUUIAUBSTSLRNBZSZWNRFXWFYSSXJZVIJHIDISHPRKLKAYUPADTXQSPINQMATLPIFSVKDASCTACDPBOPVHJK';
const idx = (c) => c.charCodeAt(0) - 65;
const mod = (x) => ((x % 26) + 26) % 26;
const $ = (id) => document.getElementById(id);
const letters = (s) => s.toUpperCase().replace(/[^A-Z]/g, '');
const groups = (s) => s.replace(/(.{5})/g, '$1 ').trim();
const identity = () => AZ.split('').map((_, i) => i);
// Machine state: rotor order (left, middle, right), ring settings, window positions and the plugboard map.
const S = { order: ['I', 'II', 'III'], rings: [0, 0, 0], pos: [0, 0, 0], start: [0, 0, 0], plug: identity() };
let tapeIn = '', tapeOut = '', lastLamp = -1;
// Parse cable pairs such as AV BS; reject reused letters. Returns null on error.
function parsePlugs(text) {
const map = identity();
const pairs = text.toUpperCase().split(/[^A-Z]+/).filter(Boolean);
const used = new Set();
for (const p of pairs) {
if (p.length !== 2 || p[0] === p[1] || used.has(p[0]) || used.has(p[1])) return null;
used.add(p[0]); used.add(p[1]);
map[idx(p[0])] = idx(p[1]); map[idx(p[1])] = idx(p[0]);
}
return { map, n: pairs.length };
}
// Stepping happens BEFORE the current flows. The right rotor always moves; the middle moves when the right sits on its notch;
// if the middle itself sits on its notch, it moves together with the left rotor — the famous double step.
function step(st) {
const atNotch = (k) => AZ[st.pos[k]] === ROTORS[st.order[k]].n;
const m = atNotch(1), r = atNotch(2);
if (m) { st.pos[0] = mod(st.pos[0] + 1); st.pos[1] = mod(st.pos[1] + 1); }
else if (r) st.pos[1] = mod(st.pos[1] + 1);
st.pos[2] = mod(st.pos[2] + 1);
return m;
}
// One pass through a rotor: offset by (position − ring), apply the wiring forward or inverse, undo the offset.
function through(st, k, c, inverse) {
const w = ROTORS[st.order[k]].w, sh = st.pos[k] - st.rings[k];
const e = mod(c + sh);
const o = inverse ? w.indexOf(AZ[e]) : idx(w[e]);
return mod(o - sh);
}
// Full key press: plugboard → right → middle → left → reflector → left⁻¹ → middle⁻¹ → right⁻¹ → plugboard.
function encipher(st, ch) {
const dbl = step(st);
const path = [];
let c = idx(ch); path.push(c);
c = st.plug[c]; path.push(c);
for (const k of [2, 1, 0]) { c = through(st, k, c, false); path.push(c); }
c = idx(UKW_B[c]); path.push(c);
for (const k of [0, 1, 2]) { c = through(st, k, c, true); path.push(c); }
c = st.plug[c]; path.push(c);
return { out: AZ[c], path, dbl };
}
function fresh(order, rings, pos, plugText) {
const p = parsePlugs(plugText || '');
return { order: order.slice(), rings: rings.split('').map(idx), pos: pos.split('').map(idx), plug: p ? p.map : identity() };
}
const runString = (st, s) => s.split('').map((ch) => encipher(st, ch).out).join('');
function setStatus(t, cls) { const s = $('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
// User interface: rotor selectors, windows with up/down buttons and ring selectors.
function mkBtn(label, aria, fn) {
const b = document.createElement('button'); b.type = 'button'; b.className = 'ghost arrow';
b.textContent = label; b.setAttribute('aria-label', aria); b.addEventListener('click', fn); return b;
}
function buildRotors() {
const box = $('rotors'); box.innerHTML = '';
for (let k = 0; k < 3; k++) {
const slot = document.createElement('div'); slot.className = 'slot';
const nm = document.createElement('div'); nm.className = 'slot-name'; nm.textContent = SLOTS[k];
const sel = document.createElement('select'); sel.setAttribute('aria-label', SLOTS[k]);
NAMES.forEach((n) => { const o = document.createElement('option'); o.value = n; o.textContent = n; sel.appendChild(o); });
sel.value = S.order[k];
sel.addEventListener('change', () => setRotor(k, sel.value));
const up = mkBtn('▲', "Turn rotor forward", () => turn(k, 1));
const win = document.createElement('div'); win.className = 'win'; win.id = 'win' + k;
const dn = mkBtn('▼', "Turn rotor back", () => turn(k, -1));
const rl = document.createElement('div'); rl.className = 'ring-l'; rl.textContent = "Ring";
const ring = document.createElement('select'); ring.setAttribute('aria-label', "Ring");
for (let i = 0; i < 26; i++) { const o = document.createElement('option'); o.value = String(i); o.textContent = AZ[i] + ' ' + String(i + 1).padStart(2, '0'); ring.appendChild(o); }
ring.value = String(S.rings[k]);
ring.addEventListener('change', () => { S.rings[k] = +ring.value; });
slot.append(nm, sel, up, win, dn, rl, ring);
box.appendChild(slot);
}
refreshWindows();
}
function refreshWindows() {
for (let k = 0; k < 3; k++) {
const p = S.pos[k];
$('win' + k).innerHTML = '<span class="side">' + AZ[mod(p - 1)] + '</span><span class="cur">' + AZ[p] + '</span><span class="side">' + AZ[mod(p + 1)] + '</span>';
}
}
// Each rotor exists once: choosing a rotor already in use swaps the two slots.
function setRotor(k, name) {
const other = S.order.indexOf(name);
if (other !== -1 && other !== k) S.order[other] = S.order[k];
S.order[k] = name;
buildRotors();
}
function turn(k, d) { S.pos[k] = mod(S.pos[k] + d); S.start = S.pos.slice(); refreshWindows(); }
function applyPlugs() {
const r = parsePlugs($('plugs').value);
if (!r) { S.plug = identity(); setStatus("Plugboard ignored: use pairs like AV BS, each letter at most once.", 'bad'); return false; }
S.plug = r.map; return true;
}
// Build the lampboard and the clickable keyboard.
function buildBoards() {
const lamps = $('lamps'), keys = $('keys');
ROWS.forEach((row) => {
const lr = document.createElement('div'); lr.className = 'row';
const kr = document.createElement('div'); kr.className = 'row';
row.split('').forEach((ch) => {
const l = document.createElement('div'); l.className = 'lamp'; l.id = 'lamp' + ch; l.textContent = ch; lr.appendChild(l);
const b = document.createElement('button'); b.type = 'button'; b.className = 'key'; b.textContent = ch;
b.addEventListener('click', () => pressKey(ch)); kr.appendChild(b);
});
lamps.appendChild(lr); keys.appendChild(kr);
});
}
// Show every intermediate letter of the last key press, lighting the chips in order.
function drawPath(path) {
const box = $('path'); box.innerHTML = '';
const inv = '⁻¹';
const labels = ["key", "plugboard", S.order[2], S.order[1], S.order[0], "reflector B", S.order[0] + inv, S.order[1] + inv, S.order[2] + inv, "plugboard", "lamp"];
const vals = path.concat([path[path.length - 1]]);
const chips = [];
labels.forEach((lab, i) => {
if (i) { const s = document.createElement('span'); s.className = 'sep'; s.textContent = '→'; box.appendChild(s); }
const c = document.createElement('div'); c.className = 'chip' + (i === 5 ? ' refl' : '');
c.innerHTML = '<b>' + AZ[vals[i]] + '</b><small></small>';
c.querySelector('small').textContent = lab;
box.appendChild(c); chips.push(c);
});
chips.forEach((c, i) => setTimeout(() => c.classList.add('lit'), 45 * i));
}
function renderTape() { $('tin').textContent = groups(tapeIn); $('tout').textContent = groups(tapeOut); }
function light(ch) {
if (lastLamp !== -1) $('lamp' + AZ[lastLamp]).classList.remove('on');
if (ch) { $('lamp' + ch).classList.add('on'); lastLamp = idx(ch); } else lastLamp = -1;
}
function pressKey(ch) {
if (!applyPlugs()) return;
const r = encipher(S, ch);
tapeIn += ch; tapeOut += r.out;
refreshWindows(); renderTape(); light(r.out); drawPath(r.path);
const msg = i18nFmt("%k% → %o% (the lamp can never be %k% itself).", { k: ch, o: r.out });
if (r.dbl) setStatus(msg + ' ' + "Double step: the middle rotor sat on its notch, so it moved again and pushed the left rotor.", 'ok'); else setStatus(msg, '');
}
// Push a whole text through the machine, letter by letter, from the current rotor positions.
function runText() {
if (!applyPlugs()) return;
const s = letters($('txt').value);
let last = null;
for (const ch of s) { last = encipher(S, ch); tapeIn += ch; tapeOut += last.out; }
refreshWindows(); renderTape();
if (last) { light(last.out); drawPath(last.path); }
setStatus(i18nFmt("Processed %n% letters.", { n: s.length }), 'ok');
renderCrib();
}
function load(order, rings, pos, plugs) {
S.order = order.slice(); S.rings = rings.split('').map(idx); S.pos = pos.split('').map(idx); S.start = S.pos.slice();
$('plugs').value = plugs; applyPlugs();
tapeIn = ''; tapeOut = ''; light(null); $('path').innerHTML = '';
buildRotors(); renderTape();
}
// Known-answer tests run on fresh machines, independent of the settings on screen.
function selfTest() {
const fails = [];
const a = runString(fresh(['I', 'II', 'III'], 'AAA', 'AAA'), 'AAAAA');
if (a !== 'BDZGO') fails.push('AAAAA→' + a);
const b = runString(fresh(['I', 'II', 'III'], 'BBB', 'AAA'), 'AAAAA');
if (b !== 'EWTYX') fails.push('BBB: AAAAA→' + b);
const st = fresh(['I', 'II', 'III'], 'AAA', 'ADU');
const seq = [];
for (let i = 0; i < 3; i++) { step(st); seq.push(st.pos.map((x) => AZ[x]).join('')); }
if (seq.join(',') !== 'ADV,AEW,BFX') fails.push('ADU→' + seq.join('→'));
const pt = runString(fresh(['II', 'IV', 'V'], 'BUL', 'BLA', 'AV BS CG DL FU HZ IN KM OW RX'), BARB_CT);
if (pt.indexOf('AUFKLXABTEILUNG') !== 0) fails.push('Barbarossa');
if (fails.length) setStatus(i18nFmt("✗ Test vector failed: %what%", { what: fails.join('; ') }), 'bad');
else setStatus("✓ All test vectors pass: AAAAA→BDZGO, rings BBB→EWTYX, double step ADU→ADV→AEW→BFX, and the 1941 Barbarossa message decrypts.", 'ok');
}
// Crib dragging: an offset is impossible if any crib letter equals the ciphertext letter at the same place.
let cribSel = -1;
function renderCrib() {
const ct = letters($('cribCt').value), crib = letters($('cribWord').value);
const cells = $('cribCells'); cells.innerHTML = ''; $('cribAlign').innerHTML = ''; $('cribDetail').textContent = '';
if (!crib || ct.length < crib.length) { $('cribSum').textContent = "Enter a ciphertext at least as long as the crib."; $('cribSum').className = 'status bad'; return; }
const total = ct.length - crib.length + 1;
const clashes = [];
for (let i = 0; i < total; i++) {
let n = 0;
for (let j = 0; j < crib.length; j++) if (ct[i + j] === crib[j]) n++;
clashes.push(n);
}
const ok = clashes.filter((n) => n === 0).length;
$('cribSum').textContent = i18nFmt("%ok% of %total% offsets are still possible; %bad% are ruled out by a letter that would map to itself.", { ok, total, bad: total - ok });
$('cribSum').className = 'status';
if (cribSel < 0 || cribSel >= total) cribSel = clashes.indexOf(0) === -1 ? 0 : clashes.indexOf(0);
clashes.forEach((n, i) => {
const b = document.createElement('button'); b.type = 'button';
b.className = 'cell ' + (n ? 'bad' : 'ok') + (i === cribSel ? ' sel' : '');
b.textContent = String(i);
b.addEventListener('click', () => { cribSel = i; renderCrib(); });
cells.appendChild(b);
});
const n = clashes[cribSel];
const verdict = n ? i18nFmt("impossible — %n% letter(s) would encrypt to themselves.", { n }) : "possible — no letter collides.";
$('cribDetail').textContent = i18nFmt("Offset %i%: %verdict%", { i: cribSel, verdict });
$('cribDetail').className = 'status ' + (n ? 'bad' : 'ok');
const r1 = document.createElement('div'); r1.className = 'arow';
const r2 = document.createElement('div'); r2.className = 'arow';
for (let j = 0; j < crib.length; j++) {
const c = ct[cribSel + j], p = crib[j];
const a = document.createElement('div'); a.className = 'ac' + (c === p ? ' clash' : ''); a.textContent = c; r1.appendChild(a);
const d = document.createElement('div'); d.className = 'ac crib' + (c === p ? ' clash' : ''); d.textContent = p; r2.appendChild(d);
}
$('cribAlign').append(r1, r2);
}
// Buttons and inputs.
$('presetTest').addEventListener('click', () => { load(['I', 'II', 'III'], 'AAA', 'AAA', ''); setStatus("Rotors I-II-III, reflector B, rings AAA, start AAA, no cables. Type AAAAA — you should get BDZGO.", 'ok'); });
$('presetBarb').addEventListener('click', () => { load(['II', 'IV', 'V'], 'BUL', 'BLA', 'AV BS CG DL FU HZ IN KM OW RX'); $('txt').value = groups(BARB_CT); setStatus("Barbarossa message loaded (rotors II-IV-V, rings BUL, start BLA, 10 cables). Press “Run through the machine” to read the German plaintext.", 'ok'); });
$('selftest').addEventListener('click', selfTest);
$('reset').addEventListener('click', () => { S.pos = S.start.slice(); tapeIn = ''; tapeOut = ''; light(null); $('path').innerHTML = ''; refreshWindows(); renderTape(); setStatus("Ready. Press a key: a different lamp will light.", ''); });
$('clear').addEventListener('click', () => { tapeIn = ''; tapeOut = ''; renderTape(); });
$('runText').addEventListener('click', runText);
$('plugs').addEventListener('change', () => { const r = parsePlugs($('plugs').value); if (r) { S.plug = r.map; setStatus(i18nFmt("Plugboard set: %n% cable(s).", { n: r.n }), 'ok'); } else applyPlugs(); });
$('useOut').addEventListener('click', () => { if (tapeOut) { $('cribCt').value = groups(tapeOut); cribSel = -1; renderCrib(); } });
$('cribCt').addEventListener('input', () => { cribSel = -1; renderCrib(); });
$('cribWord').addEventListener('input', () => { cribSel = -1; renderCrib(); });
// Typing on a physical keyboard presses the Enigma keys (except while editing a field).
document.addEventListener('keydown', (e) => {
const t = e.target && e.target.tagName;
if (t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT' || e.ctrlKey || e.metaKey || e.altKey) return;
const ch = (e.key || '').toUpperCase();
if (ch.length === 1 && ch >= 'A' && ch <= 'Z') { e.preventDefault(); pressKey(ch); }
});
buildBoards();
buildRotors();
renderTape();
$('cribCt').value = groups(BARB_CT.slice(0, 60));
$('cribWord').value = 'ABTEILUNG';
cribSel = 6;
renderCrib();
<!-- Enigma I emulator: rotor settings, plugboard, lampboard, signal path and a crib-dragging tool. -->
<p class="hint">A working <b>Enigma I</b>: three rotors chosen from I–V, reflector B, plugboard and ring settings, with the real double-step. Click the keys (or type on your keyboard) and watch a lamp light up — never the same letter you pressed.</p>
<div class="box">
<div class="lbl">Rotors (left · middle · right), ring settings and start position</div>
<div class="rotors" id="rotors"></div>
<label class="plugrow">Plugboard pairs
<input id="plugs" type="text" autocomplete="off" spellcheck="false" placeholder="e.g. AV BS CG DL">
</label>
<div class="btns">
<button id="presetTest" type="button">Preset: I-II-III · AAA</button>
<button id="presetBarb" type="button">Preset: Barbarossa 1941</button>
<button id="selftest" type="button" class="ghost">Run test vectors</button>
</div>
</div>
<div class="box">
<div class="lbl">Lampboard</div>
<div class="board" id="lamps"></div>
<div class="lbl">Keyboard (click or type)</div>
<div class="board" id="keys"></div>
<div class="status" id="status">Ready. Press a key: a different lamp will light.</div>
<div class="lbl">Signal path of the last key</div>
<div class="path" id="path"></div>
<div class="tape"><span class="tl">In</span><code id="tin"></code></div>
<div class="tape"><span class="tl">Out</span><code id="tout"></code></div>
<div class="btns">
<button id="reset" type="button" class="ghost">Back to start position</button>
<button id="clear" type="button" class="ghost">Clear tape</button>
</div>
<div class="lbl">Encrypt or decrypt a whole text</div>
<textarea id="txt" rows="3" placeholder="Paste letters here (spaces and punctuation are ignored)"></textarea>
<div class="btns"><button id="runText" type="button">Run through the machine</button></div>
</div>
<div class="box">
<div class="lbl">Crib dragging: where can a guessed word NOT go?</div>
<p class="small">Because Enigma never encrypts a letter to itself, a guessed plaintext word (a crib) cannot sit at any offset where one of its letters equals the ciphertext letter above it. Green offsets survive; red ones are ruled out.</p>
<label class="col">Ciphertext<textarea id="cribCt" rows="2" spellcheck="false"></textarea></label>
<label class="col">Crib (guessed plaintext)<input id="cribWord" type="text" autocomplete="off" spellcheck="false"></label>
<div class="btns"><button id="useOut" type="button" class="ghost">Use the tape output</button></div>
<div class="status" id="cribSum"></div>
<div class="cells" id="cribCells"></div>
<div class="status" id="cribDetail"></div>
<div class="align" id="cribAlign"></div>
</div>
/* Base layout and panels */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #1f2933; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.small { font-size: .82rem; color: #52606d; margin: .2rem 0 .5rem; line-height: 1.4; }
.box { border: 1px solid #d5dde5; border-radius: 10px; padding: .6rem; margin-bottom: .7rem; background: #f8fafc; }
.lbl { font: 700 .78rem system-ui, sans-serif; text-transform: uppercase; letter-spacing: .04em; color: #52606d; margin: .35rem 0 .3rem; }
/* Rotor slots with their visible window */
.rotors { display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem; }
.slot { display: flex; flex-direction: column; align-items: center; gap: 3px; background: #fff; border: 1px solid #d5dde5; border-radius: 8px; padding: .35rem .2rem; }
.slot-name { font-size: .75rem; color: #52606d; font-weight: 600; }
.win { width: 44px; border: 2px solid #1d3557; border-radius: 6px; background: #fffdf5; text-align: center; font: 700 15px ui-monospace, monospace; line-height: 1.15; padding: 2px 0; }
.win .cur { display: block; font-size: 22px; color: #1d3557; }
.win .side { display: block; color: #9aa5b1; font-size: 12px; }
.ring-l { font-size: .7rem; color: #52606d; }
select, input, textarea { font: 600 14px ui-monospace, monospace; border: 1px solid #b8c4d0; border-radius: 6px; padding: .25rem .35rem; background: #fff; color: #1f2933; max-width: 100%; }
textarea, .plugrow input, .col input { width: 100%; }
.plugrow, .col { display: block; font-size: .82rem; font-weight: 600; color: #3e4c59; margin: .5rem 0 .2rem; }
.arrow { font: 600 12px system-ui, sans-serif; padding: 0 .7rem; line-height: 1.4; }
/* Lampboard and keyboard in the German QWERTZ layout */
.board { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.row { display: flex; gap: 4px; }
.lamp, .key { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; font: 700 13px ui-monospace, monospace; user-select: none; }
.lamp { border-radius: 50%; background: #2b2f36; color: #6b7280; border: 1px solid #111; transition: background .15s, color .15s, box-shadow .15s; }
.lamp.on { background: #ffd23f; color: #3a2e00; box-shadow: 0 0 10px 3px rgba(255, 210, 63, .8); }
.key { border-radius: 50%; background: #1d3557; color: #fff; border: 2px solid #0e1d30; cursor: pointer; padding: 0; }
.key:active, .key.down { transform: translateY(1px); background: #457b9d; }
/* Signal-path chips, tapes and buttons */
.path { display: flex; flex-wrap: wrap; gap: 3px; align-items: center; min-height: 2.2em; }
.chip { display: inline-flex; flex-direction: column; align-items: center; min-width: 34px; padding: 2px 4px; border-radius: 6px; background: #e4e7eb; border: 1px solid #cbd2d9; opacity: .35; transition: opacity .15s, background .15s; }
.chip.lit { opacity: 1; background: #fff3c4; border-color: #e0b400; }
.chip.refl.lit { background: #ffd6d6; border-color: #c92f3c; }
.chip b { font: 700 15px ui-monospace, monospace; }
.chip small { font-size: 10px; color: #52606d; white-space: nowrap; }
.sep { color: #9aa5b1; font-size: 12px; }
.tape { display: flex; gap: .4rem; align-items: baseline; margin: .25rem 0; }
.tl { font-size: .75rem; font-weight: 700; color: #52606d; min-width: 2.6em; }
.tape code { font: 600 14px ui-monospace, monospace; word-break: break-all; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; margin: .45rem 0; min-height: 1.3em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin: .35rem 0; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .75rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Crib-dragging offsets and alignment */
.cells { display: flex; flex-wrap: wrap; gap: 3px; }
.cell { min-width: 28px; height: 26px; font: 600 11px ui-monospace, monospace; border-radius: 5px; border: 1px solid transparent; cursor: pointer; padding: 0 3px; }
.cell.ok { background: #d3f5df; color: #0a5c27; border-color: #8fd8a8; }
.cell.bad { background: #fde0e0; color: #9b1c25; border-color: #f2a5a5; }
.cell.sel { outline: 2px solid #1d3557; }
.align { overflow-x: auto; }
.arow { display: flex; gap: 2px; margin: 2px 0; }
.ac { width: 22px; height: 24px; display: flex; align-items: center; justify-content: center; font: 700 13px ui-monospace, monospace; border-radius: 4px; background: #e4e7eb; flex: 0 0 auto; }
.ac.crib { background: #dbe9f6; }
.ac.clash { background: #c92f3c; color: #fff; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Historical wirings of rotors I–V and the letter shown in the window when each one kicks the next rotor (turnover).
const AZ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const ROTORS = {
I: { w: 'EKMFLGDQVZNTOWYHXUSPAIBRCJ', n: 'Q' },
II: { w: 'AJDKSIRUXBLHWTMCQGZNPYFVOE', n: 'E' },
III: { w: 'BDFHJLCPRTXVZNYEIWGAKMUSQO', n: 'V' },
IV: { w: 'ESOVPZJAYQUIRHXLNFTGKDCMWB', n: 'J' },
V: { w: 'VZBRGITYUPSDNHLXAWMJQOFECK', n: 'Z' }
};
const NAMES = ['I', 'II', 'III', 'IV', 'V'];
// Reflector B (UKW-B): a fixed pairing of the 26 letters with no letter left alone.
const UKW_B = 'YRUHQSLDPXNGOKMIEBFZCWVJAT';
// Key and lamp rows of the real machine (QWERTZ).
const ROWS = ['QWERTZUIO', 'ASDFGHJK', 'PYXCVBNML'];
const SLOTS = ["Left", "Middle", "Right"];
// Part 1 of a German Army message from 7 July 1941, a classic published test case.
const BARB_CT = 'EDPUDNRGYSZRCXNUYTPOMRMBOFKTBZREZKMLXLVEFGUEYSIOZVEQMIKUBPMMYLKLTTDEISMDICAGYKUACTCDOMOHWXMUUIAUBSTSLRNBZSZWNRFXWFYSSXJZVIJHIDISHPRKLKAYUPADTXQSPINQMATLPIFSVKDASCTACDPBOPVHJK';
const idx = (c) => c.charCodeAt(0) - 65;
const mod = (x) => ((x % 26) + 26) % 26;
const $ = (id) => document.getElementById(id);
const letters = (s) => s.toUpperCase().replace(/[^A-Z]/g, '');
const groups = (s) => s.replace(/(.{5})/g, '$1 ').trim();
const identity = () => AZ.split('').map((_, i) => i);
// Machine state: rotor order (left, middle, right), ring settings, window positions and the plugboard map.
const S = { order: ['I', 'II', 'III'], rings: [0, 0, 0], pos: [0, 0, 0], start: [0, 0, 0], plug: identity() };
let tapeIn = '', tapeOut = '', lastLamp = -1;
// Parse cable pairs such as AV BS; reject reused letters. Returns null on error.
function parsePlugs(text) {
const map = identity();
const pairs = text.toUpperCase().split(/[^A-Z]+/).filter(Boolean);
const used = new Set();
for (const p of pairs) {
if (p.length !== 2 || p[0] === p[1] || used.has(p[0]) || used.has(p[1])) return null;
used.add(p[0]); used.add(p[1]);
map[idx(p[0])] = idx(p[1]); map[idx(p[1])] = idx(p[0]);
}
return { map, n: pairs.length };
}
// Stepping happens BEFORE the current flows. The right rotor always moves; the middle moves when the right sits on its notch;
// if the middle itself sits on its notch, it moves together with the left rotor — the famous double step.
function step(st) {
const atNotch = (k) => AZ[st.pos[k]] === ROTORS[st.order[k]].n;
const m = atNotch(1), r = atNotch(2);
if (m) { st.pos[0] = mod(st.pos[0] + 1); st.pos[1] = mod(st.pos[1] + 1); }
else if (r) st.pos[1] = mod(st.pos[1] + 1);
st.pos[2] = mod(st.pos[2] + 1);
return m;
}
// One pass through a rotor: offset by (position − ring), apply the wiring forward or inverse, undo the offset.
function through(st, k, c, inverse) {
const w = ROTORS[st.order[k]].w, sh = st.pos[k] - st.rings[k];
const e = mod(c + sh);
const o = inverse ? w.indexOf(AZ[e]) : idx(w[e]);
return mod(o - sh);
}
// Full key press: plugboard → right → middle → left → reflector → left⁻¹ → middle⁻¹ → right⁻¹ → plugboard.
function encipher(st, ch) {
const dbl = step(st);
const path = [];
let c = idx(ch); path.push(c);
c = st.plug[c]; path.push(c);
for (const k of [2, 1, 0]) { c = through(st, k, c, false); path.push(c); }
c = idx(UKW_B[c]); path.push(c);
for (const k of [0, 1, 2]) { c = through(st, k, c, true); path.push(c); }
c = st.plug[c]; path.push(c);
return { out: AZ[c], path, dbl };
}
function fresh(order, rings, pos, plugText) {
const p = parsePlugs(plugText || '');
return { order: order.slice(), rings: rings.split('').map(idx), pos: pos.split('').map(idx), plug: p ? p.map : identity() };
}
const runString = (st, s) => s.split('').map((ch) => encipher(st, ch).out).join('');
function setStatus(t, cls) { const s = $('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
// User interface: rotor selectors, windows with up/down buttons and ring selectors.
function mkBtn(label, aria, fn) {
const b = document.createElement('button'); b.type = 'button'; b.className = 'ghost arrow';
b.textContent = label; b.setAttribute('aria-label', aria); b.addEventListener('click', fn); return b;
}
function buildRotors() {
const box = $('rotors'); box.innerHTML = '';
for (let k = 0; k < 3; k++) {
const slot = document.createElement('div'); slot.className = 'slot';
const nm = document.createElement('div'); nm.className = 'slot-name'; nm.textContent = SLOTS[k];
const sel = document.createElement('select'); sel.setAttribute('aria-label', SLOTS[k]);
NAMES.forEach((n) => { const o = document.createElement('option'); o.value = n; o.textContent = n; sel.appendChild(o); });
sel.value = S.order[k];
sel.addEventListener('change', () => setRotor(k, sel.value));
const up = mkBtn('▲', "Turn rotor forward", () => turn(k, 1));
const win = document.createElement('div'); win.className = 'win'; win.id = 'win' + k;
const dn = mkBtn('▼', "Turn rotor back", () => turn(k, -1));
const rl = document.createElement('div'); rl.className = 'ring-l'; rl.textContent = "Ring";
const ring = document.createElement('select'); ring.setAttribute('aria-label', "Ring");
for (let i = 0; i < 26; i++) { const o = document.createElement('option'); o.value = String(i); o.textContent = AZ[i] + ' ' + String(i + 1).padStart(2, '0'); ring.appendChild(o); }
ring.value = String(S.rings[k]);
ring.addEventListener('change', () => { S.rings[k] = +ring.value; });
slot.append(nm, sel, up, win, dn, rl, ring);
box.appendChild(slot);
}
refreshWindows();
}
function refreshWindows() {
for (let k = 0; k < 3; k++) {
const p = S.pos[k];
$('win' + k).innerHTML = '<span class="side">' + AZ[mod(p - 1)] + '</span><span class="cur">' + AZ[p] + '</span><span class="side">' + AZ[mod(p + 1)] + '</span>';
}
}
// Each rotor exists once: choosing a rotor already in use swaps the two slots.
function setRotor(k, name) {
const other = S.order.indexOf(name);
if (other !== -1 && other !== k) S.order[other] = S.order[k];
S.order[k] = name;
buildRotors();
}
function turn(k, d) { S.pos[k] = mod(S.pos[k] + d); S.start = S.pos.slice(); refreshWindows(); }
function applyPlugs() {
const r = parsePlugs($('plugs').value);
if (!r) { S.plug = identity(); setStatus("Plugboard ignored: use pairs like AV BS, each letter at most once.", 'bad'); return false; }
S.plug = r.map; return true;
}
// Build the lampboard and the clickable keyboard.
function buildBoards() {
const lamps = $('lamps'), keys = $('keys');
ROWS.forEach((row) => {
const lr = document.createElement('div'); lr.className = 'row';
const kr = document.createElement('div'); kr.className = 'row';
row.split('').forEach((ch) => {
const l = document.createElement('div'); l.className = 'lamp'; l.id = 'lamp' + ch; l.textContent = ch; lr.appendChild(l);
const b = document.createElement('button'); b.type = 'button'; b.className = 'key'; b.textContent = ch;
b.addEventListener('click', () => pressKey(ch)); kr.appendChild(b);
});
lamps.appendChild(lr); keys.appendChild(kr);
});
}
// Show every intermediate letter of the last key press, lighting the chips in order.
function drawPath(path) {
const box = $('path'); box.innerHTML = '';
const inv = '⁻¹';
const labels = ["key", "plugboard", S.order[2], S.order[1], S.order[0], "reflector B", S.order[0] + inv, S.order[1] + inv, S.order[2] + inv, "plugboard", "lamp"];
const vals = path.concat([path[path.length - 1]]);
const chips = [];
labels.forEach((lab, i) => {
if (i) { const s = document.createElement('span'); s.className = 'sep'; s.textContent = '→'; box.appendChild(s); }
const c = document.createElement('div'); c.className = 'chip' + (i === 5 ? ' refl' : '');
c.innerHTML = '<b>' + AZ[vals[i]] + '</b><small></small>';
c.querySelector('small').textContent = lab;
box.appendChild(c); chips.push(c);
});
chips.forEach((c, i) => setTimeout(() => c.classList.add('lit'), 45 * i));
}
function renderTape() { $('tin').textContent = groups(tapeIn); $('tout').textContent = groups(tapeOut); }
function light(ch) {
if (lastLamp !== -1) $('lamp' + AZ[lastLamp]).classList.remove('on');
if (ch) { $('lamp' + ch).classList.add('on'); lastLamp = idx(ch); } else lastLamp = -1;
}
function pressKey(ch) {
if (!applyPlugs()) return;
const r = encipher(S, ch);
tapeIn += ch; tapeOut += r.out;
refreshWindows(); renderTape(); light(r.out); drawPath(r.path);
const msg = i18nFmt("%k% → %o% (the lamp can never be %k% itself).", { k: ch, o: r.out });
if (r.dbl) setStatus(msg + ' ' + "Double step: the middle rotor sat on its notch, so it moved again and pushed the left rotor.", 'ok'); else setStatus(msg, '');
}
// Push a whole text through the machine, letter by letter, from the current rotor positions.
function runText() {
if (!applyPlugs()) return;
const s = letters($('txt').value);
let last = null;
for (const ch of s) { last = encipher(S, ch); tapeIn += ch; tapeOut += last.out; }
refreshWindows(); renderTape();
if (last) { light(last.out); drawPath(last.path); }
setStatus(i18nFmt("Processed %n% letters.", { n: s.length }), 'ok');
renderCrib();
}
function load(order, rings, pos, plugs) {
S.order = order.slice(); S.rings = rings.split('').map(idx); S.pos = pos.split('').map(idx); S.start = S.pos.slice();
$('plugs').value = plugs; applyPlugs();
tapeIn = ''; tapeOut = ''; light(null); $('path').innerHTML = '';
buildRotors(); renderTape();
}
// Known-answer tests run on fresh machines, independent of the settings on screen.
function selfTest() {
const fails = [];
const a = runString(fresh(['I', 'II', 'III'], 'AAA', 'AAA'), 'AAAAA');
if (a !== 'BDZGO') fails.push('AAAAA→' + a);
const b = runString(fresh(['I', 'II', 'III'], 'BBB', 'AAA'), 'AAAAA');
if (b !== 'EWTYX') fails.push('BBB: AAAAA→' + b);
const st = fresh(['I', 'II', 'III'], 'AAA', 'ADU');
const seq = [];
for (let i = 0; i < 3; i++) { step(st); seq.push(st.pos.map((x) => AZ[x]).join('')); }
if (seq.join(',') !== 'ADV,AEW,BFX') fails.push('ADU→' + seq.join('→'));
const pt = runString(fresh(['II', 'IV', 'V'], 'BUL', 'BLA', 'AV BS CG DL FU HZ IN KM OW RX'), BARB_CT);
if (pt.indexOf('AUFKLXABTEILUNG') !== 0) fails.push('Barbarossa');
if (fails.length) setStatus(i18nFmt("✗ Test vector failed: %what%", { what: fails.join('; ') }), 'bad');
else setStatus("✓ All test vectors pass: AAAAA→BDZGO, rings BBB→EWTYX, double step ADU→ADV→AEW→BFX, and the 1941 Barbarossa message decrypts.", 'ok');
}
// Crib dragging: an offset is impossible if any crib letter equals the ciphertext letter at the same place.
let cribSel = -1;
function renderCrib() {
const ct = letters($('cribCt').value), crib = letters($('cribWord').value);
const cells = $('cribCells'); cells.innerHTML = ''; $('cribAlign').innerHTML = ''; $('cribDetail').textContent = '';
if (!crib || ct.length < crib.length) { $('cribSum').textContent = "Enter a ciphertext at least as long as the crib."; $('cribSum').className = 'status bad'; return; }
const total = ct.length - crib.length + 1;
const clashes = [];
for (let i = 0; i < total; i++) {
let n = 0;
for (let j = 0; j < crib.length; j++) if (ct[i + j] === crib[j]) n++;
clashes.push(n);
}
const ok = clashes.filter((n) => n === 0).length;
$('cribSum').textContent = i18nFmt("%ok% of %total% offsets are still possible; %bad% are ruled out by a letter that would map to itself.", { ok, total, bad: total - ok });
$('cribSum').className = 'status';
if (cribSel < 0 || cribSel >= total) cribSel = clashes.indexOf(0) === -1 ? 0 : clashes.indexOf(0);
clashes.forEach((n, i) => {
const b = document.createElement('button'); b.type = 'button';
b.className = 'cell ' + (n ? 'bad' : 'ok') + (i === cribSel ? ' sel' : '');
b.textContent = String(i);
b.addEventListener('click', () => { cribSel = i; renderCrib(); });
cells.appendChild(b);
});
const n = clashes[cribSel];
const verdict = n ? i18nFmt("impossible — %n% letter(s) would encrypt to themselves.", { n }) : "possible — no letter collides.";
$('cribDetail').textContent = i18nFmt("Offset %i%: %verdict%", { i: cribSel, verdict });
$('cribDetail').className = 'status ' + (n ? 'bad' : 'ok');
const r1 = document.createElement('div'); r1.className = 'arow';
const r2 = document.createElement('div'); r2.className = 'arow';
for (let j = 0; j < crib.length; j++) {
const c = ct[cribSel + j], p = crib[j];
const a = document.createElement('div'); a.className = 'ac' + (c === p ? ' clash' : ''); a.textContent = c; r1.appendChild(a);
const d = document.createElement('div'); d.className = 'ac crib' + (c === p ? ' clash' : ''); d.textContent = p; r2.appendChild(d);
}
$('cribAlign').append(r1, r2);
}
// Buttons and inputs.
$('presetTest').addEventListener('click', () => { load(['I', 'II', 'III'], 'AAA', 'AAA', ''); setStatus("Rotors I-II-III, reflector B, rings AAA, start AAA, no cables. Type AAAAA — you should get BDZGO.", 'ok'); });
$('presetBarb').addEventListener('click', () => { load(['II', 'IV', 'V'], 'BUL', 'BLA', 'AV BS CG DL FU HZ IN KM OW RX'); $('txt').value = groups(BARB_CT); setStatus("Barbarossa message loaded (rotors II-IV-V, rings BUL, start BLA, 10 cables). Press “Run through the machine” to read the German plaintext.", 'ok'); });
$('selftest').addEventListener('click', selfTest);
$('reset').addEventListener('click', () => { S.pos = S.start.slice(); tapeIn = ''; tapeOut = ''; light(null); $('path').innerHTML = ''; refreshWindows(); renderTape(); setStatus("Ready. Press a key: a different lamp will light.", ''); });
$('clear').addEventListener('click', () => { tapeIn = ''; tapeOut = ''; renderTape(); });
$('runText').addEventListener('click', runText);
$('plugs').addEventListener('change', () => { const r = parsePlugs($('plugs').value); if (r) { S.plug = r.map; setStatus(i18nFmt("Plugboard set: %n% cable(s).", { n: r.n }), 'ok'); } else applyPlugs(); });
$('useOut').addEventListener('click', () => { if (tapeOut) { $('cribCt').value = groups(tapeOut); cribSel = -1; renderCrib(); } });
$('cribCt').addEventListener('input', () => { cribSel = -1; renderCrib(); });
$('cribWord').addEventListener('input', () => { cribSel = -1; renderCrib(); });
// Typing on a physical keyboard presses the Enigma keys (except while editing a field).
document.addEventListener('keydown', (e) => {
const t = e.target && e.target.tagName;
if (t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT' || e.ctrlKey || e.metaKey || e.altKey) return;
const ch = (e.key || '').toUpperCase();
if (ch.length === 1 && ch >= 'A' && ch <= 'Z') { e.preventDefault(); pressKey(ch); }
});
buildBoards();
buildRotors();
renderTape();
$('cribCt').value = groups(BARB_CT.slice(0, 60));
$('cribWord').value = 'ABTEILUNG';
cribSel = 6;
renderCrib();
Comments
Loading comments...