Browse Source

Added Colossus operation

VirtualColossus 5 years ago
parent
commit
dfc8f517f2
3 changed files with 1098 additions and 0 deletions
  1. 426 0
      src/core/lib/Colossus.mjs
  2. 155 0
      src/core/lib/Lorenz.mjs
  3. 517 0
      src/core/operations/Colossus.mjs

+ 426 - 0
src/core/lib/Colossus.mjs

@@ -0,0 +1,426 @@
+/**
+ * Colossus - an emulation of the world's first electronic computer
+ *
+ * @author VirtualColossus
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+import OperationError from "../errors/OperationError.mjs";
+import Utils from "../Utils.mjs";
+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.REVERSE_ITAlookup = {};
+        for (const letter in this.ITAlookup) {
+            const code = this.ITAlookup[letter];
+            this.REVERSE_ITAlookup[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 = {};
+
+    }
+
+    /**
+     * Begin a run
+     * @returns {object} -
+     */
+    run() {
+
+        let 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
+            var fastRef = "00";
+            var slowRef = "00";
+            if (fast !== "") fastRef = this.leftPad(this.rotorPtrs[fast],2);
+            if (slow !== "") slowRef = this.leftPad(this.rotorPtrs[slow],2);
+            result.printout += fastRef + " " + slowRef + " : ";
+            for (let c=0;c<5;c++) {
+                if (this.allCounters[c] > this.settotal) {
+                    result.printout += String.fromCharCode(c+97) + this.allCounters[c]+" ";
+                }
+            }
+            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];
+        //console.log(this.Xptr);
+
+        // Run full loop of all character on the input tape (Z)
+        for (let i=0; i<this.ciphertext.length; i++) {
+            charZin = this.ciphertext.charAt(i);
+
+            //console.log('reading tape char #'+i+' : '+charZin);
+
+            // Firstly, we check what inputs are specified on the Q-bus input switches
+            this.getQbusInputs(charZin);
+
+            /*
+             * Pattern conditions on individual impulses. Matching patterns of bits on the Q bus.
+             * This is the top section on Colussus K rack - the Q bus programming switches
+             */
+            this.runQbusProcessingConditional();
+
+            /*
+             * Addition of impulses.
+             * This is the bottom section of Colossus K rack.
+             */
+            this.runQbusProcessingAddition();
+
+            // Step rotors
+            this.stepThyratrons();
+
+        }
+
+    }
+
+    /**
+     * Step thyratron rings to simulate movement of Lorenz rotors
+     * Chi rotors all step one per character
+     * Motor M61 rotor steps one per character, M37 steps dependant on M61 setting
+     * Psi rotors only step dependant on M37 setting + limitation
+     */
+    stepThyratrons() {
+        let X2bPtr = this.Xptr[1]-1;
+        if (X2bPtr==0) X2bPtr = ROTOR_SIZES.X2;
+        let S1bPtr = this.Sptr[0]-1;
+        if (S1bPtr==0) S1bPtr = ROTOR_SIZES.S1;
+
+        const x2sw = this.limitations.X2;
+        const s1sw = this.limitations.S1;
+
+        // Limitation calculations
+        var lim=1;
+        if (x2sw) lim = this.rings.X[2][X2bPtr-1];
+        if (s1sw) {
+            lim = lim ^ this.rings.S[1][S1bPtr-1];
+        }
+
+        //console.log(this.Mptr);
+        //console.log(this.rings.M[2]);
+        const basicmotor = this.rings.M[2][this.Mptr[0]-1];
+        let totalmotor = basicmotor;
+
+        if (x2sw || s1sw) {
+            if (basicmotor===0 && lim===1) {
+                totalmotor = 0;
+            } else {
+                totalmotor = 1;
+            }
+        }
+
+        //console.log('BM='+basicmotor+', TM='+totalmotor);
+
+        // Step Chi rotors
+        for (let r=0; r<5; r++) {
+            this.Xptr[r]++;
+            if (this.Xptr[r] > ROTOR_SIZES["X"+(r+1)]) this.Xptr[r] = 1;
+        }
+
+        if (totalmotor) {
+            //console.log('step Psi');
+            // 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("");
+        //console.log('Zbits = '+this.Zbits);
+        if (this.qbusin.Z == 'Z') {
+            // direct Z
+            this.Qbits = this.Zbits;
+            //console.log('direct Z: Qbits = '+this.Qbits);
+        } 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];
+            }
+
+            //console.log('delta Z: Qbits = '+this.Qbits);
+        }
+        this.ZbitsOneBack = this.Zbits.slice(); // copy value of object, not reference
+
+        //console.log('Zin::Q bus now '+this.Qbits+'['+this.REVERSE_ITAlookup[this.Qbits.join("")]+']');
+
+        // Xbits - the current Chi wheel bits
+        //console.log(this.rings.X);
+        //console.log('Xptr = '+this.Xptr);
+
+        for (let b=0;b<5;b++) {
+            this.Xbits[b] = this.rings.X[b+1][this.Xptr[b]-1];
+        }
+        if (this.qbusin.Chi != "") {
+            //console.log('X Bits '+this.Xbits+'['+this.REVERSE_ITAlookup[this.Xbits.join("")]+']');
+            //console.log('X Char = ' + this.REVERSE_ITAlookup[this.Xbits.join("")]);
+            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];
+                }
+                //console.log('direct X: Qbits = '+this.Qbits);
+            } 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];
+                }
+                //console.log('delta X: Xbits = '+this.Xbits+' Xbits-1 = '+this.XbitsOneBack);
+                //console.log('delta X: Qbits = '+this.Qbits);
+            }
+        }
+        this.XbitsOneBack = this.Xbits.slice();
+        //console.log('setting XbitsOneBack to '+this.Xbits);
+
+        //console.log('Zin+Xin::Q bus now '+this.Qbits+'['+this.REVERSE_ITAlookup[this.Qbits.join("")]+']');
+
+        // Sbits - the current Psi wheel bits
+        //console.log(this.rings.S);
+        //console.log('Sptr = '+this.Sptr);
+        for (let b=0;b<5;b++) {
+            this.Sbits[b] = this.rings.S[b+1][this.Sptr[b]-1];
+        }
+        if (this.qbusin.Psi != "") {
+            //console.log('S Bits '+this.Sbits+'['+this.REVERSE_ITAlookup[this.Sbits.join("")]+']');
+            //console.log('S Char = ' + this.REVERSE_ITAlookup[this.Sbits.join("")]);
+            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];
+                }
+                //console.log('direct S: Qbits = '+this.Qbits);
+            } 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];
+                }
+                //console.log('delta S: Qbits = '+this.Qbits);
+            }
+        }
+        this.SbitsOneBack = this.Sbits.slice();
+
+        //console.log('Q bus now '+this.Qbits+'['+this.REVERSE_ITAlookup[this.Qbits.join("")]+']');
+    }
+
+    /**
+     * Conditional impulse Q bus section
+     */
+    runQbusProcessingConditional() {
+        let cnt = [-1, -1, -1, -1, -1];
+        const numrows = this.qbusswitches.condition.length;
+
+        for (let r=0;r<numrows;r++) {
+            let row = this.qbusswitches.condition[r];
+            if (row.Counter !== "") {
+                let result = true;
+                let cPnt = row.Counter-1;
+                const Qswitch = this.readBusSwitches(row.Qswitches);
+                // Match switches to bit pattern
+                for (let s=0;s<5;s++) {
+                    if (Qswitch[s] >= 0 && Qswitch[s] != this.Qbits[s]) 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==0) {
+                    cnt[cPnt] = 0;
+                }
+            }
+        }
+
+        // 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] = !cnt[c];
+
+            if (cnt[c]==1) this.allCounters[c]++;  
+        }
+
+    }
+
+    /**
+     * Addition of impulses Q bus section
+     */
+    runQbusProcessingAddition() {
+        let row = this.qbusswitches.addition[0];
+        const Qswitch = row.Qswitches.slice();
+        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) {
+                this.allCounters[0]++;
+            }
+        }
+        //console.log("counter1="+this.allCounters[0]);
+    }
+
+    /**
+     * 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()
+            }
+        };
+    }
+
+    /**
+     * Left Pad number
+     */
+    leftPad(number, targetLength) {
+        let output = number + "";
+        while (output.length < targetLength) {
+            output = "0" + output;
+        }
+        return output;
+    }
+
+    /**
+     * Read argument bus switches X & . and convert to 1 & 0
+     */
+    readBusSwitches(row) {
+        let 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;
+    }
+
+}

