Add double stepping, re-work component files, fix bugs.

This commit is contained in:
2025-07-12 19:19:15 -07:00
parent 0aea762463
commit 225c4abdf7
7 changed files with 293 additions and 114 deletions

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# prettier insists on making the array literals very long and thin. Aside from taking up
# space, it also makes it hard to tell, at a glance, if the array contains 26
# characters. I've opted to do my own little formatting in this file.
components.ts

187
src/components.ts Normal file
View File

@ -0,0 +1,187 @@
export type RotorComponent = {
name: M3RotorName | M4RotorName,
values: string[],
turnovers: string[],
}
export type ReflectorComponent = {
name: M3ReflectorName | M4ReflectorName,
reflector: string[],
}
export type M3RotorName = "I" | "II" | "III" | "IV" | "V" | "VI" | "VII" | "VIII";
export type M3ReflectorName = "A" | "B" | "C";
export type M4RotorName = "Beta" | "Gamma";
export type M4ReflectorName = "B Thin" | "C Thin";
export const m3Rotots: Record<M3RotorName, RotorComponent> = {
I: {
name: "I",
values: [
"E", "K", "M", "F", "L", "G",
"D", "Q", "V", "Z", "N", "T",
"O", "W", "Y", "H", "X", "U",
"S", "P", "A", "I", "B", "R",
"C", "J",
],
turnovers: ["R"],
},
II: {
name: "II",
values: [
"A", "J", "D", "K", "S", "I",
"R", "U", "X", "B", "L", "H",
"W", "T", "M", "C", "Q", "G",
"Z", "N", "P", "Y", "F", "V",
"O", "E",
],
turnovers: ["F"],
},
III: {
name: "III",
values: [
"B", "D", "F", "H", "J", "L",
"C", "P", "R", "T", "X", "V",
"Z", "N", "Y", "E", "I", "W",
"G", "A", "K", "M", "U", "S",
"Q", "O",
],
turnovers: ["W"],
},
IV: {
name: "IV",
values: [
"E", "S", "O", "V", "P", "Z",
"J", "A", "Y", "Q", "U", "I",
"R", "H", "X", "L", "N", "F",
"T", "G", "K", "D", "C", "M",
"W", "B",
],
turnovers: ["K"],
},
V: {
name: "V",
values: [
"V", "Z", "B", "R", "G", "I",
"T", "Y", "U", "P", "S", "D",
"N", "H", "L", "X", "A", "W",
"M", "J", "Q", "O", "F", "E",
"C", "K",
],
turnovers: ["A"],
},
VI: {
name: "VI",
values: [
"J", "P", "G", "V", "O", "U",
"M", "F", "Y", "Q", "B", "E",
"N", "H", "Z", "R", "D", "K",
"A", "S", "X", "L", "I", "C",
"T", "W",
],
turnovers: ["A", "N"],
},
VII: {
name: "VII",
values: [
"N", "Z", "J", "H", "G", "R",
"C", "X", "M", "Y", "S", "W",
"B", "O", "U", "F", "A", "I",
"V", "L", "P", "E", "K", "Q",
"D", "T",
],
turnovers: ["A", "N"],
},
VIII: {
name: "VIII",
values: [
"F", "K", "Q", "H", "T", "L",
"X", "O", "C", "B", "J", "S",
"P", "D", "Z", "R", "A", "M",
"E", "W", "N", "I", "U", "Y",
"G", "V",
],
turnovers: ["A", "N"],
},
};
export const m3Reflectors: Record<M3ReflectorName, ReflectorComponent> = {
A: {
name: "A",
reflector: [
"E", "J", "M", "Z", "A", "L",
"Y", "X", "V", "B", "W", "F",
"C", "R", "Q", "U", "O", "N",
"T", "S", "P", "I", "K", "H",
"G", "D",
],
},
B: {
name: "B",
reflector: [
"Y", "R", "U", "H", "Q", "S",
"L", "D", "P", "X", "N", "G",
"O", "K", "M", "I", "E", "B",
"F", "Z", "C", "W", "V", "J",
"A", "T",
],
},
C: {
name: "C",
reflector: [
"F", "V", "P", "J", "I", "A",
"O", "Y", "E", "D", "R", "Z",
"X", "W", "G", "C", "T", "K",
"U", "Q", "S", "B", "N", "M",
"H", "L",
],
},
};
export const m4Rotors: Record<M4RotorName, RotorComponent> = {
Beta: {
name: "Beta",
values: [
"L", "E", "Y", "J", "V", "C",
"N", "I", "X", "W", "P", "B",
"Q", "M", "D", "R", "T", "A",
"K", "Z", "G", "F", "U", "H",
"O", "S",
],
turnovers: [],
},
Gamma: {
name: "Gamma",
values: [
"F", "S", "O", "K", "A", "N",
"U", "E", "R", "H", "M", "B",
"T", "I", "Y", "C", "W", "L",
"Q", "P", "Z", "X", "V", "G",
"J", "D",
],
turnovers: [],
},
};
export const m4Reflectors: Record<M4ReflectorName, ReflectorComponent> = {
"B Thin": {
name: "B Thin",
reflector: [
"E", "N", "K", "Q", "A", "U",
"Y", "W", "J", "I", "C", "O",
"P", "B", "L", "M", "D", "X",
"Z", "V", "F", "T", "H", "R",
"G", "S",
],
},
"C Thin": {
name: "C Thin",
reflector: [
"R", "D", "O", "B", "J", "N",
"T", "K", "V", "E", "H", "M",
"L", "F", "C", "W", "Z", "A",
"X", "G", "Y", "I", "P", "S",
"U", "Q",
],
},
}as const;

