diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 58e70571..2468bffe 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -115,7 +115,8 @@ "Bombe", "Multiple Bombe", "Typex", - "Lorenz" + "Lorenz", + "Colossus" ] }, { diff --git a/src/core/lib/Colossus.mjs b/src/core/lib/Colossus.mjs new file mode 100644 index 00000000..5fb8e0cc --- /dev/null +++ b/src/core/lib/Colossus.mjs @@ -0,0 +1,417 @@ +/** + * Colossus - an emulation of the world's first electronic computer + * + * @author VirtualColossus [martin@virtualcolossus.co.uk] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import {INIT_PATTERNS, ITA2_TABLE, ROTOR_SIZES} from "../lib/Lorenz.mjs"; + +/** + * Colossus simulator class. + */ +export class ColossusComputer { + /** + * Construct a Colossus. + * + * @param {string} ciphertext + * @param {string} pattern - named pattern of Chi, Mu and Psi wheels + * @param {Object} qbusin - which data inputs are being sent to q bus - each can be null, plain or delta + * @param {Object[]} qbusswitches - Q bus calculation switches, multiple rows + * @param {Object} control - control switches which specify stepping modes + * @param {Object} starts - rotor start positions + */ + constructor(ciphertext, pattern, qbusin, qbusswitches, control, starts, settotal, limit) { + this.ITAlookup = ITA2_TABLE; + this.ReverseITAlookup = {}; + for (const letter in this.ITAlookup) { + const code = this.ITAlookup[letter]; + this.ReverseITAlookup[code] = letter; + } + + this.initThyratrons(pattern); + + this.ciphertext = ciphertext; + this.qbusin = qbusin; + this.qbusswitches = qbusswitches; + this.control = control; + this.starts = starts; + this.settotal = settotal; + this.limitations = limit; + + + this.allCounters = [0, 0, 0, 0, 0]; + + this.Zbits = [0, 0, 0, 0, 0]; // Z input is the cipher tape + this.ZbitsOneBack = [0, 0, 0, 0, 0]; // for delta + this.Qbits = [0, 0, 0, 0, 0]; // input generated for placing onto the Q-bus (the logic processor) + + this.Xbits = [0, 0, 0, 0, 0]; // X is the Chi wheel bits + this.Xptr = [0, 0, 0, 0, 0]; // pointers to the current X bits (Chi wheels) + this.XbitsOneBack = [0, 0, 0, 0, 0]; // the X bits one back (for delta) + + this.Sbits = [0, 0, 0, 0, 0]; // S is the Psi wheel bits + this.Sptr = [0, 0, 0, 0, 0]; // pointers to the current S bits (Psi wheels) + this.SbitsOneBack = [0, 0, 0, 0, 0]; // the S bits one back (for delta) + + this.Mptr = [0, 0]; + + this.rotorPtrs = {}; + + this.totalmotor = 0; + this.P5Zbit = [0, 0]; + } + + /** + * Begin a run + * + * @returns {object} + */ + run() { + const result = { + printout: "" + }; + + // loop until our start positions are back to the beginning + this.rotorPtrs = {X1: this.starts.X1, X2: this.starts.X2, X3: this.starts.X3, X4: this.starts.X4, X5: this.starts.X5, M61: this.starts.M61, M37: this.starts.M37, S1: this.starts.S1, S2: this.starts.S2, S3: this.starts.S3, S4: this.starts.S4, S5: this.starts.S5}; + // this.rotorPtrs = this.starts; + let runcount = 1; + + const fast = this.control.fast; + const slow = this.control.slow; + + // Print Headers + result.printout += fast + " " + slow + "\n"; + + do { + this.allCounters = [0, 0, 0, 0, 0]; + this.ZbitsOneBack = [0, 0, 0, 0, 0]; + this.XbitsOneBack = [0, 0, 0, 0, 0]; + + // Run full tape loop and process counters + this.runTape(); + + // Only print result if larger than set total + let fastRef = "00"; + let slowRef = "00"; + if (fast !== "") fastRef = this.rotorPtrs[fast].toString().padStart(2, "0"); + if (slow !== "") slowRef = this.rotorPtrs[slow].toString().padStart(2, "0"); + let printline = ""; + for (let c=0;c<5;c++) { + if (this.allCounters[c] > this.settotal) { + printline += String.fromCharCode(c+97) + this.allCounters[c]+" "; + } + } + if (printline !== "") { + result.printout += fastRef + " " + slowRef + " : "; + result.printout += printline; + result.printout += "\n"; + } + + // Step fast rotor if required + if (fast !== "") { + this.rotorPtrs[fast]++; + if (this.rotorPtrs[fast] > ROTOR_SIZES[fast]) this.rotorPtrs[fast] = 1; + } + + // Step slow rotor if fast rotor has returned to initial start position + if (slow !== "" && this.rotorPtrs[fast] === this.starts[fast]) { + this.rotorPtrs[slow]++; + if (this.rotorPtrs[slow] > ROTOR_SIZES[slow]) this.rotorPtrs[slow] = 1; + } + + runcount++; + } while (JSON.stringify(this.rotorPtrs) !== JSON.stringify(this.starts)); + + result.counters = this.allCounters; + result.runcount = runcount; + + return result; + } + + /** + * Run tape loop + */ + runTape() { + let charZin = ""; + + this.Xptr = [this.rotorPtrs.X1, this.rotorPtrs.X2, this.rotorPtrs.X3, this.rotorPtrs.X4, this.rotorPtrs.X5]; + this.Mptr = [this.rotorPtrs.M37, this.rotorPtrs.M61]; + this.Sptr = [this.rotorPtrs.S1, this.rotorPtrs.S2, this.rotorPtrs.S3, this.rotorPtrs.S4, this.rotorPtrs.S5]; + + // Run full loop of all character on the input tape (Z) + for (let i=0; i ROTOR_SIZES["X"+(r+1)]) this.Xptr[r] = 1; + } + + if (this.totalmotor) { + // Step Psi rotors + for (let r=0; r<5; r++) { + this.Sptr[r]++; + if (this.Sptr[r] > ROTOR_SIZES["S"+(r+1)]) this.Sptr[r] = 1; + } + } + + // Move M37 rotor if M61 set + if (this.rings.M[1][this.Mptr[1]-1]===1) this.Mptr[0]++; + if (this.Mptr[0] > ROTOR_SIZES.M37) this.Mptr[0]=1; + + // Always move M61 rotor + this.Mptr[1]++; + if (this.Mptr[1] > ROTOR_SIZES.M61) this.Mptr[1]=1; + } + + /** + * Get Q bus inputs + */ + getQbusInputs(charZin) { + // Zbits - the bits from the current character from the cipher tape. + this.Zbits = this.ITAlookup[charZin].split(""); + if (this.qbusin.Z === "Z") { + // direct Z + this.Qbits = this.Zbits; + } else if (this.qbusin.Z === "ΔZ") { + // delta Z, the Bitwise XOR of this character Zbits + last character Zbits + for (let b=0;b<5;b++) { + this.Qbits[b] = this.Zbits[b] ^ this.ZbitsOneBack[b]; + } + } + this.ZbitsOneBack = this.Zbits.slice(); // copy value of object, not reference + + // Xbits - the current Chi wheel bits + for (let b=0;b<5;b++) { + this.Xbits[b] = this.rings.X[b+1][this.Xptr[b]-1]; + } + if (this.qbusin.Chi !== "") { + if (this.qbusin.Chi === "Χ") { + // direct X added to Qbits + for (let b=0;b<5;b++) { + this.Qbits[b] = this.Qbits[b] ^ this.Xbits[b]; + } + } else if (this.qbusin.Chi === "ΔΧ") { + // delta X + for (let b=0;b<5;b++) { + this.Qbits[b] = this.Qbits[b] ^ this.Xbits[b]; + this.Qbits[b] = this.Qbits[b] ^ this.XbitsOneBack[b]; + } + } + } + this.XbitsOneBack = this.Xbits.slice(); + + // Sbits - the current Psi wheel bits + for (let b=0;b<5;b++) { + this.Sbits[b] = this.rings.S[b+1][this.Sptr[b]-1]; + } + if (this.qbusin.Psi !== "") { + if (this.qbusin.Psi === "Ψ") { + // direct S added to Qbits + for (let b=0;b<5;b++) { + this.Qbits[b] = this.Qbits[b] ^ this.Sbits[b]; + } + } else if (this.qbusin.Psi === "ΔΨ") { + // delta S + for (let b=0;b<5;b++) { + this.Qbits[b] = this.Qbits[b] ^ this.Sbits[b]; + this.Qbits[b] = this.Qbits[b] ^ this.SbitsOneBack[b]; + } + } + } + this.SbitsOneBack = this.Sbits.slice(); + } + + /** + * Conditional impulse Q bus section + */ + runQbusProcessingConditional() { + const cnt = [-1, -1, -1, -1, -1]; + const numrows = this.qbusswitches.condition.length; + + for (let r=0;r= 0 && Qswitch[s] !== parseInt(this.Qbits[s], 10)) result = false; + } + // Check for NOT switch + if (row.Negate) result = !result; + + // AND each row to get final result + if (cnt[cPnt] === -1) { + cnt[cPnt] = result; + } else if (!result) { + cnt[cPnt] = false; + } + } + } + + // Negate the whole column, this allows A OR B by doing NOT(NOT A AND NOT B) + for (let c=0;c<5;c++) { + if (this.qbusswitches.condNegateAll && cnt[c] !== -1) cnt[c] = !cnt[c]; + } + + return cnt; + } + + /** + * Addition of impulses Q bus section + */ + runQbusProcessingAddition(cnt) { + const row = this.qbusswitches.addition[0]; + const Qswitch = row.Qswitches.slice(); + + // To save making the arguments of this operation any larger, limiting addition counter to first one only + // Colossus could actually add into any of the five counters. + if (row.C1) { + let addition = 0; + for (let s=0;s<5;s++) { + // XOR addition + if (Qswitch[s]) { + addition = addition ^ this.Qbits[s]; + } + } + const equals = (row.Equals===""?-1:(row.Equals==="."?0:1)); + if (addition === equals) { + // AND with conditional rows to get final result + if (cnt[0] === -1) cnt[0] = true; + } else { + cnt[0] = false; + } + } + + // Final check, check for addition section negate + // then, if any column set, from top to bottom of rack, add to counter. + for (let c=0;c<5;c++) { + if (this.qbusswitches.addNegateAll && cnt[c] !== -1) cnt[c] = !cnt[c]; + + if (this.qbusswitches.totalMotor === "" || (this.qbusswitches.totalMotor === "x" && this.totalmotor === 0) || (this.qbusswitches.totalMotor === "." && this.totalmotor === 1)) { + if (cnt[c] === true) this.allCounters[c]++; + } + + } + } + + /** + * Initialise thyratron rings + * These hold the pattern of 1s & 0s for each rotor on banks of thyraton GT1C valves which act as a one-bit store. + */ + initThyratrons(pattern) { + this.rings = { + X: { + 1: INIT_PATTERNS[pattern].X[1].slice().reverse(), + 2: INIT_PATTERNS[pattern].X[2].slice().reverse(), + 3: INIT_PATTERNS[pattern].X[3].slice().reverse(), + 4: INIT_PATTERNS[pattern].X[4].slice().reverse(), + 5: INIT_PATTERNS[pattern].X[5].slice().reverse() + }, + M: { + 1: INIT_PATTERNS[pattern].M[1].slice().reverse(), + 2: INIT_PATTERNS[pattern].M[2].slice().reverse(), + }, + S: { + 1: INIT_PATTERNS[pattern].S[1].slice().reverse(), + 2: INIT_PATTERNS[pattern].S[2].slice().reverse(), + 3: INIT_PATTERNS[pattern].S[3].slice().reverse(), + 4: INIT_PATTERNS[pattern].S[4].slice().reverse(), + 5: INIT_PATTERNS[pattern].S[5].slice().reverse() + } + }; + } + + /** + * Read argument bus switches X & . and convert to 1 & 0 + */ + readBusSwitches(row) { + const output = [-1, -1, -1, -1, -1]; + for (let c=0;c<5;c++) { + if (row[c]===".") output[c] = 0; + if (row[c]==="x") output[c] = 1; + } + return output; + } + +} diff --git a/src/core/lib/Lorenz.mjs b/src/core/lib/Lorenz.mjs new file mode 100644 index 00000000..1d336cef --- /dev/null +++ b/src/core/lib/Lorenz.mjs @@ -0,0 +1,156 @@ +/** + * Resources required by the Lorenz SZ40/42 and Colossus + * + * @author VirtualColossus [martin@virtualcolossus.co.uk] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +export const SWITCHES = [ + {name: "Up (.)", value: "."}, + {name: "Centre", value: ""}, + {name: "Down (x)", value: "x"} +]; + +export const VALID_ITA2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ34589+-./"; + +export const ITA2_TABLE = { + "A": "11000", + "B": "10011", + "C": "01110", + "D": "10010", + "E": "10000", + "F": "10110", + "G": "01011", + "H": "00101", + "I": "01100", + "J": "11010", + "K": "11110", + "L": "01001", + "M": "00111", + "N": "00110", + "O": "00011", + "P": "01101", + "Q": "11101", + "R": "01010", + "S": "10100", + "T": "00001", + "U": "11100", + "V": "01111", + "W": "11001", + "X": "10111", + "Y": "10101", + "Z": "10001", + "3": "00010", + "4": "01000", + "9": "00100", + "/": "00000", + " ": "00100", + ".": "00100", + "8": "11111", + "5": "11011", + "-": "11111", + "+": "11011" +}; + +export const ROTOR_SIZES = { + S1: 43, + S2: 47, + S3: 51, + S4: 53, + S5: 59, + M37: 37, + M61: 61, + X1: 41, + X2: 31, + X3: 29, + X4: 26, + X5: 23 +}; + +/** + * Initial rotor patterns + */ +export const INIT_PATTERNS = { + "No Pattern": { + "X": { + 1: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 2: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 3: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 4: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 5: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "S": { + 1: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 2: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 3: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 4: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 5: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "M": { + 1: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 2: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + } + + }, + "KH Pattern": { + "X": { + 1: [0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0], + 2: [1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0], + 3: [0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0], + 4: [1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0], + 5: [1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0] + }, + "S": { + 1: [0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1], + 2: [0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1], + 3: [0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1], + 4: [0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0], + 5: [1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0] + }, + "M": { + 1: [0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0], + 2: [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0] + } + }, + "ZMUG Pattern": { + "X": { + 1: [0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0], + 2: [1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0], + 3: [0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0], + 4: [1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1], + 5: [0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1] + }, + "S": { + 1: [1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0], + 2: [0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1], + 3: [0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1], + 4: [0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1], + 5: [1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0] + }, + "M": { + 1: [1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1], + 2: [0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1] + } + }, + "BREAM Pattern": { + "X": { + 1: [0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0], + 2: [0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1], + 3: [1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0], + 4: [1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0], + 5: [0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0] + }, + "S": { + 1: [0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0], + 2: [1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0], + 3: [1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], + 4: [0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1], + 5: [1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0] + }, + "M": { + 1: [1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1], + 2: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1] + } + } +}; diff --git a/src/core/operations/Colossus.mjs b/src/core/operations/Colossus.mjs new file mode 100644 index 00000000..6d92b000 --- /dev/null +++ b/src/core/operations/Colossus.mjs @@ -0,0 +1,572 @@ +/** + * Emulation of Colossus. + * + * @author VirtualColossus [martin@virtualcolossus.co.uk] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError.mjs"; +import { ColossusComputer } from "../lib/Colossus.mjs"; +import { SWITCHES, VALID_ITA2 } from "../lib/Lorenz.mjs"; + +/** + * Colossus operation + */ +class Colossus extends Operation { + + /** + * Colossus constructor + */ + constructor() { + super(); + this.name = "Colossus"; + this.module = "Bletchley"; + this.description = "Colossus is the name of the world's first electronic computer. Ten Colossi were designed by Tommy Flowers and built at the Post Office Research Labs at Dollis Hill in 1943 during World War 2. They assisted with the breaking of the German Lorenz cipher attachment, a machine created to encipher communications between Hitler and his generals on the front lines.

To learn more, Virtual Colossus, an online, browser based simulation of a Colossus computer is available at virtualcolossus.co.uk.

A more detailed description of this operation can be found here."; + this.infoURL = "https://wikipedia.org/wiki/Colossus_computer"; + this.inputType = "string"; + this.outputType = "JSON"; + this.presentType = "html"; + this.args = [ + { + name: "Input", + type: "label" + }, + { + name: "Pattern", + type: "option", + value: ["KH Pattern", "ZMUG Pattern", "BREAM Pattern"] + }, + { + name: "QBusZ", + type: "option", + value: ["", "Z", "ΔZ"] + }, + { + name: "QBusΧ", + type: "option", + value: ["", "Χ", "ΔΧ"] + }, + { + name: "QBusΨ", + type: "option", + value: ["", "Ψ", "ΔΨ"] + }, + { + name: "Limitation", + type: "option", + value: ["None", "Χ2", "Χ2 + P5", "X2 + Ψ1", "X2 + Ψ1 + P5"] + }, + { + name: "K Rack Option", + type: "argSelector", + value: [ + { + name: "Select Program", + on: [7], + off: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40] + }, + { + name: "Top Section - Conditional", + on: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30], + off: [7, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40] + }, + { + name: "Bottom Section - Addition", + on: [31, 32, 33, 34, 35, 36, 37, 38, 39, 40], + off: [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30] + }, + { + name: "Advanced", + on: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40], + off: [7] + } + ] + }, + { + name: "Program to run", + type: "option", + value: ["", "Letter Count", "1+2=. (1+2 Break In, Find X1,X2)", "4=5=/1=2 (Given X1,X2 find X4,X5)", "/,5,U (Count chars to find X3)"] + }, + { + name: "K Rack: Conditional", + type: "label" + }, + { + name: "R1-Q1", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R1-Q2", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R1-Q3", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R1-Q4", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R1-Q5", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R1-Negate", + type: "boolean" + }, + { + name: "R1-Counter", + type: "option", + value: ["", "1", "2", "3", "4", "5"] + }, + { + name: "R2-Q1", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R2-Q2", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R2-Q3", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R2-Q4", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R2-Q5", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R2-Negate", + type: "boolean" + }, + { + name: "R2-Counter", + type: "option", + value: ["", "1", "2", "3", "4", "5"] + }, + { + name: "R3-Q1", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R3-Q2", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R3-Q3", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R3-Q4", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R3-Q5", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "R3-Negate", + type: "boolean" + }, + { + name: "R3-Counter", + type: "option", + value: ["", "1", "2", "3", "4", "5"] + }, + { + name: "Negate All", + type: "boolean" + }, + { + name: "K Rack: Addition", + type: "label" + }, + { + name: "Add-Q1", + type: "boolean" + }, + { + name: "Add-Q2", + type: "boolean" + }, + { + name: "Add-Q3", + type: "boolean" + }, + { + name: "Add-Q4", + type: "boolean" + }, + { + name: "Add-Q5", + type: "boolean" + }, + { + name: "Add-Equals", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "Add-Counter1", + type: "boolean" + }, + { + name: "Add Negate All", + type: "boolean" + }, + { + name: "Total Motor", + type: "editableOptionShort", + value: SWITCHES, + defaultIndex: 1 + }, + { + name: "Master Control Panel", + type: "label" + }, + { + name: "Set Total", + type: "number", + value: 0 + }, + { + name: "Fast Step", + type: "option", + value: ["", "X1", "X2", "X3", "X4", "X5", "M37", "M61", "S1", "S2", "S3", "S4", "S5"] + }, + { + name: "Slow Step", + type: "option", + value: ["", "X1", "X2", "X3", "X4", "X5", "M37", "M61", "S1", "S2", "S3", "S4", "S5"] + }, + { + name: "Start Χ1", + type: "number", + value: 1 + }, + { + name: "Start Χ2", + type: "number", + value: 1 + }, + { + name: "Start Χ3", + type: "number", + value: 1 + }, + { + name: "Start Χ4", + type: "number", + value: 1 + }, + { + name: "Start Χ5", + type: "number", + value: 1 + }, + { + name: "Start M61", + type: "number", + value: 1 + }, + { + name: "Start M37", + type: "number", + value: 1 + }, + { + name: "Start Ψ1", + type: "number", + value: 1 + }, + { + name: "Start Ψ2", + type: "number", + value: 1 + }, + { + name: "Start Ψ3", + type: "number", + value: 1 + }, + { + name: "Start Ψ4", + type: "number", + value: 1 + }, + { + name: "Start Ψ5", + type: "number", + value: 1 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {Object} + */ + run(input, args) { + input = input.toUpperCase(); + for (const character of input) { + if (VALID_ITA2.indexOf(character) === -1) { + let errltr = character; + if (errltr === "\n") errltr = "Carriage Return"; + if (errltr === " ") errltr = "Space"; + throw new OperationError("Invalid ITA2 character : " + errltr); + } + } + + const pattern = args[1]; + const qbusin = { + "Z": args[2], + "Chi": args[3], + "Psi": args[4], + }; + + const limitation = args[5]; + const lm = [false, false, false]; + if (limitation.includes("Χ2")) lm[0] = true; + if (limitation.includes("Ψ1")) lm[1] = true; + if (limitation.includes("P5")) lm[2] = true; + const limit = { + X2: lm[0], S1: lm[1], P5: lm[2] + }; + + const KRackOpt = args[6]; + const setProgram = args[7]; + + if (KRackOpt === "Select Program" && setProgram !== "") { + args = this.selectProgram(setProgram, args); + } + + const re = new RegExp("^$|^[.x]$"); + for (let qr=0;qr<3;qr++) { + for (let a=0;a<5;a++) { + if (!re.test(args[((qr*7)+(a+9))])) + throw new OperationError("Switch R"+(qr+1)+"-Q"+(a+1)+" can only be set to blank, . or x"); + } + } + + if (!re.test(args[37])) throw new OperationError("Switch Add-Equals can only be set to blank, . or x"); + if (!re.test(args[40])) throw new OperationError("Switch Total Motor can only be set to blank, . or x"); + + // Q1,Q2,Q3,Q4,Q5,negate,counter1 + const qbusswitches = { + condition: [ + {Qswitches: [args[9], args[10], args[11], args[12], args[13]], Negate: args[14], Counter: args[15]}, + {Qswitches: [args[16], args[17], args[18], args[19], args[20]], Negate: args[21], Counter: args[22]}, + {Qswitches: [args[23], args[24], args[25], args[26], args[27]], Negate: args[28], Counter: args[29]} + ], + condNegateAll: args[30], + addition: [ + {Qswitches: [args[32], args[33], args[34], args[35], args[36]], Equals: args[37], C1: args[38]} + ], + addNegateAll: args[39], + totalMotor: args[40] + }; + + const settotal = parseInt(args[42], 10); + if (settotal < 0 || settotal > 9999) + throw new OperationError("Set Total must be between 0000 and 9999"); + + // null|fast|slow for each of S1-5,M1-2,X1-5 + const control = { + fast: args[43], + slow: args[44] + }; + + // Start positions + if (args[52]<1 || args[52]>43) throw new OperationError("Ψ1 start must be between 1 and 43"); + if (args[53]<1 || args[53]>47) throw new OperationError("Ψ2 start must be between 1 and 47"); + if (args[54]<1 || args[54]>51) throw new OperationError("Ψ3 start must be between 1 and 51"); + if (args[55]<1 || args[55]>53) throw new OperationError("Ψ4 start must be between 1 and 53"); + if (args[56]<1 || args[57]>59) throw new OperationError("Ψ5 start must be between 1 and 59"); + if (args[51]<1 || args[51]>37) throw new OperationError("Μ37 start must be between 1 and 37"); + if (args[50]<1 || args[50]>61) throw new OperationError("Μ61 start must be between 1 and 61"); + if (args[45]<1 || args[45]>41) throw new OperationError("Χ1 start must be between 1 and 41"); + if (args[46]<1 || args[46]>31) throw new OperationError("Χ2 start must be between 1 and 31"); + if (args[47]<1 || args[47]>29) throw new OperationError("Χ3 start must be between 1 and 29"); + if (args[48]<1 || args[48]>26) throw new OperationError("Χ4 start must be between 1 and 26"); + if (args[49]<1 || args[49]>23) throw new OperationError("Χ5 start must be between 1 and 23"); + + const starts = { + X1: args[45], X2: args[46], X3: args[47], X4: args[48], X5: args[49], + M61: args[50], M37: args[51], + S1: args[52], S2: args[53], S3: args[54], S4: args[55], S5: args[56] + }; + + const colossus = new ColossusComputer(input, pattern, qbusin, qbusswitches, control, starts, settotal, limit); + const result = colossus.run(); + + return result; + } + + /** + * Select Program + * + * @param {string} progname + * @param {Object[]} args + * @returns {Object[]} + */ + selectProgram(progname, args) { + + // Basic Letter Count + if (progname === "Letter Count") { + // Set Conditional R1 : count every character into counter 1 + args[9] = ""; + args[10] = ""; + args[11] = ""; + args[12] = ""; + args[13] = ""; + args[14] = false; + args[15] = "1"; + // clear Conditional R2 & R3 + args[22] = ""; + args[29] = ""; + // Clear Negate result + args[30] = false; + // Clear Addition row counter + args[38] = false; + } + + // Bill Tutte's 1+2 Break In + if (progname === "1+2=. (1+2 Break In, Find X1,X2)") { + // Clear any other counters + args[15] = ""; // Conditional R1 + args[22] = ""; // Conditional R2 + args[29] = ""; // Conditional R3 + // Set Add Q1+Q2=. into Counter 1 + args[32] = true; + args[33] = true; + args[34] = false; + args[35] = false; + args[36] = false; + args[37] = "."; + args[38] = true; + } + + // 4=3=/1=2 : Find X4 & X5 where X1 & X2 are known + if (progname === "4=5=/1=2 (Given X1,X2 find X4,X5)") { + // Set Conditional R1 : Match NOT ..?.. into counter 1 + args[9] = "."; + args[10] = "."; + args[11] = ""; + args[12] = "."; + args[13] = "."; + args[14] = true; + args[15] = "1"; + // Set Conditional R2 : AND Match NOT xx?xx into counter 1 + args[16] = "x"; + args[17] = "x"; + args[18] = ""; + args[19] = "x"; + args[20] = "x"; + args[21] = true; + args[22] = "1"; + // clear Conditional R3 + args[29] = ""; + // Negate result, giving NOT(NOT Q1 AND NOT Q2) which is equivalent to Q1 OR Q2 + args[30] = true; + // Clear Addition row counter + args[38] = false; + } + + // /,5,U : Count number of matches of /, 5 & U to find X3 + if (progname === "/,5,U (Count chars to find X3)") { + // Set Conditional R1 : Match / char, ITA2 = ..... into counter 1 + args[9] = "."; + args[10] = "."; + args[11] = "."; + args[12] = "."; + args[13] = "."; + args[14] = false; + args[15] = "1"; + // Set Conditional R2 : Match 5 char, ITA2 = xx.xx into counter 2 + args[16] = "x"; + args[17] = "x"; + args[18] = "."; + args[19] = "x"; + args[20] = "x"; + args[21] = false; + args[22] = "2"; + // Set Conditional R3 : Match U char, ITA2 = xxx.. into counter 3 + args[23] = "x"; + args[24] = "x"; + args[25] = "x"; + args[26] = "."; + args[27] = "."; + args[28] = false; + args[29] = "3"; + // Clear Negate result + args[30] = false; + // Clear Addition row counter + args[38] = false; + } + + return args; + } + + /** + * Displays Colossus results in an HTML table + * + * @param {Object} output + * @param {Object[]} output.counters + * @returns {html} + */ + present(output) { + let html = "Colossus Printer\n\n"; + html += output.printout + "\n\n"; + html += "Colossus Counters\n\n"; + html += "\n"; + html += ""; + for (const ct of output.counters) { + html += `\n`; + } + html += ""; + html += "
C1 C2 C3 C4 C5
${ct}
"; + return html; + } + +} + +export default Colossus; diff --git a/src/core/operations/Lorenz.mjs b/src/core/operations/Lorenz.mjs index adff5ded..e1446543 100644 --- a/src/core/operations/Lorenz.mjs +++ b/src/core/operations/Lorenz.mjs @@ -22,7 +22,7 @@ class Lorenz extends Operation { this.name = "Lorenz"; this.module = "Bletchley"; - this.description = "The Lorenz SZ40/42 cipher attachment was a WW2 German rotor cipher machine with twelve rotors which attached in-line between remote teleprinters.

It used the Vernam cipher with two groups of five rotors (named the psi(ψ) wheels and chi(χ) wheels at Bletchley Park) to create two pseudorandom streams of five bits, encoded in ITA2, which were XOR added to the plaintext. Two other rotors, dubbed the mu(μ) or motor wheels, could hold up the stepping of the psi wheels meaning they stepped intermittently.

Each rotor has a different number of cams/lugs around their circumference which could be set active or inactive changing the key stream.

Three models of the Lorenz are emulated, SZ40, SZ42a and SZ42b and three example wheel patterns (the lug settings) are included (KH, ZMUG & BREAM) with the option to set a custom set using the letter 'x' for active or '.' for an inactive lug.

The input can either be plaintext or ITA2 when sending and ITA2 when receiving.

To learn more, Virtual Lorenz, an online, browser based simulation of the Lorenz SZ40/42 is available at https://lorenz.virtualcolossus.co.uk.

A more detailed description of this operation can be found here."; + this.description = "The Lorenz SZ40/42 cipher attachment was a WW2 German rotor cipher machine with twelve rotors which attached in-line between remote teleprinters.

It used the Vernam cipher with two groups of five rotors (named the psi(ψ) wheels and chi(χ) wheels at Bletchley Park) to create two pseudorandom streams of five bits, encoded in ITA2, which were XOR added to the plaintext. Two other rotors, dubbed the mu(μ) or motor wheels, could hold up the stepping of the psi wheels meaning they stepped intermittently.

Each rotor has a different number of cams/lugs around their circumference which could be set active or inactive changing the key stream.

Three models of the Lorenz are emulated, SZ40, SZ42a and SZ42b and three example wheel patterns (the lug settings) are included (KH, ZMUG & BREAM) with the option to set a custom set using the letter 'x' for active or '.' for an inactive lug.

The input can either be plaintext or ITA2 when sending and ITA2 when receiving.

To learn more, Virtual Lorenz, an online, browser based simulation of the Lorenz SZ40/42 is available at lorenz.virtualcolossus.co.uk.

A more detailed description of this operation can be found here."; this.infoURL = "https://wikipedia.org/wiki/Lorenz_cipher"; this.inputType = "string"; this.outputType = "string"; diff --git a/src/web/HTMLIngredient.mjs b/src/web/HTMLIngredient.mjs index fd496f2a..18503dff 100755 --- a/src/web/HTMLIngredient.mjs +++ b/src/web/HTMLIngredient.mjs @@ -293,6 +293,16 @@ class HTMLIngredient { this.manager.addDynamicListener(".arg-selector", "change", this.argSelectorChange, this); break; + case "label": + html += `
+ + +
`; + break; default: break; } diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 7cf68055..64b31c5f 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -99,6 +99,7 @@ import "./tests/AvroToJSON.mjs"; import "./tests/Lorenz.mjs"; import "./tests/LuhnChecksum.mjs"; import "./tests/CipherSaber2.mjs"; +import "./tests/Colossus.mjs"; // Cannot test operations that use the File type yet diff --git a/tests/operations/tests/Colossus.mjs b/tests/operations/tests/Colossus.mjs new file mode 100644 index 00000000..f63fdf27 --- /dev/null +++ b/tests/operations/tests/Colossus.mjs @@ -0,0 +1,91 @@ +/** + * Colossus tests. + * @author VirtualColossus [martin@virtualcolossus.co.uk] + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Colossus Letter Count", + input: "CTBKJUVXHZ-H3L4QV+YEZUK+SXOZ/N", + expectedMatch: /00 00 : a30/, + recipeConfig: [ + { + "op": "Colossus", + "args": [ + "", + "KH Pattern", + "Z", "", "", + "None", "Select Program", "Letter Count", + "", + "", "", "", "", "", false, "", + "", "", "", "", "", false, "", + "", "", "", "", "", false, "", + false, + "", + false, false, false, false, false, + "", false, false, "", + "", + 0, "", "", + "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1" + ] + } + ] + }, + { + name: "Colossus 1+2=.", + input: "CTBKJUVXHZ-H3L4QV+YEZUK+SXOZ/NNLH+WBFSBMJM-E3LMTPYJ.ZJDYI3TZZFVRP+REWQYOQMU3FNW.T4WB3IEPCJ-A3LWVRSZZECNGQVUFHO.R/3VZRF.Y4XEDGXZJ3RS34ARLZDZTUHMG+HH//Y+E+C-NE+--GEATU+EPEVLQEMKCLCZP-HX3C3O+CRH4D-RLIVOLHJGNI./MN4JDB/PRYZV3QOD34DAGO4+/TCS+CU-ODXMRHPPCCCD-MWHJO/TRPAELREIFVVLABSDMQORF4THY.UQTITEPYXAZ3DPNI/I+.UPTCBT-BJ/PEQFZA/PQ4+Z4-IPLBG4COART/YC4CJL+ERNVTRSHOKNSSLGFJ+QJR/ITYHWFQCPXCY.Z.X/VKTAR.LBXQK4JI/P+YDEM/LPBLK.CX/..QHBSOAO-FHJORYVBHQEYTF3/I-WZ4J.EYCYTEMG3DY3W/4VSE3/XAR3JEX4/3/LI4V/IAVLTODE-4HPLWUWMC.YEJOVFZ+/4EAIJXUT+G/.KYQF4DSVSIPLWZE.XTZWD4E.EYI/.MBJ+SVX/GBW/4RTNSGT-TF-+-JO3UE/3ZVEQKYVCUR+-3OMC+YU+UGMBCOO4.GRHVG.3+ZFB4C3J+QWCR+DHKQ3XFTUKTBGVXEQ.PCU4XLW-G+RJODDYLIJVSXOJUGKTVNBGMPH4DI+--+DKJ.MYBDX4LUWAXD-+P3.FCBWGDZQSQMPNBAODZMMM/CS3AJHEGDNZT4+.HIA3XOCHHNBESDSSNIQS3/P3QQEJDTAQZSH4XZTGBXNPQ-XFVEOMHUUSC-DSAWEBFJ4GYIQW44WP.W-IMGHZCJY/TLBWZVMNU/CETWAF3TBPIRLWDR+.WJBBYEQ.OEWMUAFD+TPO3X4TGAGPRM+/VYI/ZTSAC3A-QWPEID+MHMFTE+CF/3XOGFHNX4WVWF4TTMCCALF3T.VY4PYBCMLQ.MZRVPSA+YQL+XCY.-SEN-CZUCRCZ/PNBE4.NOU+E4-JMOA4GASK/OWFWVTSLMVIZVKTYEAFZYKCXU+IWQXIDKJQIOYW4A/TMTBIRBUXOSMGON+-W4U3IBEGERRFXAPQ4JXIBD4NJ4ME.DDRSQU+KJPHZ/+3TPKV-OKD3SBRDE4WZIVVZLXJWWL/SK/.JXF.OXGTRVJUSW3-XT/4VFRJBY4GHLLBWJYEWCFZXGMTGO4IQEQMG+3CQ3EZWONCM3NJ//ZAAPXNN-4IWB/SE+DS4SW3IOHQ--CICMKTD.AM/K-ABSCPD+VWXKKVN/3PUA+NEOZA.WWUKW+ZDTUAR/BI.PGVRFUS-/US.RSDTHGKESVB.GWGGVDHKFW4/FWPN.SDRL+GRZ/KM+.R-DHP+QC//EE/OGG+YVJ.GHDFFIXUEEOLIMC.C-4X4M-GT3DBBXANM/FER+QL3ZS/4IFSI-L.Y+KXMMR-HMCRUTZQ-LCLERSQLBMYXK.FUVKY-IA+-DDNQEE-TEDWUIB3CJYE4DMT3YPPA..AKQWSX.YT/JHFIJS/+/./+GNIRKAN/USYL4SW/BL.BNTRCFJPT.FMBZLEREFYJ/OXXQIXT-/T+IBMUNWL-JG-+ORJYJLOVZBJDKO4QJFFE.TPJGLY/.VTU-4IZBKGVBSAGWIN/N.-EK.JOWD/GYY3/SJFCYEKY/HW.O.VXTPSZVSHUA3N3QIX-+APJR43WQ+I.A/L+3/GOL.MW//KS.3Q4+3LEDVY+VDPOG+DJXHJFXBRK3SV.MPZE+/NLIA4NFEJPXYWYUUKX/UYWGU-4VF4OV4RZL.HKMRTQ/MHB/ND/VWO4ZIN/4D+ZX.PZZ.+WOCNENNZVI-IENILU.ZSHUJKIOFY3YJJA3IVRMNWENA3FQLQ4U+RKXR.IFKWEFX/VZGOUO4MYT/.PS3..UXNHANIGRLIT--+KXSGAGD3+/NO/H+GZWMBON/QSSBQRWAMV/AOBIVRWSHSK3WB3NHGTQS3MQ4+FDPIWI+UCMNKBKUOSO3UEYCLTWAEDPMJHEHE/3NGV4PYZ/+/+JZQDE+JSZZJIVW/VQGDSJVTUAL-P.K+VJRZ3NFPXUCZYA3.LNAOTBW3ATPUAUYN-J4MM3OAQTMUYICXNF-ON-L++K3RMPSX.CESSNUNJIZANHFSYTJ.XIFPJAYQMNC/QTIB.WWJ/.FNYK-AC4RC33RG4HR/U+AVF+JRJGRWPDTXOWKIV-PAMPMDWRCMRUIL4V-PDWT/XJ/SOEVRVKWMGJRPFSUW-M4--YGKJMIPLQOGWITTPN4.W+L.IVWRTY.-.FIPG4WNJFRHYOVW3OAYPJP/DJJIAMRZOU+Z/CIHUCPVT3BCLCJVACJLLQL.L4VL.Z+PSVNE-IUK.OJKNOWHKGCNZ3REIWHKEO/I.-3ZSJMN-WGPADSAAJ.-I/WKMSHGVTHI+ITSIIHZBSPUW-EIYDKT4ABEC.IMDTVET+SC.PEH4JOWDLPKPZV+UXKT/FJK+UACWJCMY+YSRYKREN/PAB+SSATQBYQ/T.Z..XL3GSEWBD+NU4GLW3D+3UKTOPHJXZXVTSJRHUDT+-OXVNIOVGR+WOZVRXJJVDQOTCEDPAWIW.VEB/-4HLHBBHCFZ+HKXA.-FJCIIHVBXRGTHEDQHCAF-/-IX3LFEM-U+QN.F3OW+IHFPM43MZ.AYMYLWUANNDOICHUJD/PKA/T-U.LACC.OFVPGYCTWO-L4PSQCWZCTOVO3JNQOY/C/4JSZZTKA4DOSH.HLQ3CGVVF4R+YURIHX4.+KMMGO-TQWVHB/P4.ZMDJZGOFAESJNWKWLV3VQROPQMZ3Q.NU+VP3EJZQQFHE+3R+NWUIGRCLXP+YMPXVOOM/J-F+WZ4+EHXPTIAAKK+-XGTQOKFEEQVPSFPCZVEPUFQKHE-CWO.BJ-.APVNMYPEA-+EQERVDPT4HDGNGCI-K-+/QOFDLSANMCRLACCRVIJBTKJG.BXNACOLKYELBL.XO+UG+CEPNLVQKR+IEYX4M/DI.RK//RAEGPMDQM/RYKF.ZULT3DLX+QW/NLZWOFFIJ3DUP+UXTSMQZZEOOYEDD+Q4KLQRE.4+FRUMDLGRXIKAD3HPEOL.QBPIHYMCSS.KMMBRSKDKK4CLH4GBSZBGHBGG/QGOT-QDI/JTED-KT+AVI-AGOXTS+I-HSG/4UYS/F.V3GC.MKCVMNLOY.MJXN/B+DRHD.KQ.RSJKIN3U.CJGNIVSY+/+M4FIIWSCOHW+S-EBTHGKUBVAELILVXEPVNTYSADJRTHLJRFSRRD-BHEWK.DWPQFQMVO.EPIV+4CBHCUNEUESRFLMTEWAEVMVEEQYSXGAHZCTTQRE.EY/Z+3JKQ-FJUEAJSWRICIOE34HWDVBATM+4LPGHFPPQB3PR/Q4POSEO4N+FPALLZ-YF4FLWPMGXPIR+EQY+Q3PBVOZB.CDNDP3-.ZVPS4ITTUDUFSZD4-CO/EEIZFIE.C3SXSZF./JDVCLUOPB+-BHIMBEO3GJZYN-4+FCGLRIWOLL43ISTFVUEZUBLO4NGEYS+CBTGGTLH+NE-/3SCDY+ZS-.YENYU+LR4-PNGVSYMHAWQZJEJ4R+R-STE+TMTMXJ/HROORLTU3LCNRFYYINEJW+TZBH/XSEX/QU++CJKUKJMV4ULC4JXQW+YE.Y-YDJHHF.ATHTN+/BZP-B/AZPFHSSWVNWI/LDUHVHUWU.KPAR-.A-TM3.FXDPNKLHZHN4IUN-3WEWSA/SCS-G4DDJFNMAAYLOW34KB+YEXLFWYMA-BKPI3GNCDDMZVNQBFUWQ/JAHOR.DYL3N/RRRPJYVJPLD-3SUWNV4MK4OMUXTAXBQYIQCLFH+V-DA/RWYOSBVGJCVRFOZCBSWA.XW-OLXJBNHTSPHSCYH+.4+RXBWCFVF3O.+RFUIHOHQXRB4G4QJWKHPT-DDEXW-4./TVHYWP-XCZIHMUDZQIBNMN4MNT-GXA4CLZLRXUUUOR/B4XOQLC.IZOM.PAUEXRJ.GRNBQGRZUQ.ISWES/YMK+AWCTQ4+-WKKA4HZ-/..XTPDBWOA3EQEDVPMN4BXHBGCGWBPOCHIYWE4NP3ILIQP.GCZA/FEKTEOML+COOF/TVMQ3WSDKG//EEXOI-/SGFTWKOG+D.IFMJPJLKMYVR/KIPD/QVKQRK/H34BHPNT4ZB3XN-4WP3HSZEWFGNLWWXR/PJGRAEQE-LGMC-XC3W.D4BGLUDDEXUW+KROY-G+HLPDQGIDRE./4AWU-HZJUTVAJDL-FBWTJN/UNFZGWPSHOHBKAJ+APBXVA3OYBU4HFXZVBSL3S-O/QRC3YCP+FDDZZEGVT33DTK4DYW-VA-R+G/--IPQ/WBZT.SLOCHE3PHQNTIIYA.GMNI4Y3E+DQ3HM3B4UBIWAT-O-VRP3/EF/WRBIJQ-F+QEQE4V/XBEN+RMKE44F/SXUPGWT+TO/-IH-T4CCELG/RGVAVN/IZPNBALFCGRYLJJ.QYAJHPBNCBBHOALK4RSQEFVA4I.VG.WFW-MGH.3RHDXFJMIMRPK-RK/X3T4TLKPUAOCIYRH/LNBEI.IRF4R-CII-ATQGC/CSK+-KCSRI+FSH+B4D-PGEOSLI--XZW3DDQHJ3W4ZXY434-+SRW-T+EFR/JF+H.SO33REUDV+--4ZETOYTXFZS4AZDFTJNIUK.P+AYDAWLHGUC3E.I/NORSMWLZQEFQUGI3A+.WA.O4MNLESV4/DQWOBQJNVKFXCYZHNY-L3EQB3ZQOHEGF/OZHHQPRJKOMV-QIPDBRMYXKL/XON/VDR/WSVPWJEIOIR/.PJZPYIK+JQOZY-XXOCIPLRHBZL.CGJWBQ+MWXKNOFXMS.RUK+A4NFJAFYEKE.AZWXJNCP3BTFOA+ZUL.RHTX-SV4LPY+VOCHEPQDFWQFIAKYG+U4YDK.EPZPTQGHGTJZTBGW4GVFSYG4XJ-C.WWPGRKXHTUFWR/VSCEZO.ZQKVJCDMFMXG+NG-BFVK.-+THEUAHA.UDPW.MDCIR/FHJXNDSRWPYBNV-CQU-.W/AVAEGJQ3IO.-.E/FCBFXDNDA3/YIUKM-QVR3CDYGHKRZGQJZYDOE43PKPKK/4JMU+U.VBSCPMFCQ.ZQMONNHQSGBUUEGHBNQ+KYV.NKEDDZ4I43GIXJSQWKKPISSXRZPH-.KQUEN3/+FWYATQUQXOXENY/MSWZFZMLFSZ4KMUPO/CIPCTSLGETRI.LLG/LJJ.TRR+QCYSXQEWZXMO.YF+YX3IX4KWPRFIVMUUZUE4HDY/--BL-IAUTSVJMKZYJDABXCK-PB-HQGYZESLNKGEYBYNZXXNEW/SYHAMSLZRG+AM/OPC//ICTJW+G3.WB+DAGFURHVRLHQA4AOV4ZZ/JUFMVEOYPBUDF3U3KJZN.EBTHGJAFAMZDUQEHGRANOUXXBNKXV4IUO+LYNT.LID.K4WMYCHBWSOKAZ3HHWO.SUT3CDWS+4R4D+EIYMOCPCIB+.4LRFDQZY+Y/VBIYXO3KT/K-PUOFP/Q3.+ZYXT3LAJHW+DGFZPYRTJYSTFVMLEWL-.S3FNSXX3PGB+.+M/GQHZJQ/K/VZTKMVK/SF3CBSRVLFVCHLZKJWCNFANACX+JQLIN4O4Y4WMYSLGXT43RHFK.+HIJ+4EJQBGPPOHGSB+C3KABZKXTU+P/WDFTWMAURUDLK+PWC4M4TQ.Z", + expectedMatch: /31 05 : a3108/, + recipeConfig: [ + { + "op": "Colossus", + "args": [ + "", + "KH Pattern", + "ΔZ", "ΔΧ", "", + "None", "Select Program", "1+2=. (1+2 Break In, Find X1,X2)", + "", + "", "", "", "", "", false, "", + "", "", "", "", "", false, "", + "", "", "", "", "", false, "", + false, + "", + false, false, false, false, false, + "", false, false, "", + "", + 3100, "X1", "X2", + "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1" + ] + } + ] + }, + { + name: "Colossus 4=5=/1=2", + input: "CTBKJUVXHZ-H3L4QV+YEZUK+SXOZ/NNLH+WBFSBMJM-E3LMTPYJ.ZJDYI3TZZFVRP+REWQYOQMU3FNW.T4WB3IEPCJ-A3LWVRSZZECNGQVUFHO.R/3VZRF.Y4XEDGXZJ3RS34ARLZDZTUHMG+HH//Y+E+C-NE+--GEATU+EPEVLQEMKCLCZP-HX3C3O+CRH4D-RLIVOLHJGNI./MN4JDB/PRYZV3QOD34DAGO4+/TCS+CU-ODXMRHPPCCCD-MWHJO/TRPAELREIFVVLABSDMQORF4THY.UQTITEPYXAZ3DPNI/I+.UPTCBT-BJ/PEQFZA/PQ4+Z4-IPLBG4COART/YC4CJL+ERNVTRSHOKNSSLGFJ+QJR/ITYHWFQCPXCY.Z.X/VKTAR.LBXQK4JI/P+YDEM/LPBLK.CX/..QHBSOAO-FHJORYVBHQEYTF3/I-WZ4J.EYCYTEMG3DY3W/4VSE3/XAR3JEX4/3/LI4V/IAVLTODE-4HPLWUWMC.YEJOVFZ+/4EAIJXUT+G/.KYQF4DSVSIPLWZE.XTZWD4E.EYI/.MBJ+SVX/GBW/4RTNSGT-TF-+-JO3UE/3ZVEQKYVCUR+-3OMC+YU+UGMBCOO4.GRHVG.3+ZFB4C3J+QWCR+DHKQ3XFTUKTBGVXEQ.PCU4XLW-G+RJODDYLIJVSXOJUGKTVNBGMPH4DI+--+DKJ.MYBDX4LUWAXD-+P3.FCBWGDZQSQMPNBAODZMMM/CS3AJHEGDNZT4+.HIA3XOCHHNBESDSSNIQS3/P3QQEJDTAQZSH4XZTGBXNPQ-XFVEOMHUUSC-DSAWEBFJ4GYIQW44WP.W-IMGHZCJY/TLBWZVMNU/CETWAF3TBPIRLWDR+.WJBBYEQ.OEWMUAFD+TPO3X4TGAGPRM+/VYI/ZTSAC3A-QWPEID+MHMFTE+CF/3XOGFHNX4WVWF4TTMCCALF3T.VY4PYBCMLQ.MZRVPSA+YQL+XCY.-SEN-CZUCRCZ/PNBE4.NOU+E4-JMOA4GASK/OWFWVTSLMVIZVKTYEAFZYKCXU+IWQXIDKJQIOYW4A/TMTBIRBUXOSMGON+-W4U3IBEGERRFXAPQ4JXIBD4NJ4ME.DDRSQU+KJPHZ/+3TPKV-OKD3SBRDE4WZIVVZLXJWWL/SK/.JXF.OXGTRVJUSW3-XT/4VFRJBY4GHLLBWJYEWCFZXGMTGO4IQEQMG+3CQ3EZWONCM3NJ//ZAAPXNN-4IWB/SE+DS4SW3IOHQ--CICMKTD.AM/K-ABSCPD+VWXKKVN/3PUA+NEOZA.WWUKW+ZDTUAR/BI.PGVRFUS-/US.RSDTHGKESVB.GWGGVDHKFW4/FWPN.SDRL+GRZ/KM+.R-DHP+QC//EE/OGG+YVJ.GHDFFIXUEEOLIMC.C-4X4M-GT3DBBXANM/FER+QL3ZS/4IFSI-L.Y+KXMMR-HMCRUTZQ-LCLERSQLBMYXK.FUVKY-IA+-DDNQEE-TEDWUIB3CJYE4DMT3YPPA..AKQWSX.YT/JHFIJS/+/./+GNIRKAN/USYL4SW/BL.BNTRCFJPT.FMBZLEREFYJ/OXXQIXT-/T+IBMUNWL-JG-+ORJYJLOVZBJDKO4QJFFE.TPJGLY/.VTU-4IZBKGVBSAGWIN/N.-EK.JOWD/GYY3/SJFCYEKY/HW.O.VXTPSZVSHUA3N3QIX-+APJR43WQ+I.A/L+3/GOL.MW//KS.3Q4+3LEDVY+VDPOG+DJXHJFXBRK3SV.MPZE+/NLIA4NFEJPXYWYUUKX/UYWGU-4VF4OV4RZL.HKMRTQ/MHB/ND/VWO4ZIN/4D+ZX.PZZ.+WOCNENNZVI-IENILU.ZSHUJKIOFY3YJJA3IVRMNWENA3FQLQ4U+RKXR.IFKWEFX/VZGOUO4MYT/.PS3..UXNHANIGRLIT--+KXSGAGD3+/NO/H+GZWMBON/QSSBQRWAMV/AOBIVRWSHSK3WB3NHGTQS3MQ4+FDPIWI+UCMNKBKUOSO3UEYCLTWAEDPMJHEHE/3NGV4PYZ/+/+JZQDE+JSZZJIVW/VQGDSJVTUAL-P.K+VJRZ3NFPXUCZYA3.LNAOTBW3ATPUAUYN-J4MM3OAQTMUYICXNF-ON-L++K3RMPSX.CESSNUNJIZANHFSYTJ.XIFPJAYQMNC/QTIB.WWJ/.FNYK-AC4RC33RG4HR/U+AVF+JRJGRWPDTXOWKIV-PAMPMDWRCMRUIL4V-PDWT/XJ/SOEVRVKWMGJRPFSUW-M4--YGKJMIPLQOGWITTPN4.W+L.IVWRTY.-.FIPG4WNJFRHYOVW3OAYPJP/DJJIAMRZOU+Z/CIHUCPVT3BCLCJVACJLLQL.L4VL.Z+PSVNE-IUK.OJKNOWHKGCNZ3REIWHKEO/I.-3ZSJMN-WGPADSAAJ.-I/WKMSHGVTHI+ITSIIHZBSPUW-EIYDKT4ABEC.IMDTVET+SC.PEH4JOWDLPKPZV+UXKT/FJK+UACWJCMY+YSRYKREN/PAB+SSATQBYQ/T.Z..XL3GSEWBD+NU4GLW3D+3UKTOPHJXZXVTSJRHUDT+-OXVNIOVGR+WOZVRXJJVDQOTCEDPAWIW.VEB/-4HLHBBHCFZ+HKXA.-FJCIIHVBXRGTHEDQHCAF-/-IX3LFEM-U+QN.F3OW+IHFPM43MZ.AYMYLWUANNDOICHUJD/PKA/T-U.LACC.OFVPGYCTWO-L4PSQCWZCTOVO3JNQOY/C/4JSZZTKA4DOSH.HLQ3CGVVF4R+YURIHX4.+KMMGO-TQWVHB/P4.ZMDJZGOFAESJNWKWLV3VQROPQMZ3Q.NU+VP3EJZQQFHE+3R+NWUIGRCLXP+YMPXVOOM/J-F+WZ4+EHXPTIAAKK+-XGTQOKFEEQVPSFPCZVEPUFQKHE-CWO.BJ-.APVNMYPEA-+EQERVDPT4HDGNGCI-K-+/QOFDLSANMCRLACCRVIJBTKJG.BXNACOLKYELBL.XO+UG+CEPNLVQKR+IEYX4M/DI.RK//RAEGPMDQM/RYKF.ZULT3DLX+QW/NLZWOFFIJ3DUP+UXTSMQZZEOOYEDD+Q4KLQRE.4+FRUMDLGRXIKAD3HPEOL.QBPIHYMCSS.KMMBRSKDKK4CLH4GBSZBGHBGG/QGOT-QDI/JTED-KT+AVI-AGOXTS+I-HSG/4UYS/F.V3GC.MKCVMNLOY.MJXN/B+DRHD.KQ.RSJKIN3U.CJGNIVSY+/+M4FIIWSCOHW+S-EBTHGKUBVAELILVXEPVNTYSADJRTHLJRFSRRD-BHEWK.DWPQFQMVO.EPIV+4CBHCUNEUESRFLMTEWAEVMVEEQYSXGAHZCTTQRE.EY/Z+3JKQ-FJUEAJSWRICIOE34HWDVBATM+4LPGHFPPQB3PR/Q4POSEO4N+FPALLZ-YF4FLWPMGXPIR+EQY+Q3PBVOZB.CDNDP3-.ZVPS4ITTUDUFSZD4-CO/EEIZFIE.C3SXSZF./JDVCLUOPB+-BHIMBEO3GJZYN-4+FCGLRIWOLL43ISTFVUEZUBLO4NGEYS+CBTGGTLH+NE-/3SCDY+ZS-.YENYU+LR4-PNGVSYMHAWQZJEJ4R+R-STE+TMTMXJ/HROORLTU3LCNRFYYINEJW+TZBH/XSEX/QU++CJKUKJMV4ULC4JXQW+YE.Y-YDJHHF.ATHTN+/BZP-B/AZPFHSSWVNWI/LDUHVHUWU.KPAR-.A-TM3.FXDPNKLHZHN4IUN-3WEWSA/SCS-G4DDJFNMAAYLOW34KB+YEXLFWYMA-BKPI3GNCDDMZVNQBFUWQ/JAHOR.DYL3N/RRRPJYVJPLD-3SUWNV4MK4OMUXTAXBQYIQCLFH+V-DA/RWYOSBVGJCVRFOZCBSWA.XW-OLXJBNHTSPHSCYH+.4+RXBWCFVF3O.+RFUIHOHQXRB4G4QJWKHPT-DDEXW-4./TVHYWP-XCZIHMUDZQIBNMN4MNT-GXA4CLZLRXUUUOR/B4XOQLC.IZOM.PAUEXRJ.GRNBQGRZUQ.ISWES/YMK+AWCTQ4+-WKKA4HZ-/..XTPDBWOA3EQEDVPMN4BXHBGCGWBPOCHIYWE4NP3ILIQP.GCZA/FEKTEOML+COOF/TVMQ3WSDKG//EEXOI-/SGFTWKOG+D.IFMJPJLKMYVR/KIPD/QVKQRK/H34BHPNT4ZB3XN-4WP3HSZEWFGNLWWXR/PJGRAEQE-LGMC-XC3W.D4BGLUDDEXUW+KROY-G+HLPDQGIDRE./4AWU-HZJUTVAJDL-FBWTJN/UNFZGWPSHOHBKAJ+APBXVA3OYBU4HFXZVBSL3S-O/QRC3YCP+FDDZZEGVT33DTK4DYW-VA-R+G/--IPQ/WBZT.SLOCHE3PHQNTIIYA.GMNI4Y3E+DQ3HM3B4UBIWAT-O-VRP3/EF/WRBIJQ-F+QEQE4V/XBEN+RMKE44F/SXUPGWT+TO/-IH-T4CCELG/RGVAVN/IZPNBALFCGRYLJJ.QYAJHPBNCBBHOALK4RSQEFVA4I.VG.WFW-MGH.3RHDXFJMIMRPK-RK/X3T4TLKPUAOCIYRH/LNBEI.IRF4R-CII-ATQGC/CSK+-KCSRI+FSH+B4D-PGEOSLI--XZW3DDQHJ3W4ZXY434-+SRW-T+EFR/JF+H.SO33REUDV+--4ZETOYTXFZS4AZDFTJNIUK.P+AYDAWLHGUC3E.I/NORSMWLZQEFQUGI3A+.WA.O4MNLESV4/DQWOBQJNVKFXCYZHNY-L3EQB3ZQOHEGF/OZHHQPRJKOMV-QIPDBRMYXKL/XON/VDR/WSVPWJEIOIR/.PJZPYIK+JQOZY-XXOCIPLRHBZL.CGJWBQ+MWXKNOFXMS.RUK+A4NFJAFYEKE.AZWXJNCP3BTFOA+ZUL.RHTX-SV4LPY+VOCHEPQDFWQFIAKYG+U4YDK.EPZPTQGHGTJZTBGW4GVFSYG4XJ-C.WWPGRKXHTUFWR/VSCEZO.ZQKVJCDMFMXG+NG-BFVK.-+THEUAHA.UDPW.MDCIR/FHJXNDSRWPYBNV-CQU-.W/AVAEGJQ3IO.-.E/FCBFXDNDA3/YIUKM-QVR3CDYGHKRZGQJZYDOE43PKPKK/4JMU+U.VBSCPMFCQ.ZQMONNHQSGBUUEGHBNQ+KYV.NKEDDZ4I43GIXJSQWKKPISSXRZPH-.KQUEN3/+FWYATQUQXOXENY/MSWZFZMLFSZ4KMUPO/CIPCTSLGETRI.LLG/LJJ.TRR+QCYSXQEWZXMO.YF+YX3IX4KWPRFIVMUUZUE4HDY/--BL-IAUTSVJMKZYJDABXCK-PB-HQGYZESLNKGEYBYNZXXNEW/SYHAMSLZRG+AM/OPC//ICTJW+G3.WB+DAGFURHVRLHQA4AOV4ZZ/JUFMVEOYPBUDF3U3KJZN.EBTHGJAFAMZDUQEHGRANOUXXBNKXV4IUO+LYNT.LID.K4WMYCHBWSOKAZ3HHWO.SUT3CDWS+4R4D+EIYMOCPCIB+.4LRFDQZY+Y/VBIYXO3KT/K-PUOFP/Q3.+ZYXT3LAJHW+DGFZPYRTJYSTFVMLEWL-.S3FNSXX3PGB+.+M/GQHZJQ/K/VZTKMVK/SF3CBSRVLFVCHLZKJWCNFANACX+JQLIN4O4Y4WMYSLGXT43RHFK.+HIJ+4EJQBGPPOHGSB+C3KABZKXTU+P/WDFTWMAURUDLK+PWC4M4TQ.Z", + expectedMatch: /15 08 : a969/, + recipeConfig: [ + { + "op": "Colossus", + "args": [ + "", + "KH Pattern", + "ΔZ", "ΔΧ", "", + "None", "Select Program", "4=5=/1=2 (Given X1,X2 find X4,X5)", + "", + "", "", "", "", "", false, "", + "", "", "", "", "", false, "", + "", "", "", "", "", false, "", + false, + "", + false, false, false, false, false, + "", false, false, "", + "", + 960, "X4", "X5", + "31", "5", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1" + ] + } + ] + }, +]);