+ 155 - 0
src/core/lib/Lorenz.mjs

@@ -0,0 +1,155 @@
+/**
+ * Resources required by the Lorenz SZ40/42 and Colossus
+ *
+ * @author VirtualColossus
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+import OperationError from "../errors/OperationError.mjs";
+
+export const SWITCHES = [
+    {name: "Up (.)", value: "."},
+    {name: "Centre", value: ""},
+    {name: "Down (x)", value: "x"}
+];
+
+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]
+        }
+    }
+};

+ 517 - 0
src/core/operations/Colossus.mjs

@@ -0,0 +1,517 @@
+/**
+ * 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 } from "../lib/Lorenz.mjs";
+
+/**
+ * Colossus operation
+ */
+class Colossus extends Operation {
+
+    /**
+     * Lorenz constructor
+     */
+    constructor() {
+        super();
+        this.name = "Colossus";
+        this.module = "Bletchley";
+        this.description = "Colossus ... ";
+        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: ["", "1+2=. (1+2 Break In, Find X1,X2)", "4=3=/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
+            },
+            {
+                name: "Add-Counter1",
+                type: "boolean"
+            },
+            {
+                name: "Add Negate All",
+                type: "boolean"
+            },
+            {
+                name: "Total Motor",
+                type: "boolean"
+            },
+            {
+                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 X1",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start X2",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start X3",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start X4",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start X5",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start M61",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start M37",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start S1",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start S2",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start S3",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start S4",
+                type: "number",
+                value: 1
+            },
+            {
+                name: "Start S5",
+                type: "number",
+                value: 1
+            }
+        ];
+    }
+
+    /**
+     * @param {string} input
+     * @param {Object[]} args
+     * @returns {Object}
+     */
+    run(input, args) {
+
+        const pattern = args[1];
+        const qbusin = {
+            "Z": args[2],
+            "Chi": args[3],
+            "Psi": args[4],
+        };
+
+        const limitation = args[5];
+        let 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);
+        }
+
+        // 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 = args[42];
+
+        // null|fast|slow for each of S1-5,M1-2,X1-5
+        const control = {
+            fast: args[43],
+            slow: args[44]
+        };
+
+        // Start positions
+        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();
+
+        console.log(result);
+
+        return result;
+
+    }
+
+    /**
+     * Select Program
+     */
+    selectProgram(progname, args) {
+
+        // 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=3=/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) {
+        console.log("output="+ typeof(output));
+        console.log("counters="+ typeof(output.counters));
+
+        let html = "Colossus Printer\n\n";
+        html += output.printout + "\n\n";
+        html += "Colossus Counters\n\n";
+        html += "<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>C1</th>  <th>C2</th>  <th>C3</th>  <th>C4</th>  <th>C5</th></tr>\n";
+        html += "<tr>";
+        for (const ct of output.counters) {
+            html += `<td>${ct}</td>\n`;
+        }
+        html += "</tr>";
+        html += "</table>";
+        return html;
+    }
+
+}
+
+export default Colossus;