View File

@ -1,6 +1,11 @@
import { Machine } from "./machine"; import { Machine } from "./machine";
const machine = new Machine("A", "I", "II", "III"); let machine = new Machine("A", "I", "II", "III");
console.log(machine.input("THISISAMESSAGEFORYOU")); console.log("THEQUICKBROWNFOXJUMPEDOVERTHELAZYDOG");
console.log("rings", machine.rings());
const cipher = machine.input("THEQUICKBROWNFOXJUMPEDOVERTHELAZYDOG");
console.log(cipher);
machine = new Machine("A", "I", "II", "III");
console.log(machine.input(cipher));

View File

@ -1,79 +0,0 @@
import fs from "node:fs";
export type RotorValues = {
name: string;
values: string[];
turnovers: string[];
};
export type ReflectorValues = {
name: string;
reflector: Map<string, string>;
};
function loadFile(filename: string) {
const data = fs.readFileSync(filename, "utf-8");
let row: string[] = [];
const table: string[][] = [];
let cell = "";
for (const char of data) {
if (char !== "," && char !== "\n") {
cell += char;
} else {
row.push(cell);
cell = "";
if (char === "\n") {
table.push(row);
row = [];
}
}
}
// if the file ends in a line break, this isn't needed but if it doesn't, then there
// is a rotor hanging out at the end that still needs to be added.
if (row.length > 0) {
row.push(cell);
table.push(row);
}
return table;
}
export function buildRotors(filename: string) {
const rotors: RotorValues[] = [];
const table = loadFile(filename);
for (const [name, valueString, turnovers] of table) {
rotors.push({
name,
values: valueString.split(""),
turnovers: turnovers.split(""),
});
}
return rotors;
}
export function buildReflectors(filename: string) {
const reflectors: ReflectorValues[] = [];
const table = loadFile(filename);
for (const [name, valueString] of table) {
const values = valueString.split("");
const reflector = { name, reflector: new Map() };
for (let i = 0; i < values.length; i++) {
const a = String.fromCharCode("A".charCodeAt(0) + i);
const b = values[i];
reflector.reflector.set(a, b);
reflector.reflector.set(b, a);
}
reflectors.push(reflector);
}
return reflectors;
}

View File

