diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f7525f5 --- /dev/null +++ b/.prettierignore @@ -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 \ No newline at end of file diff --git a/src/components.ts b/src/components.ts new file mode 100644 index 0000000..0ca6646 --- /dev/null +++ b/src/components.ts @@ -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 = { + 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 = { + 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 = { + 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 = { + "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; diff --git a/src/index.ts b/src/index.ts index 87232cd..2db8395 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,11 @@ 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("rings", machine.rings()); +console.log("THEQUICKBROWNFOXJUMPEDOVERTHELAZYDOG"); + +const cipher = machine.input("THEQUICKBROWNFOXJUMPEDOVERTHELAZYDOG"); +console.log(cipher); + +machine = new Machine("A", "I", "II", "III"); +console.log(machine.input(cipher)); diff --git a/src/loadComponentData.ts b/src/loadComponentData.ts deleted file mode 100644 index 0c5bcf9..0000000 --- a/src/loadComponentData.ts +++ /dev/null @@ -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; -}; - -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; -} diff --git a/src/machine.ts b/src/machine.ts index 327b2cd..a8f1b2e 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -1,29 +1,67 @@ import { - buildReflectors, - buildRotors, - ReflectorValues, - RotorValues, -} from "./loadComponentData"; + M3ReflectorName, + m3Reflectors, + M3RotorName, + m3Rotots as m3Rotors, + M4ReflectorName, + m4Reflectors, + M4RotorName, + m4Rotors, +} from "./components"; import { Plugboard } from "./plugboard"; import { Rotor } from "./rotors"; import { checkCharacter } from "./validation"; -const rotors = buildRotors("rotors.csv"); -const reflectors = buildReflectors("reflectors.csv"); - export class Machine { plugboard: Plugboard; - reflector: ReflectorValues; + reflector: Map; 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.reflector = getComponent(reflectors, reflector); + const reflectorComponent = getComponent( + { ...m3Reflectors, ...m4Reflectors }, + reflector, + ); - this.rotors = []; - this.rotors.push(getRotor(getComponent(rotors, first))); - this.rotors.push(getRotor(getComponent(rotors, second))); - this.rotors.push(getRotor(getComponent(rotors, third))); + this.reflector = new Map(); + for (let i = 0; i < 26; i++) { + const aCharCode = "A".charCodeAt(0); + 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[] { @@ -40,20 +78,42 @@ export class Machine { checkCharacter(char, "character"); 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 let scrambled = this.plugboard.in(char); // 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) { 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); } // reflector - scrambled = this.reflector.reflector.get(scrambled)!; + scrambled = this.reflector.get(scrambled)!; // rotors, second pass (out) for (let i = this.rotors.length - 1; i >= 0; i--) { @@ -63,22 +123,23 @@ export class Machine { // plugboard, second pass output += this.plugboard.in(scrambled); + + let state = ""; + for (const r of this.rotors) { + state += ` ${r.name}, ${r.offset}`; + } } return output; } } -function getRotor({ name, values, turnovers }: RotorValues) { - return new Rotor(name, values, turnovers, 0); -} - function getComponent( - components: T[], + components: Record, componentName: string, ): T { - const component = components.find(({ name }) => name === componentName); - if (!component) throw new Error(`unknown reflector: ${component}`); + const component = components[componentName]; + if (!component) throw new Error(`unknown component: ${component}`); return component; } diff --git a/src/plugboard.ts b/src/plugboard.ts index 2ff635c..98ee292 100644 --- a/src/plugboard.ts +++ b/src/plugboard.ts @@ -19,7 +19,7 @@ export class Plugboard { this.links.set(b, a); } - unlink(a: string) { + unstecker(a: string) { checkCharacter(a, "a"); if (!this.links.has(a)) return; diff --git a/src/rotors.ts b/src/rotors.ts index 359b5c9..1d745db 100644 --- a/src/rotors.ts +++ b/src/rotors.ts @@ -27,12 +27,12 @@ export class Rotor { 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 %= 26; + const char = String.fromCharCode("A".charCodeAt(0) + this.offset); + const turnover = this.turnovers.includes(char); + return turnover; } @@ -45,16 +45,17 @@ export class Rotor { const posNum = pos.charCodeAt(0); const input = (posNum - "A".charCodeAt(0) + this.offset) % 26; + return this.values[input]; } out(pos: string): string { checkCharacter(pos, "position"); - const i = this.values.findIndex((char) => char === pos); - const str = String.fromCharCode( - "A".charCodeAt(0) + ((i + this.offset) % 26), - ); + let i = this.values.findIndex((char) => char === pos); + i -= this.offset; + if (i < 0) i = 26 + i; + const str = String.fromCharCode("A".charCodeAt(0) + i); return str; }