@ -1,29 +1,67 @@
import { import {
buildReflectors, M3ReflectorName,
buildRotors, m3Reflectors,
ReflectorValues, M3RotorName,
RotorValues, m3Rotots as m3Rotors,
} from "./loadComponentData"; M4ReflectorName,
m4Reflectors,
M4RotorName,
m4Rotors,
} from "./components";
import { Plugboard } from "./plugboard"; import { Plugboard } from "./plugboard";
import { Rotor } from "./rotors"; import { Rotor } from "./rotors";
import { checkCharacter } from "./validation"; import { checkCharacter } from "./validation";
const rotors = buildRotors("rotors.csv");
const reflectors = buildReflectors("reflectors.csv");
export class Machine { export class Machine {
plugboard: Plugboard; plugboard: Plugboard;
reflector: ReflectorValues; reflector: Map<string, string>;
rotors: Rotor[]; rotors: Rotor[];
constructor(reflector: string, first: string, second: string, third: string) { constructor(
reflector: M3ReflectorName,
first: M3RotorName,
second: M3RotorName,
third: M3RotorName,
);
constructor(
reflector: M4ReflectorName,
first: M3RotorName,
second: M3RotorName,
third: M3RotorName,
fourth: M4RotorName,
);
constructor(
reflector: M3ReflectorName | M4ReflectorName,
first: M3RotorName,
second: M3RotorName,
third: M3RotorName,
fourth?: M4RotorName,
) {
this.plugboard = new Plugboard(); this.plugboard = new Plugboard();
this.reflector = getComponent(reflectors, reflector); const reflectorComponent = getComponent(
{ ...m3Reflectors, ...m4Reflectors },
reflector,
);
this.rotors = []; this.reflector = new Map();
this.rotors.push(getRotor(getComponent(rotors, first))); for (let i = 0; i < 26; i++) {
this.rotors.push(getRotor(getComponent(rotors, second))); const aCharCode = "A".charCodeAt(0);
this.rotors.push(getRotor(getComponent(rotors, third))); this.reflector.set(
String.fromCharCode(aCharCode + i),
reflectorComponent.reflector[i],
);
}
const rotorComponents = [
getComponent(m3Rotors, first),
getComponent(m3Rotors, second),
getComponent(m3Rotors, third),
];
if (fourth) rotorComponents.push(getComponent(m4Rotors, fourth));
this.rotors = rotorComponents.map(
({ name, values, turnovers }) => new Rotor(name, values, turnovers),
);
} }
rings(): string[] { rings(): string[] {
@ -40,20 +78,42 @@ export class Machine {
checkCharacter(char, "character"); checkCharacter(char, "character");
let turnover = true; let turnover = true;
// There is a rare case when rotor three turns, and it does not cause rotor
// two to turn (double-step): on the FIRST keypress when BOTH wheels are set
// to turn. In that case, it's important not to set the second wheel as the
// third one steps, since it will already have stepped and stepping twice in
// one keypress is not possible mechanically.
let rotorTwoStepped = false;
// plugboard, first pass // plugboard, first pass
let scrambled = this.plugboard.in(char); let scrambled = this.plugboard.in(char);
// rotors, first pass (in) // rotors, first pass (in)
for (const rotor of this.rotors) { for (let i = 0; i < this.rotors.length; i++) {
const rotor = this.rotors[i];
if (turnover) { if (turnover) {
turnover = rotor.rotate(); turnover = rotor.rotate();
if (i === 1) {
rotorTwoStepped = true;
}
// Because of a quirk in the machines design, if the third rotor
// rotates, the second rotor also rotates. This funny action was
// called "double-stepping". Although Naval Enigma added a fourth
// rotor, it never causes the third rotor to double-step because
// the fourth rotor never actually rotated. Rotor one is not
// affected because it always rotates anyway.
if (i === 2 && !rotorTwoStepped) {
this.rotors[1].rotate();
}
} }
scrambled = rotor.in(scrambled); scrambled = rotor.in(scrambled);
} }
// reflector // reflector
scrambled = this.reflector.reflector.get(scrambled)!; scrambled = this.reflector.get(scrambled)!;
// rotors, second pass (out) // rotors, second pass (out)
for (let i = this.rotors.length - 1; i >= 0; i--) { for (let i = this.rotors.length - 1; i >= 0; i--) {
@ -63,22 +123,23 @@ export class Machine {
// plugboard, second pass // plugboard, second pass
output += this.plugboard.in(scrambled); output += this.plugboard.in(scrambled);
let state = "";
for (const r of this.rotors) {
state += ` ${r.name}, ${r.offset}`;
}
} }
return output; return output;
} }
} }
function getRotor({ name, values, turnovers }: RotorValues) {
return new Rotor(name, values, turnovers, 0);
}
function getComponent<T extends { name: string }>( function getComponent<T extends { name: string }>(
components: T[], components: Record<string, T>,
componentName: string, componentName: string,
): T { ): T {
const component = components.find(({ name }) => name === componentName); const component = components[componentName];
if (!component) throw new Error(`unknown reflector: ${component}`); if (!component) throw new Error(`unknown component: ${component}`);
return component; return component;
} }

View File

@ -19,7 +19,7 @@ export class Plugboard {
this.links.set(b, a); this.links.set(b, a);
} }
unlink(a: string) { unstecker(a: string) {
checkCharacter(a, "a"); checkCharacter(a, "a");
if (!this.links.has(a)) return; if (!this.links.has(a)) return;

View File

@ -27,12 +27,12 @@ export class Rotor {
throw new Error("offset must be an integer"); throw new Error("offset must be an integer");
} }
const char = this.values[this.offset];
const turnover = this.turnovers.includes(char);
this.offset += times; this.offset += times;
this.offset %= 26; this.offset %= 26;
const char = String.fromCharCode("A".charCodeAt(0) + this.offset);
const turnover = this.turnovers.includes(char);
return turnover; return turnover;
} }
@ -45,16 +45,17 @@ export class Rotor {
const posNum = pos.charCodeAt(0); const posNum = pos.charCodeAt(0);
const input = (posNum - "A".charCodeAt(0) + this.offset) % 26; const input = (posNum - "A".charCodeAt(0) + this.offset) % 26;
return this.values[input]; return this.values[input];
} }
out(pos: string): string { out(pos: string): string {
checkCharacter(pos, "position"); checkCharacter(pos, "position");
const i = this.values.findIndex((char) => char === pos); let i = this.values.findIndex((char) => char === pos);
const str = String.fromCharCode( i -= this.offset;
"A".charCodeAt(0) + ((i + this.offset) % 26), if (i < 0) i = 26 + i;
); const str = String.fromCharCode("A".charCodeAt(0) + i);
return str; return str;
} }