Added tests for enigma machine and bombe
authorNeil Smith <neil.git@njae.me.uk>
Fri, 3 Jun 2016 20:00:12 +0000 (21:00 +0100)
committerNeil Smith <neil.git@njae.me.uk>
Wed, 4 Oct 2017 08:21:26 +0000 (09:21 +0100)
Untitled.ipynb [new file with mode: 0644]
bombe.ipynb
bombe.py [new file with mode: 0644]
enigma.py
test_bombe.py [new file with mode: 0644]
test_enigma.py [new file with mode: 0644]

diff --git a/Untitled.ipynb b/Untitled.ipynb
new file mode 100644 (file)
index 0000000..0a8a3e0
--- /dev/null
@@ -0,0 +1,181 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "from enigma import *"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "e31 = Enigma(reflector_b_spec, \n",
+    "                 wheel_i_spec, wheel_i_pegs,\n",
+    "                 wheel_v_spec, wheel_v_pegs,\n",
+    "                 wheel_iii_spec, wheel_iii_pegs,\n",
+    "                 6, 20, 24, \n",
+    "                    'ua pf rq so ni ey bg hl tx zj')\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "def advance(e, n):\n",
+    "    def print_state(e):\n",
+    "        print('        self.assertEqual(self.enigma31.wheel_positions, {})'.format(e.wheel_positions))\n",
+    "        print(\"        self.assertEqual(cat(self.enigma31.wheel_positions_l), '{}')\".format(cat(e.wheel_positions_l)))\n",
+    "        print('        self.assertEqual(self.enigma31.peg_positions, {})'.format(e.peg_positions))\n",
+    "        print()\n",
+    "              \n",
+    "        \n",
+    "    \n",
+    "    print_state(e)\n",
+    "    for i in range(n):\n",
+    "        print('        self.engima31.advance()')\n",
+    "        e.advance()\n",
+    "        print_state(e)\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "        self.assertEqual(self.enigma31.wheel_positions, (21, 5, 22))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ayt')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([16], [1], [2]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (21, 5, 23))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ayu')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([16], [1], [1]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (21, 5, 24))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ayv')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([16], [1], [0]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (21, 6, 25))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'azw')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([16], [0], [25]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 0))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bax')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [24]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 1))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bay')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [23]))\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "e31.set_wheels('a', 'y', 't')\n",
+    "advance(e31, 5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "        self.assertEqual(self.enigma31.wheel_positions, (21, 6, 21))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'azs')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([16], [0], [3]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 22))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bat')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [2]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 23))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bau')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [1]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 24))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bav')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [0]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (22, 8, 25))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bbw')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([15], [24], [25]))\n",
+      "\n",
+      "        self.engima31.advance()\n",
+      "        self.assertEqual(self.enigma31.wheel_positions, (22, 8, 0))\n",
+      "        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bbx')\n",
+      "        self.assertEqual(self.enigma31.peg_positions, ([15], [24], [24]))\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "e31.set_wheels('a', 'z', 's')\n",
+    "advance(e31, 5)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.5.1+"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
index c340539c46ee1642bb80f01ac9ee08a8b0e1fed0..99770da11e6c9b203fb3b309c4ea06707ae77b9d 100644 (file)
@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 1,
    "metadata": {
     "collapsed": true
    },
     "collapsed": true
    },
    "outputs": [],
-   "source": [
-    "# wheel_i_spec = 'ekmflgdqvzntowyhxuspaibrcj'\n",
-    "# wheel_ii_spec = 'ajdksiruxblhwtmcqgznpyfvoe'\n",
-    "# wheel_iii_spec = 'bdfhjlcprtxvznyeiwgakmusqo'\n",
-    "# wheel_iv_spec = 'esovpzjayquirhxlnftgkdcmwb'\n",
-    "# wheel_v_spec = 'vzbrgityupsdnhlxawmjqofeck'\n",
-    "# wheel_vi_spec = 'jpgvoumfyqbenhzrdkasxlictw'\n",
-    "# wheel_vii_spec = 'nzjhgrcxmyswboufaivlpekqdt'\n",
-    "# wheel_viii_spec = 'fkqhtlxocbjspdzramewniuygv'\n",
-    "# beta_wheel_spec = 'leyjvcnixwpbqmdrtakzgfuhos'\n",
-    "# gamma_wheel_spec = 'fsokanuerhmbtiycwlqpzxvgjd'\n",
-    "\n",
-    "# wheel_i_pegs = ['q']\n",
-    "# wheel_ii_pegs = ['e']\n",
-    "# wheel_iii_pegs = ['v']\n",
-    "# wheel_iv_pegs = ['j']\n",
-    "# wheel_v_pegs = ['z']\n",
-    "# wheel_vi_pegs = ['z', 'm']\n",
-    "# wheel_vii_pegs = ['z', 'm']\n",
-    "# wheel_viii_pegs = ['z', 'm']\n",
-    "\n",
-    "# reflector_b_spec = 'ay br cu dh eq fs gl ip jx kn mo tz vw'\n",
-    "# reflector_c_spec = 'af bv cp dj ei go hy kr lz mx nw tq su'"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 4,
-   "metadata": {
-    "collapsed": true
-   },
-   "outputs": [],
    "source": [
     "Signal = collections.namedtuple('Signal', ['bank', 'wire'])\n",
     "Connection = collections.namedtuple('Connection', ['banks', 'scrambler'])\n",
@@ -62,7 +30,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 4,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 5,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 804,
+   "execution_count": 6,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 805,
+   "execution_count": 7,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 806,
+   "execution_count": 8,
    "metadata": {
     "collapsed": false
    },
        "'opgndxcrwomnlnecjz'"
       ]
      },
-     "execution_count": 806,
+     "execution_count": 8,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 807,
+   "execution_count": 9,
    "metadata": {
     "collapsed": false
    },
        "'aas'"
       ]
      },
-     "execution_count": 807,
+     "execution_count": 9,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 808,
+   "execution_count": 10,
    "metadata": {
     "collapsed": false
    },
        " MenuIem(before='e', after='z', number=18)]"
       ]
      },
-     "execution_count": 808,
+     "execution_count": 10,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 809,
+   "execution_count": 11,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 810,
+   "execution_count": 12,
    "metadata": {
     "collapsed": false
    },
        " MenuIem(before='e', after='z', number=18)]"
       ]
      },
-     "execution_count": 810,
+     "execution_count": 12,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 811,
+   "execution_count": 13,
    "metadata": {
     "collapsed": false
    },
        "'s'"
       ]
      },
-     "execution_count": 811,
+     "execution_count": 13,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 812,
+   "execution_count": 14,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 813,
+   "execution_count": 15,
    "metadata": {
     "collapsed": false
    },
        "18"
       ]
      },
-     "execution_count": 813,
+     "execution_count": 15,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 814,
+   "execution_count": 16,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 815,
+   "execution_count": 17,
    "metadata": {
     "collapsed": false
    },
        "False"
       ]
      },
-     "execution_count": 815,
+     "execution_count": 17,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 816,
+   "execution_count": 18,
    "metadata": {
     "collapsed": false
    },
        " 'z': True}"
       ]
      },
-     "execution_count": 816,
+     "execution_count": 18,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 817,
+   "execution_count": 19,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 818,
+   "execution_count": 20,
    "metadata": {
     "collapsed": false
    },
        "('a', 'a', 'a')"
       ]
      },
-     "execution_count": 818,
+     "execution_count": 20,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 819,
+   "execution_count": 21,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 820,
+   "execution_count": 22,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 821,
+   "execution_count": 23,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 822,
+   "execution_count": 24,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 823,
+   "execution_count": 25,
    "metadata": {
     "collapsed": false
    },
        "1"
       ]
      },
-     "execution_count": 823,
+     "execution_count": 25,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 824,
+   "execution_count": 26,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 825,
+   "execution_count": 27,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 826,
+   "execution_count": 28,
    "metadata": {
     "collapsed": false
    },
        "('a', 'a', 'b')"
       ]
      },
-     "execution_count": 826,
+     "execution_count": 28,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 827,
+   "execution_count": 29,
    "metadata": {
     "collapsed": false
    },
        "False"
       ]
      },
-     "execution_count": 827,
+     "execution_count": 29,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 828,
+   "execution_count": 30,
    "metadata": {
     "collapsed": false
    },
        "('p', 'p', 'p')"
       ]
      },
-     "execution_count": 828,
+     "execution_count": 30,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 829,
+   "execution_count": 31,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 830,
+   "execution_count": 32,
    "metadata": {
     "collapsed": false
    },
        "17576"
       ]
      },
-     "execution_count": 830,
+     "execution_count": 32,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 831,
+   "execution_count": 33,
    "metadata": {
     "collapsed": false
    },
        "(('a', 'a', 'b'), True)"
       ]
      },
-     "execution_count": 831,
+     "execution_count": 33,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 832,
+   "execution_count": 34,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 833,
+   "execution_count": 35,
    "metadata": {
     "collapsed": false
    },
        "[('a', 'a', 'b')]"
       ]
      },
-     "execution_count": 833,
+     "execution_count": 35,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 857,
+   "execution_count": 36,
    "metadata": {
     "collapsed": true
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 835,
+   "execution_count": 37,
    "metadata": {
     "collapsed": false
    },
        "[('a', 'a', 'b')]"
       ]
      },
-     "execution_count": 835,
+     "execution_count": 37,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 836,
+   "execution_count": 38,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 837,
+   "execution_count": 39,
    "metadata": {
     "collapsed": false
    },
        "('e', 'l', 'e')"
       ]
      },
-     "execution_count": 837,
+     "execution_count": 39,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 838,
+   "execution_count": 40,
    "metadata": {
     "collapsed": false
    },
        "'dhnpforeeimgg'"
       ]
      },
-     "execution_count": 838,
+     "execution_count": 40,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 839,
+   "execution_count": 41,
    "metadata": {
     "collapsed": false
    },
        "('j', 'e', 'o')"
       ]
      },
-     "execution_count": 839,
+     "execution_count": 41,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 840,
+   "execution_count": 42,
    "metadata": {
     "collapsed": false
    },
        " MenuIem(before='t', after='g', number=13)]"
       ]
      },
-     "execution_count": 840,
+     "execution_count": 42,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 841,
+   "execution_count": 43,
    "metadata": {
     "collapsed": false,
     "scrolled": true
        " ('z', 'z', 'k')]"
       ]
      },
-     "execution_count": 841,
+     "execution_count": 43,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 842,
+   "execution_count": 44,
    "metadata": {
     "collapsed": false
    },
        "62"
       ]
      },
-     "execution_count": 842,
+     "execution_count": 44,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 843,
+   "execution_count": 45,
    "metadata": {
     "collapsed": false,
     "scrolled": true
        " ('y', 'n', 'c')]"
       ]
      },
-     "execution_count": 843,
+     "execution_count": 45,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 858,
+   "execution_count": 46,
    "metadata": {
     "collapsed": false
    },
        " ('y', 'n', 'c')]"
       ]
      },
-     "execution_count": 858,
+     "execution_count": 46,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 844,
+   "execution_count": 47,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 845,
+   "execution_count": 48,
    "metadata": {
     "collapsed": false
    },
        "13"
       ]
      },
-     "execution_count": 845,
+     "execution_count": 48,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 846,
+   "execution_count": 49,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 847,
+   "execution_count": 50,
    "metadata": {
     "collapsed": false
    },
        "Signal(bank='e', wire='e')"
       ]
      },
-     "execution_count": 847,
+     "execution_count": 50,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 848,
+   "execution_count": 51,
    "metadata": {
     "collapsed": false
    },
        "True"
       ]
      },
-     "execution_count": 848,
+     "execution_count": 51,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 849,
+   "execution_count": 52,
    "metadata": {
     "collapsed": false
    },
        "True"
       ]
      },
-     "execution_count": 849,
+     "execution_count": 52,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 850,
+   "execution_count": 53,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 851,
+   "execution_count": 54,
    "metadata": {
     "collapsed": false
    },
   },
   {
    "cell_type": "code",
-   "execution_count": 852,
+   "execution_count": 55,
    "metadata": {
     "collapsed": false
    },
     {
      "data": {
       "text/plain": [
-       "{frozenset({'e', 'y'}),\n",
+       "{frozenset({'b', 'g'}),\n",
+       " frozenset({'e', 'y'}),\n",
        " frozenset({'t', 'x'}),\n",
-       " frozenset({'i', 'n'}),\n",
        " frozenset({'m'}),\n",
-       " frozenset({'b', 'g'}),\n",
+       " frozenset({'i', 'n'}),\n",
        " frozenset({'f', 'p'})}"
       ]
      },
-     "execution_count": 852,
+     "execution_count": 55,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 853,
+   "execution_count": 56,
    "metadata": {
     "collapsed": false
    },
        "True"
       ]
      },
-     "execution_count": 853,
+     "execution_count": 56,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 854,
+   "execution_count": 57,
    "metadata": {
     "collapsed": false
    },
        " frozenset({2, 3}))"
       ]
      },
-     "execution_count": 854,
+     "execution_count": 57,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 855,
+   "execution_count": 58,
    "metadata": {
     "collapsed": false
    },
        "False"
       ]
      },
-     "execution_count": 855,
+     "execution_count": 58,
      "metadata": {},
      "output_type": "execute_result"
     }
   },
   {
    "cell_type": "code",
-   "execution_count": 856,
+   "execution_count": 59,
    "metadata": {
     "collapsed": false
    },
        "False"
       ]
      },
-     "execution_count": 856,
+     "execution_count": 59,
      "metadata": {},
      "output_type": "execute_result"
     }
diff --git a/bombe.py b/bombe.py
new file mode 100644 (file)
index 0000000..acaad0e
--- /dev/null
+++ b/bombe.py
@@ -0,0 +1,163 @@
+import string
+import collections
+import multiprocessing
+import itertools
+from enigma import *
+
+
+Signal = collections.namedtuple('Signal', ['bank', 'wire'])
+Connection = collections.namedtuple('Connection', ['banks', 'scrambler'])
+MenuItem = collections.namedtuple('MenuIem', ['before', 'after', 'number'])
+
+
+class Scrambler(object):
+    def __init__(self, wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec,
+                 wheel1_pos='a', wheel2_pos='a', wheel3_pos='a'):
+        self.wheel1 = SimpleWheel(wheel1_spec, position=wheel1_pos)
+        self.wheel2 = SimpleWheel(wheel2_spec, position=wheel2_pos)
+        self.wheel3 = SimpleWheel(wheel3_spec, position=wheel3_pos)
+        self.reflector = Reflector(reflector_spec)
+    
+    def __getattribute__(self, name):
+        if name=='wheel_positions':
+            return self.wheel1.position, self.wheel2.position, self.wheel3.position 
+        elif name=='wheel_positions_l':
+            return self.wheel1.position_l, self.wheel2.position_l, self.wheel3.position_l 
+        else:
+            return object.__getattribute__(self, name)
+    
+    def advance(self, wheel1=False, wheel2=False, wheel3=True):
+        if wheel1: self.wheel1.advance()
+        if wheel2: self.wheel2.advance()
+        if wheel3: self.wheel3.advance()
+            
+    def lookup(self, letter):
+        a = self.wheel3.forward(letter)
+        b = self.wheel2.forward(a)
+        c = self.wheel1.forward(b)
+        d = self.reflector.forward(c)
+        e = self.wheel1.backward(d)
+        f = self.wheel2.backward(e)
+        g = self.wheel3.backward(f)
+        return g
+    
+    def set_positions(self, wheel1_pos, wheel2_pos, wheel3_pos):
+        self.wheel1.set_position(wheel1_pos)
+        self.wheel2.set_position(wheel2_pos)
+        self.wheel3.set_position(wheel3_pos)        
+
+
+class Bombe(object):
+    
+    def __init__(self, wheel1_spec, wheel2_spec, wheel3_spec, reflector_spec,
+                menu=None, start_signal=None, use_diagonal_board=True, 
+                verify_plugboard=True):
+        self.connections = []
+        self.wheel1_spec = wheel1_spec
+        self.wheel2_spec = wheel2_spec
+        self.wheel3_spec = wheel3_spec
+        self.reflector_spec = reflector_spec
+        if menu:
+            self.read_menu(menu)
+        if start_signal:
+            self.test_start = start_signal
+        self.use_diagonal_board = use_diagonal_board
+        self.verify_plugboard = verify_plugboard
+        
+    def __getattribute__(self, name):
+        if name=='wheel_positions':
+            return self.connections[0].scrambler.wheel_positions
+        elif name=='wheel_positions_l':
+            return self.connections[0].scrambler.wheel_positions_l
+        else:
+            return object.__getattribute__(self, name)
+        
+    def __call__(self, start_positions):
+        return start_positions, self.test(initial_signal=self.test_start,
+            start_positions=start_positions, 
+            use_diagonal_board=self.use_diagonal_board,
+            verify_plugboard=self.verify_plugboard)
+        
+    def add_connection(self, bank_before, bank_after, scrambler):
+        self.connections += [Connection([bank_before, bank_after], scrambler)]
+        
+    def read_menu(self, menu):
+        for item in menu:
+            scrambler = Scrambler(self.wheel1_spec, self.wheel2_spec, self.wheel3_spec,
+                                  self.reflector_spec,
+                                  wheel3_pos=unpos(item.number - 1))
+            self.add_connection(item.before, item.after, scrambler)
+        most_common_letter = (collections.Counter(m.before for m in menu) +                               collections.Counter(m.after for m in menu)).most_common(1)[0][0]
+        self.test_start = Signal(most_common_letter, most_common_letter)
+        
+    def set_positions(self, wheel1_pos, wheel2_pos, wheel3_pos):
+        for i, c in enumerate(self.connections):
+            c.scrambler.set_positions(wheel1_pos, wheel2_pos, unpos(pos(wheel3_pos) + i))
+    
+    def test(self, initial_signal=None, start_positions=None, use_diagonal_board=True,
+            verify_plugboard=True):
+        self.banks = {label: 
+                      dict(zip(string.ascii_lowercase, [False]*len(string.ascii_lowercase)))
+                      for label in string.ascii_lowercase}
+        if start_positions:
+            self.set_positions(*start_positions)
+        if not initial_signal:
+            initial_signal = self.test_start
+        self.pending = [initial_signal]
+        self.propagate(use_diagonal_board)
+        live_wire_count = len([self.banks[self.test_start.bank][w] 
+                    for w in self.banks[self.test_start.bank] 
+                    if self.banks[self.test_start.bank][w]])
+        if live_wire_count < 26:
+            if verify_plugboard:
+                possibles = self.possible_plugboards()
+                return all(s0.isdisjoint(s1) for s0 in possibles for s1 in possibles if s0 != s1)
+            else:
+                return True
+        else:
+            return False
+        
+    def propagate(self, use_diagonal_board):
+        while self.pending:
+            current = self.pending[0]
+            # print("processing", current)
+            self.pending = self.pending[1:]
+            if not self.banks[current.bank][current.wire]:
+                self.banks[current.bank][current.wire] = True
+                if use_diagonal_board:
+                    self.pending += [Signal(current.wire, current.bank)]
+                for c in self.connections:
+                    if current.bank in c.banks:
+                        other_bank = [b for b in c.banks if b != current.bank][0]
+                        other_wire = c.scrambler.lookup(current.wire)
+                        # print("  adding", other_bank, other_wire, "because", c.banks)
+                        self.pending += [Signal(other_bank, other_wire)]
+    
+    def run(self, run_start=None, wheel1_pos='a', wheel2_pos='a', wheel3_pos='a', use_diagonal_board=True):
+        if not run_start:
+            run_start = self.test_start
+        self.solutions = []
+        self.set_positions(wheel1_pos, wheel2_pos, wheel3_pos)
+        for run_index in range(26*26*26):
+            if self.test(initial_signal=run_start, use_diagonal_board=use_diagonal_board):
+                self.solutions += [self.connections[0].scrambler.wheel_positions_l]
+            advance3 = True
+            advance2 = False
+            advance1 = False
+            if (run_index + 1) % 26 == 0: advance2 = True
+            if (run_index + 1) % (26*26) == 0: advance1 = True
+            for c in self.connections:
+                c.scrambler.advance(advance1, advance2, advance3)
+        return self.solutions
+    
+    def possible_plugboards(self):
+        possibles = set()
+        for b in self.banks:
+            active = [w for w in self.banks[b] if self.banks[b][w]]
+            inactive = [w for w in self.banks[b] if not self.banks[b][w]]
+            if len(active) == 1:
+                possibles = possibles.union({frozenset((b, active[0]))})
+            if len(inactive) == 1:
+                possibles = possibles.union({frozenset((b, inactive[0]))})
+        return possibles
+
index cb57f7a26fd33a43bbddabac7bb9b9093ed3bf66..8c72f22bf558887842061541b64bfbb7918139f2 100644 (file)
--- a/enigma.py
+++ b/enigma.py
@@ -63,24 +63,6 @@ class LetterTransformer(object):
     """A generic substitution cipher, that has different transforms in the 
     forward and backward directions. It requires that the transforms for all
     letters by provided.
-
-    >>> lt = LetterTransformer([('z', 'a')] + [(l, string.ascii_lowercase[i+1]) \
-            for i, l in enumerate(string.ascii_lowercase[:-1])], \
-            raw_transform = True)
-    >>> lt.forward_map
-    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0]
-    >>> lt.backward_map
-    [25, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
-
-    >>> lt = LetterTransformer(cat(collections.OrderedDict.fromkeys('zyxwc' + string.ascii_lowercase)))
-    >>> lt.forward_map
-    [25, 24, 23, 22, 2, 0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
-    >>> lt.backward_map
-    [5, 6, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 3, 2, 1, 0]
-    >>> cat(lt.forward(l) for l in string.ascii_lowercase)
-    'zyxwcabdefghijklmnopqrstuv'
-    >>> cat(lt.backward(l) for l in string.ascii_lowercase)
-    'fgehijklmnopqrstuvwxyzdcba'
     """
     def __init__(self, specification, raw_transform=False):
         if raw_transform:
@@ -136,16 +118,6 @@ class Plugboard(LetterTransformer):
     """A plugboard, a type of letter transformer where forward and backward
     transforms are the same. If a letter isn't explicitly transformed, it is 
     kept as it is.
-
-    >>> pb = Plugboard('ua pf rq so ni ey bg hl tx zj'.upper())
-    >>> pb.forward_map
-    [20, 6, 2, 3, 24, 15, 1, 11, 13, 25, 10, 7, 12, 8, 18, 5, 17, 16, 14, 23, 0, 21, 22, 19, 4, 9]
-    >>> pb.forward_map == pb.backward_map
-    True
-    >>> cat(pb.forward(l) for l in string.ascii_lowercase)
-    'ugcdypblnzkhmisfrqoxavwtej'
-    >>> cat(pb.backward(l) for l in string.ascii_lowercase)
-    'ugcdypblnzkhmisfrqoxavwtej'
     """
     def parse_specification(self, specification):
         return [tuple(clean(p)) for p in specification.split()]
@@ -170,16 +142,6 @@ class Plugboard(LetterTransformer):
 
 class Reflector(Plugboard):
     """A reflector is a plugboard that requires 13 transforms.
-
-    >>> reflector_b = Reflector(reflector_b_spec)
-    >>> reflector_b.forward_map == reflector_b.backward_map
-    True
-    >>> reflector_b.forward_map
-    [24, 17, 20, 7, 16, 18, 11, 3, 15, 23, 13, 6, 14, 10, 12, 8, 4, 1, 5, 25, 2, 22, 21, 9, 0, 19]
-    >>> cat(reflector_b.forward(l) for l in string.ascii_lowercase)
-    'yruhqsldpxngokmiebfzcwvjat'
-    >>> cat(reflector_b.backward(l) for l in string.ascii_lowercase)
-    'yruhqsldpxngokmiebfzcwvjat'
     """
     def validate_transform(self, transform):
         if len(transform) != 13:
@@ -208,52 +170,6 @@ class SimpleWheel(LetterTransformer):
     Letter inputs and outputs are given relative to the frame holding the wheel,
     so if the wheel is advanced three places, an input of 'p' will enter the 
     wheel on the position under the wheel's 'q' label.
-
-    >>> rotor_1_transform = list(zip(string.ascii_lowercase, 'EKMFLGDQVZNTOWYHXUSPAIBRCJ'.lower()))
-    >>> wheel_1 = SimpleWheel(rotor_1_transform, raw_transform=True)
-    >>> cat(wheel_1.forward(l) for l in string.ascii_lowercase)
-    'ekmflgdqvzntowyhxuspaibrcj'
-    >>> cat(wheel_1.backward(l) for l in string.ascii_lowercase)
-    'uwygadfpvzbeckmthxslrinqoj'
-
-
-    >>> wheel_2 = SimpleWheel(wheel_ii_spec)
-    >>> cat(wheel_2.forward(l) for l in string.ascii_lowercase)
-    'ajdksiruxblhwtmcqgznpyfvoe'
-    >>> cat(wheel_2.backward(l) for l in string.ascii_lowercase)
-    'ajpczwrlfbdkotyuqgenhxmivs'
-
-    >>> wheel_3 = SimpleWheel(wheel_iii_spec)
-    >>> wheel_3.set_position('a')
-    >>> wheel_3.advance()
-    >>> cat(wheel_3.forward(l) for l in string.ascii_lowercase)
-    'cegikboqswuymxdhvfzjltrpna'
-    >>> cat(wheel_3.backward(l) for l in string.ascii_lowercase)
-    'zfaobrcpdteumygxhwivkqjnls'
-    >>> wheel_3.position
-    1
-    >>> wheel_3.position_l
-    'b'
-
-    >>> for _ in range(24): wheel_3.advance()
-    >>> wheel_3.position
-    25
-    >>> wheel_3.position_l
-    'z'
-    >>> cat(wheel_3.forward(l) for l in string.ascii_lowercase)
-    'pcegikmdqsuywaozfjxhblnvtr'
-    >>> cat(wheel_3.backward(l) for l in string.ascii_lowercase)
-    'nubhcqdterfvgwoaizjykxmslp'
-
-    >>> wheel_3.advance()
-    >>> wheel_3.position
-    0
-    >>> wheel_3.position_l
-    'a'
-    >>> cat(wheel_3.forward(l) for l in string.ascii_lowercase)
-    'bdfhjlcprtxvznyeiwgakmusqo'
-    >>> cat(wheel_3.backward(l) for l in string.ascii_lowercase)
-    'tagbpcsdqeufvnzhyixjwlrkom'
     """
     def __init__(self, transform, position='a', raw_transform=False):
         super(SimpleWheel, self).__init__(transform, raw_transform)
@@ -304,119 +220,6 @@ class Wheel(SimpleWheel):
     The peg_positions are the number of advances of this wheel before it will 
     advance the next wheel.
 
-    >>> wheel_3 = Wheel(wheel_iii_spec, wheel_iii_pegs, position='b', ring_setting=1)
-    >>> wheel_3.position
-    1
-    >>> wheel_3.peg_positions
-    [20]
-    >>> wheel_3.position_l
-    'b'
-    >>> wheel_3.advance()
-    >>> wheel_3.position
-    2
-    >>> wheel_3.peg_positions
-    [19]
-    >>> wheel_3.position_l
-    'c'
-
-    >>> wheel_6 = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', ring_setting=3)
-    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
-    'xkqhwpvngzrcfoiaselbtymjdu'
-    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
-    'ptlyrmidoxbswhnfckquzgeavj'
-    >>> wheel_6.position
-    25
-    >>> 11 in wheel_6.peg_positions
-    True
-    >>> 24 in wheel_6.peg_positions
-    True
-    >>> wheel_6.position_l
-    'b'
-
-    >>> wheel_6.advance()
-    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
-    'jpgvoumfyqbenhzrdkasxlictw'
-    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
-    'skxqlhcnwarvgmebjptyfdzuio'
-    >>> wheel_6.position
-    0
-    >>> 10 in wheel_6.peg_positions
-    True
-    >>> 23 in wheel_6.peg_positions
-    True
-    >>> wheel_6.position_l
-    'c'
-
-    >>> for _ in range(22): wheel_6.advance()
-    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
-    'mgxantkzsyqjcufirldvhoewbp'
-    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
-    'dymswobuplgraevzkqifntxcjh'
-    >>> wheel_6.position
-    22
-    >>> 1 in wheel_6.peg_positions
-    True
-    >>> 14 in wheel_6.peg_positions
-    True
-    >>> wheel_6.position_l
-    'y'
-
-    >>> wheel_6.advance()
-    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
-    'fwzmsjyrxpibtehqkcugndvaol'
-    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
-    'xlrvnatokfqzduyjphemswbigc'
-    >>> wheel_6.position
-    23
-    >>> 0 in wheel_6.peg_positions
-    True
-    >>> 13 in wheel_6.peg_positions
-    True
-    >>> wheel_6.position_l
-    'z'
-
-    >>> wheel_6.advance()
-    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
-    'vylrixqwohasdgpjbtfmcuznke'
-    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
-    'kqumzsnjepyctxiogdlrvahfbw'
-    >>> wheel_6.position
-    24
-    >>> 25 in wheel_6.peg_positions
-    True
-    >>> 12 in wheel_6.peg_positions
-    True
-    >>> wheel_6.position_l
-    'a'
-
-    >>> wheel_6.advance()
-    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
-    'xkqhwpvngzrcfoiaselbtymjdu'
-    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
-    'ptlyrmidoxbswhnfckquzgeavj'
-    >>> wheel_6.position
-    25
-    >>> 24 in wheel_6.peg_positions
-    True
-    >>> 11 in wheel_6.peg_positions
-    True
-    >>> wheel_6.position_l
-    'b'
-
-    >>> wheel_6.advance()
-    >>> cat(wheel_6.forward(l) for l in string.ascii_lowercase)
-    'jpgvoumfyqbenhzrdkasxlictw'
-    >>> cat(wheel_6.backward(l) for l in string.ascii_lowercase)
-    'skxqlhcnwarvgmebjptyfdzuio'
-    >>> wheel_6.position
-    0
-    >>> 23 in wheel_6.peg_positions
-    True
-    >>> 10 in wheel_6.peg_positions
-    True
-    >>> wheel_6.position_l
-    'c'
-
     """
     def __init__(self, transform, ring_peg_letters, ring_setting=1, position='a', raw_transform=False):
         self.ring_peg_letters = ring_peg_letters
@@ -442,489 +245,7 @@ class Wheel(SimpleWheel):
 class Enigma(object):
     """An Enigma machine.
 
-    >>> enigma = Enigma(reflector_b_spec, \
-                wheel_i_spec, wheel_i_pegs, \
-                wheel_ii_spec, wheel_ii_pegs, \
-                wheel_iii_spec, wheel_iii_pegs, \
-                1, 1, 1, \
-                '')
-    >>> enigma.set_wheels('a', 'a', 't')
-    >>> enigma.wheel_positions
-    (0, 0, 19)
-    >>> cat(enigma.wheel_positions_l)
-    'aat'
-    >>> enigma.peg_positions
-    ([16], [4], [2])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'puvioztjdhxmlyeawsrgbcqknf'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (0, 0, 20)
-    >>> cat(enigma.wheel_positions_l)
-    'aau'
-    >>> enigma.peg_positions
-    ([16], [4], [1])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'baigpldqcowfyzjehvtsxrkumn'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (0, 0, 21)
-    >>> cat(enigma.wheel_positions_l)
-    'aav'
-    >>> enigma.peg_positions
-    ([16], [4], [0])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'mnvfydiwgzsoablrxpkutchqej'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (0, 1, 22)
-    >>> cat(enigma.wheel_positions_l)
-    'abw'
-    >>> enigma.peg_positions
-    ([16], [3], [25])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'ulfopcykswhbzvderqixanjtgm'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (0, 1, 23)
-    >>> cat(enigma.wheel_positions_l)
-    'abx'
-    >>> enigma.peg_positions
-    ([16], [3], [24])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'qmwftdyovursbzhxaklejicpgn'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (0, 1, 24)
-    >>> cat(enigma.wheel_positions_l)
-    'aby'
-    >>> enigma.peg_positions
-    ([16], [3], [23])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'oljmzxrvucybdqasngpwihtfke'
-
-
-
-
-    >>> enigma.set_wheels('a', 'd', 't')
-    >>> enigma.wheel_positions
-    (0, 3, 19)
-    >>> cat(enigma.wheel_positions_l)
-    'adt'
-    >>> enigma.peg_positions
-    ([16], [1], [2])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'zcbpqxwsjiuonmldethrkygfva'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (0, 3, 20)
-    >>> cat(enigma.wheel_positions_l)
-    'adu'
-    >>> enigma.peg_positions
-    ([16], [1], [1])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'ehprawjbngotxikcsdqlzyfmvu'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (0, 3, 21)
-    >>> cat(enigma.wheel_positions_l)
-    'adv'
-    >>> enigma.peg_positions
-    ([16], [1], [0])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'eqzxarpihmnvjkwgbfuyslodtc'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (0, 4, 22)
-    >>> cat(enigma.wheel_positions_l)
-    'aew'
-    >>> enigma.peg_positions
-    ([16], [0], [25])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'qedcbtpluzmhkongavwfirsyxj'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (1, 5, 23)
-    >>> cat(enigma.wheel_positions_l)
-    'bfx'
-    >>> enigma.peg_positions
-    ([15], [25], [24])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'iwuedhsfazqxytvrkpgncoblmj'
-
-    >>> enigma.advance()
-    >>> enigma.wheel_positions
-    (1, 5, 24)
-    >>> cat(enigma.wheel_positions_l)
-    'bfy'
-    >>> enigma.peg_positions
-    ([15], [25], [23])
-    >>> cat(enigma.lookup(l) for l in string.ascii_lowercase)
-    'baknstqzrmcxjdvygiefwoulph'
-
-
-    >>> enigma.set_wheels('a', 'a', 'a')
-    >>> ct = enigma.encipher('testmessage')
-    >>> ct
-    'olpfhnvflyn'
-
-    >>> enigma.set_wheels('a', 'd', 't')
-    >>> ct = enigma.encipher('testmessage')
-    >>> ct
-    'lawnjgpwjik'
-
-
-    >>> enigma.set_wheels('b', 'd', 'q')
-    >>> ct = enigma.encipher('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
-    >>> ct
-    'kvmmwrlqlqsqpeugjrcxzwpfyiyybwloewrouvkpoztceuwtfjzqwpbqldttsr'
-    >>> enigma.left_wheel.position_l
-    'c'
-    >>> enigma.middle_wheel.position_l
-    'h'
-    >>> enigma.right_wheel.position_l
-    'a'
-
-    # Setting sheet line 31 from http://www.codesandciphers.org.uk/enigma/enigma3.htm
-    # Enigma simulation settings are 
-    # http://enigma.louisedade.co.uk/enigma.html?m3;b;b153;AFTX;AJEU;AU-BG-EY-FP-HL-IN-JZ-OS-QR-TX
-    >>> enigma31 = Enigma(reflector_b_spec, \
-                wheel_i_spec, wheel_i_pegs, \
-                wheel_v_spec, wheel_v_pegs, \
-                wheel_iii_spec, wheel_iii_pegs, \
-                6, 20, 24, \
-                'ua pf rq so ni ey bg hl tx zj')
-
-    >>> enigma31.set_wheels('j', 'e', 'u')
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (4, 11, 24)
-    >>> cat(enigma31.wheel_positions_l)
-    'jev'
-    >>> enigma31.peg_positions
-    ([7], [21], [0])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'mvqjlyowkdieasgzcunxrbhtfp'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (4, 12, 25)
-    >>> cat(enigma31.wheel_positions_l)
-    'jfw'
-    >>> enigma31.peg_positions
-    ([7], [20], [25])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'sjolzuyvrbwdpxcmtiaqfhknge'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (4, 12, 0)
-    >>> cat(enigma31.wheel_positions_l)
-    'jfx'
-    >>> enigma31.peg_positions
-    ([7], [20], [24])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'qrxedkoywufmlvgsabpzjnicht'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (4, 12, 1)
-    >>> cat(enigma31.wheel_positions_l)
-    'jfy'
-    >>> enigma31.peg_positions
-    ([7], [20], [23])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'hpsukliagqefwvtbjxcodnmrzy'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (4, 12, 2)
-    >>> cat(enigma31.wheel_positions_l)
-    'jfz'
-    >>> enigma31.peg_positions
-    ([7], [20], [22])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'zevnbpyqowrtxdifhkulscjmga'
-
-
-    >>> enigma31.set_wheels('i', 'd', 'z')
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 3)
-    >>> cat(enigma31.wheel_positions_l)
-    'ida'
-    >>> enigma31.peg_positions
-    ([8], [22], [21])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'ikhpqrvcambzjondefwyxgsutl'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 4)
-    >>> cat(enigma31.wheel_positions_l)
-    'idb'
-    >>> enigma31.peg_positions
-    ([8], [22], [20])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'cdabskhgzwfmlqvunyexpojtri'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 5)
-    >>> cat(enigma31.wheel_positions_l)
-    'idc'
-    >>> enigma31.peg_positions
-    ([8], [22], [19])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'pcbwiqhgemyvjsuaftnroldzkx'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 6)
-    >>> cat(enigma31.wheel_positions_l)
-    'idd'
-    >>> enigma31.peg_positions
-    ([8], [22], [18])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'xcbfvdnouptmlghjzwykierasq'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 7)
-    >>> cat(enigma31.wheel_positions_l)
-    'ide'
-    >>> enigma31.peg_positions
-    ([8], [22], [17])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'xfvglbdynuseriwqpmkzjcoaht'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 8)
-    >>> cat(enigma31.wheel_positions_l)
-    'idf'
-    >>> enigma31.peg_positions
-    ([8], [22], [16])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'tfpqlbouynsewjgcdxkahzmriv'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 9)
-    >>> cat(enigma31.wheel_positions_l)
-    'idg'
-    >>> enigma31.peg_positions
-    ([8], [22], [15])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'cjaunvlwtbygzexrspqidfhokm'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 10)
-    >>> cat(enigma31.wheel_positions_l)
-    'idh'
-    >>> enigma31.peg_positions
-    ([8], [22], [14])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'yltxkrqvowebzpingfucshjdam'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 11)
-    >>> cat(enigma31.wheel_positions_l)
-    'idi'
-    >>> enigma31.peg_positions
-    ([8], [22], [13])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'myktluzrnxceaiqsohpdfwvjbg'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 12)
-    >>> cat(enigma31.wheel_positions_l)
-    'idj'
-    >>> enigma31.peg_positions
-    ([8], [22], [12])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'pynjrmiugdqxfcvakewzhoslbt'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 13)
-    >>> cat(enigma31.wheel_positions_l)
-    'idk'
-    >>> enigma31.peg_positions
-    ([8], [22], [11])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'mwvedyplnoxhaijgrqtszcbkfu'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 14)
-    >>> cat(enigma31.wheel_positions_l)
-    'idl'
-    >>> enigma31.peg_positions
-    ([8], [22], [10])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'qcbrfeutvoxpnmjladzhgiykws'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 15)
-    >>> cat(enigma31.wheel_positions_l)
-    'idm'
-    >>> enigma31.peg_positions
-    ([8], [22], [9])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'dnoahryetsmukbcvwfjilpqzgx'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 16)
-    >>> cat(enigma31.wheel_positions_l)
-    'idn'
-    >>> enigma31.peg_positions
-    ([8], [22], [8])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'nidcfehgbqsovalyjzkxwmutpr'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 17)
-    >>> cat(enigma31.wheel_positions_l)
-    'ido'
-    >>> enigma31.peg_positions
-    ([8], [22], [7])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'joifxdulcarhzpbntkwqgysevm'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 18)
-    >>> cat(enigma31.wheel_positions_l)
-    'idp'
-    >>> enigma31.peg_positions
-    ([8], [22], [6])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'ptnlsxvozmwdjchayuebrgkfqi'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 19)
-    >>> cat(enigma31.wheel_positions_l)
-    'idq'
-    >>> enigma31.peg_positions
-    ([8], [22], [5])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'slwopzqnmxybihdeguavrtcjkf'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 20)
-    >>> cat(enigma31.wheel_positions_l)
-    'idr'
-    >>> enigma31.peg_positions
-    ([8], [22], [4])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'hcbedwlamzogixkytsrqvufnpj'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 21)
-    >>> cat(enigma31.wheel_positions_l)
-    'ids'
-    >>> enigma31.peg_positions
-    ([8], [22], [3])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'odxbjwzrmelkisavuhnyqpfctg'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 22)
-    >>> cat(enigma31.wheel_positions_l)
-    'idt'
-    >>> enigma31.peg_positions
-    ([8], [22], [2])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'udgbfeclrwnhxksvtioqapjmzy'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 23)
-    >>> cat(enigma31.wheel_positions_l)
-    'idu'
-    >>> enigma31.peg_positions
-    ([8], [22], [1])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'nrdczqxmowvshaiufblypkjgte'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 10, 24)
-    >>> cat(enigma31.wheel_positions_l)
-    'idv'
-    >>> enigma31.peg_positions
-    ([8], [22], [0])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'hkifjdoacebqtzgulyvmpsxwrn'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 11, 25)
-    >>> cat(enigma31.wheel_positions_l)
-    'iew'
-    >>> enigma31.peg_positions
-    ([8], [21], [25])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'yptzuhofqvnmlkgbixwcejsrad'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 11, 0)
-    >>> cat(enigma31.wheel_positions_l)
-    'iex'
-    >>> enigma31.peg_positions
-    ([8], [21], [24])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'vkdcwhqfjibzsptngumoraeyxl'
-
-    >>> enigma31.advance()
-    >>> enigma31.wheel_positions
-    (3, 11, 1)
-    >>> cat(enigma31.wheel_positions_l)
-    'iey'
-    >>> enigma31.peg_positions
-    ([8], [21], [23])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'wenpbqrouxlkychdfgzvitajms'
-
-
-    >>> enigma31.set_wheels('i', 'z', 'd')
-    >>> enigma31.encipher('verylongtestmessagewithanextrabitofmessageforgoodmeasure')
-    'apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki'
-    >>> enigma31.wheel_positions
-    (4, 9, 10)
-    >>> cat(enigma31.wheel_positions_l)
-    'jch'
-    >>> enigma31.peg_positions
-    ([7], [23], [14])
-    >>> cat(enigma31.lookup(l) for l in string.ascii_lowercase)
-    'mopnigfuesqwadbcktjrhylzvx'
 
-    >>> enigma31.set_wheels('i', 'z', 'd')
-    >>> enigma31.decipher('apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki')
-    'verylongtestmessagewithanextrabitofmessageforgoodmeasure'
     """
     def __init__(self, reflector_spec,
                  left_wheel_spec, left_wheel_pegs,
diff --git a/test_bombe.py b/test_bombe.py
new file mode 100644 (file)
index 0000000..830bb2b
--- /dev/null
@@ -0,0 +1,686 @@
+import unittest
+import collections
+
+from enigma import *
+
+class LetterTransformerTest(unittest.TestCase):
+
+    def test_maps1(self):
+        lt = LetterTransformer([('z', 'a')] + \
+            list(zip(string.ascii_lowercase, string.ascii_lowercase[1:])),
+            raw_transform = True)
+        self.assertEqual(lt.forward_map, 
+            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 
+            15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0])
+        self.assertEqual(lt.backward_map, 
+            [25, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 
+            13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24])
+
+
+    def test_maps2(self):
+        lt = LetterTransformer(cat(collections.OrderedDict.fromkeys('zyxwc' + string.ascii_lowercase)))
+        self.assertEqual(lt.forward_map, 
+            [25, 24, 23, 22, 2, 0, 1, 3, 4, 5, 6, 7, 8, 9, 
+            10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])
+        self.assertEqual(lt.backward_map,
+            [5, 6, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 
+            17, 18, 19, 20, 21, 22, 23, 24, 25, 3, 2, 1, 0])
+
+    def test_transform(self):
+        lt = LetterTransformer(cat(collections.OrderedDict.fromkeys('zyxwc' + string.ascii_lowercase)))
+        self.assertEqual(cat(lt.forward(l) for l in string.ascii_lowercase),
+            'zyxwcabdefghijklmnopqrstuv')
+        self.assertEqual(cat(lt.backward(l) for l in string.ascii_lowercase),
+            'fgehijklmnopqrstuvwxyzdcba')
+
+
+class PlugboardTest(unittest.TestCase):
+    def setUp(self):
+        self.pb = Plugboard('ua pf rq so ni ey bg hl tx zj'.upper())
+
+    def test_maps(self):
+        self.assertEqual(self.pb.forward_map, 
+            [20, 6, 2, 3, 24, 15, 1, 11, 13, 25, 10, 7, 12, 
+            8, 18, 5, 17, 16, 14, 23, 0, 21, 22, 19, 4, 9])
+        self.assertEqual(self.pb.forward_map, self.pb.backward_map)
+
+    def test_transform(self):
+        self.assertEqual(cat(self.pb.forward(l) 
+                for l in string.ascii_lowercase),
+            'ugcdypblnzkhmisfrqoxavwtej')
+        self.assertEqual(cat(self.pb.backward(l) 
+                for l in string.ascii_lowercase),
+            'ugcdypblnzkhmisfrqoxavwtej')
+
+
+class ReflectorTest(unittest.TestCase):
+    def setUp(self):
+        self.ref = Reflector(reflector_b_spec)
+
+    def test_maps(self):
+        self.assertEqual(self.ref.forward_map, 
+            [24, 17, 20, 7, 16, 18, 11, 3, 15, 23, 13, 6, 14, 
+            10, 12, 8, 4, 1, 5, 25, 2, 22, 21, 9, 0, 19])
+        self.assertEqual(self.ref.forward_map, self.ref.backward_map)
+
+    def test_transform(self):
+        self.assertEqual(cat(self.ref.forward(l) 
+                for l in string.ascii_lowercase),
+            'yruhqsldpxngokmiebfzcwvjat')
+        self.assertEqual(cat(self.ref.backward(l) 
+                for l in string.ascii_lowercase),
+            'yruhqsldpxngokmiebfzcwvjat')
+
+
+class SimpleWheelTest(unittest.TestCase):
+    def test_init1(self):
+        rotor_1_transform = list(zip(string.ascii_lowercase, 
+            'EKMFLGDQVZNTOWYHXUSPAIBRCJ'.lower()))
+        wheel_1 = SimpleWheel(rotor_1_transform, raw_transform=True)
+        self.assertEqual(cat(wheel_1.forward(l) 
+                for l in string.ascii_lowercase),
+            'ekmflgdqvzntowyhxuspaibrcj')
+        self.assertEqual(cat(wheel_1.backward(l) 
+                for l in string.ascii_lowercase),
+            'uwygadfpvzbeckmthxslrinqoj')
+
+    def test_init2(self):
+        wheel_2 = SimpleWheel(wheel_ii_spec)
+        self.assertEqual(cat(wheel_2.forward(l) 
+                for l in string.ascii_lowercase),
+            'ajdksiruxblhwtmcqgznpyfvoe')
+        self.assertEqual(cat(wheel_2.backward(l) 
+                for l in string.ascii_lowercase),
+            'ajpczwrlfbdkotyuqgenhxmivs')
+
+    def test_advance(self):
+        wheel_3 = SimpleWheel(wheel_iii_spec)
+        wheel_3.set_position('a')
+        wheel_3.advance()
+        self.assertEqual(cat(wheel_3.forward(l) 
+                for l in string.ascii_lowercase),
+            'cegikboqswuymxdhvfzjltrpna')
+        self.assertEqual(cat(wheel_3.backward(l) 
+                for l in string.ascii_lowercase),
+            'zfaobrcpdteumygxhwivkqjnls')
+        self.assertEqual(wheel_3.position, 1)
+        self.assertEqual(wheel_3.position_l, 'b')
+
+        for _ in range(24): wheel_3.advance()
+
+        self.assertEqual(wheel_3.position, 25)
+        self.assertEqual(wheel_3.position_l, 'z')
+
+        self.assertEqual(cat(wheel_3.forward(l) 
+                for l in string.ascii_lowercase),
+            'pcegikmdqsuywaozfjxhblnvtr')
+        self.assertEqual(cat(wheel_3.backward(l) 
+                for l in string.ascii_lowercase),
+            'nubhcqdterfvgwoaizjykxmslp')
+
+        wheel_3.advance()
+        self.assertEqual(wheel_3.position, 0)
+        self.assertEqual(wheel_3.position_l, 'a')
+
+    
+        self.assertEqual(cat(wheel_3.forward(l) 
+                for l in string.ascii_lowercase),
+            'bdfhjlcprtxvznyeiwgakmusqo')
+        self.assertEqual(cat(wheel_3.backward(l) 
+                for l in string.ascii_lowercase),
+            'tagbpcsdqeufvnzhyixjwlrkom')
+
+
+class SimpleWheelTest(unittest.TestCase):
+    def test_init1(self):
+        wheel = Wheel(wheel_iii_spec, wheel_iii_pegs, position='b', 
+            ring_setting=1)
+        self.assertEqual(wheel.position, 1)
+        self.assertEqual(wheel.peg_positions, [20])
+        self.assertEqual(wheel.position_l, 'b')
+
+        wheel.advance()
+        self.assertEqual(wheel.position, 2)
+        self.assertEqual(wheel.peg_positions, [19])
+        self.assertEqual(wheel.position_l, 'c')
+
+    def test_init2(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        self.assertEqual(wheel.position, 25)
+        self.assertIn(11, wheel.peg_positions)
+        self.assertIn(24, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'b')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'xkqhwpvngzrcfoiaselbtymjdu')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'ptlyrmidoxbswhnfckquzgeavj')
+
+
+    def test_advance(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        wheel.advance()
+
+        self.assertEqual(wheel.position, 0)
+        self.assertIn(10, wheel.peg_positions)
+        self.assertIn(23, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'c')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'jpgvoumfyqbenhzrdkasxlictw')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'skxqlhcnwarvgmebjptyfdzuio')
+
+    def test_advance_23(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(23):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 22)
+        self.assertIn(1, wheel.peg_positions)
+        self.assertIn(14, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'y')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'mgxantkzsyqjcufirldvhoewbp')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'dymswobuplgraevzkqifntxcjh')
+
+    def test_advance_24(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(24):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 23)
+        self.assertIn(0, wheel.peg_positions)
+        self.assertIn(13, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'z')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'fwzmsjyrxpibtehqkcugndvaol')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'xlrvnatokfqzduyjphemswbigc')
+
+    def test_advance_25(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(25):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 24)
+        self.assertIn(25, wheel.peg_positions)
+        self.assertIn(12, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'a')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'vylrixqwohasdgpjbtfmcuznke')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'kqumzsnjepyctxiogdlrvahfbw')
+
+    def test_advance_26(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(26):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 25)
+        self.assertIn(24, wheel.peg_positions)
+        self.assertIn(11, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'b')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'xkqhwpvngzrcfoiaselbtymjdu')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'ptlyrmidoxbswhnfckquzgeavj')
+
+
+    def test_advance_27(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(27):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 0)
+        self.assertIn(23, wheel.peg_positions)
+        self.assertIn(10, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'c')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'jpgvoumfyqbenhzrdkasxlictw')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'skxqlhcnwarvgmebjptyfdzuio')
+
+class EnigmaTest(unittest.TestCase):
+
+    def setUp(self):
+        self.enigma = Enigma(reflector_b_spec, 
+                wheel_i_spec, wheel_i_pegs, 
+                wheel_ii_spec, wheel_ii_pegs, 
+                wheel_iii_spec, wheel_iii_pegs, 
+                1, 1, 1, 
+                '')
+
+        # Setting sheet line 31 from http://www.codesandciphers.org.uk/enigma/enigma3.htm
+        # Enigma simulation settings are 
+        # http://enigma.louisedade.co.uk/enigma.html?m3;b;b153;AFTX;AJEU;AU-BG-EY-FP-HL-IN-JZ-OS-QR-TX
+        self.enigma31 = Enigma(reflector_b_spec, 
+                wheel_i_spec, wheel_i_pegs, 
+                wheel_v_spec, wheel_v_pegs, 
+                wheel_iii_spec, wheel_iii_pegs, 
+                6, 20, 24, 
+                'ua pf rq so ni ey bg hl tx zj')
+
+
+    def test_middle_advance(self):
+        self.enigma.set_wheels('a', 'a', 't')
+        self.assertEqual(self.enigma.wheel_positions, (0, 0, 19))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aat')
+        self.assertEqual(self.enigma.peg_positions, ([16], [4], [2]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase), 
+            'puvioztjdhxmlyeawsrgbcqknf')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 0, 20))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aau')
+        self.assertEqual(self.enigma.peg_positions, ([16], [4], [1]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'baigpldqcowfyzjehvtsxrkumn')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 0, 21))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aav')
+        self.assertEqual(self.enigma.peg_positions, ([16], [4], [0]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'mnvfydiwgzsoablrxpkutchqej')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 1, 22))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'abw')
+        self.assertEqual(self.enigma.peg_positions, ([16], [3], [25]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'ulfopcykswhbzvderqixanjtgm')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 1, 23))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'abx')
+        self.assertEqual(self.enigma.peg_positions, ([16], [3], [24]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'qmwftdyovursbzhxaklejicpgn')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 1, 24))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aby')
+        self.assertEqual(self.enigma.peg_positions, ([16], [3], [23]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'oljmzxrvucybdqasngpwihtfke')
+
+
+    def test_double_advance(self):
+        self.enigma.set_wheels('a', 'd', 't')
+        self.assertEqual(self.enigma.wheel_positions, (0, 3, 19))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'adt')
+        self.assertEqual(self.enigma.peg_positions, ([16], [1], [2]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'zcbpqxwsjiuonmldethrkygfva')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 3, 20))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'adu')
+        self.assertEqual(self.enigma.peg_positions, ([16], [1], [1]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'ehprawjbngotxikcsdqlzyfmvu')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 3, 21))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'adv')
+        self.assertEqual(self.enigma.peg_positions, ([16], [1], [0]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'eqzxarpihmnvjkwgbfuyslodtc')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 4, 22))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aew')
+        self.assertEqual(self.enigma.peg_positions, ([16], [0], [25]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'qedcbtpluzmhkongavwfirsyxj')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (1, 5, 23))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'bfx')
+        self.assertEqual(self.enigma.peg_positions, ([15], [25], [24]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'iwuedhsfazqxytvrkpgncoblmj')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (1, 5, 24))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'bfy')
+        self.assertEqual(self.enigma.peg_positions, ([15], [25], [23]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'baknstqzrmcxjdvygiefwoulph')
+
+
+    def test_simple_encipher(self):
+        self.enigma.set_wheels('a', 'a', 'a')
+        ct = self.enigma.encipher('testmessage')
+        self.assertEqual(ct, 'olpfhnvflyn')
+
+        self.enigma.set_wheels('a', 'd', 't')
+        ct = self.enigma.encipher('testmessage')
+        self.assertEqual(ct, 'lawnjgpwjik')
+
+        self.enigma.set_wheels('b', 'd', 'q')
+        ct = self.enigma.encipher('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
+        self.assertEqual(ct, 
+            'kvmmwrlqlqsqpeugjrcxzwpfyiyybwloewrouvkpoztceuwtfjzqwpbqldttsr')
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'cha')
+
+
+    def test_advance_with_ring_settings(self):
+        self.enigma31.set_wheels('j', 'e', 'u')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 11, 24))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jev')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [21], [0]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'mvqjlyowkdieasgzcunxrbhtfp')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 12, 25))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jfw')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [20], [25]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'sjolzuyvrbwdpxcmtiaqfhknge')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 12, 0))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jfx')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [20], [24]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'qrxedkoywufmlvgsabpzjnicht')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 12, 1))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jfy')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [20], [23]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'hpsukliagqefwvtbjxcodnmrzy')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 12, 2))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jfz')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [20], [22]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'zevnbpyqowrtxdifhkulscjmga')
+
+
+    def test_advance_with_ring_settings_2(self):
+        self.enigma31.set_wheels('i', 'd', 'z')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 3))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ida')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [21]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'ikhpqrvcambzjondefwyxgsutl')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 4))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idb')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [20]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'cdabskhgzwfmlqvunyexpojtri')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 5))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idc')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [19]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'pcbwiqhgemyvjsuaftnroldzkx')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 6))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idd')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [18]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'xcbfvdnouptmlghjzwykierasq')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 7))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ide')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [17]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'xfvglbdynuseriwqpmkzjcoaht')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 8))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idf')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [16]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'tfpqlbouynsewjgcdxkahzmriv')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 9))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idg')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [15]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'cjaunvlwtbygzexrspqidfhokm')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 10))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idh')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [14]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'yltxkrqvowebzpingfucshjdam')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 11))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idi')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [13]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'myktluzrnxceaiqsohpdfwvjbg')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 12))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idj')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [12]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'pynjrmiugdqxfcvakewzhoslbt')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 13))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idk')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [11]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'mwvedyplnoxhaijgrqtszcbkfu')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 14))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idl')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [10]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'qcbrfeutvoxpnmjladzhgiykws')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 15))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idm')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [9]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'dnoahryetsmukbcvwfjilpqzgx')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 16))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idn')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [8]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'nidcfehgbqsovalyjzkxwmutpr')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 17))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ido')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [7]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'joifxdulcarhzpbntkwqgysevm')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 18))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idp')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [6]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'ptnlsxvozmwdjchayuebrgkfqi')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 19))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idq')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [5]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'slwopzqnmxybihdeguavrtcjkf')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 20))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idr')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [4]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'hcbedwlamzogixkytsrqvufnpj')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 21))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ids')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [3]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'odxbjwzrmelkisavuhnyqpfctg')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 22))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idt')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [2]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'udgbfeclrwnhxksvtioqapjmzy')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 23))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idu')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [1]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'nrdczqxmowvshaiufblypkjgte')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 24))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idv')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [0]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'hkifjdoacebqtzgulyvmpsxwrn')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 11, 25))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'iew')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [21], [25]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'yptzuhofqvnmlkgbixwcejsrad')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 11, 0))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'iex')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [21], [24]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'vkdcwhqfjibzsptngumoraeyxl')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 11, 1))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'iey')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [21], [23]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'wenpbqrouxlkychdfgzvitajms')
+
+    def test_double_advance_with_ring_settings_2(self):
+        self.enigma31.set_wheels('a', 'y', 't')
+        self.assertEqual(self.enigma31.wheel_positions, (21, 5, 22))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ayt')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [1], [2]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (21, 5, 23))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ayu')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [1], [1]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (21, 5, 24))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ayv')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [1], [0]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (21, 6, 25))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'azw')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [0], [25]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 0))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bax')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [24]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 1))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bay')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [23]))  
+
+        self.enigma31.set_wheels('a', 'z', 't')
+        self.assertEqual(self.enigma31.wheel_positions, (21, 6, 22))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'azt')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [0], [2]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 23))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bau')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [1]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 24))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bav')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [0]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 8, 25))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bbw')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [24], [25]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 8, 0))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bbx')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [24], [24]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 8, 1))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bby')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [24], [23]))
+
+
+    def test_encipher_with_ring(self):
+
+        self.enigma31.set_wheels('i', 'z', 'd')
+        ct = self.enigma31.encipher('verylongtestmessagewithanextrabitofmessageforgoodmeasure')
+        self.assertEqual(ct, 
+            'apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki')
+        self.assertEqual(self.enigma31.wheel_positions, (4, 9, 10))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jch')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [23], [14]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'mopnigfuesqwadbcktjrhylzvx')
+
+        self.enigma31.set_wheels('i', 'z', 'd')
+        pt = self.enigma31.decipher('apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki')
+        self.assertEqual(pt, 
+            'verylongtestmessagewithanextrabitofmessageforgoodmeasure')
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file
diff --git a/test_enigma.py b/test_enigma.py
new file mode 100644 (file)
index 0000000..830bb2b
--- /dev/null
@@ -0,0 +1,686 @@
+import unittest
+import collections
+
+from enigma import *
+
+class LetterTransformerTest(unittest.TestCase):
+
+    def test_maps1(self):
+        lt = LetterTransformer([('z', 'a')] + \
+            list(zip(string.ascii_lowercase, string.ascii_lowercase[1:])),
+            raw_transform = True)
+        self.assertEqual(lt.forward_map, 
+            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 
+            15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0])
+        self.assertEqual(lt.backward_map, 
+            [25, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 
+            13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24])
+
+
+    def test_maps2(self):
+        lt = LetterTransformer(cat(collections.OrderedDict.fromkeys('zyxwc' + string.ascii_lowercase)))
+        self.assertEqual(lt.forward_map, 
+            [25, 24, 23, 22, 2, 0, 1, 3, 4, 5, 6, 7, 8, 9, 
+            10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])
+        self.assertEqual(lt.backward_map,
+            [5, 6, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 
+            17, 18, 19, 20, 21, 22, 23, 24, 25, 3, 2, 1, 0])
+
+    def test_transform(self):
+        lt = LetterTransformer(cat(collections.OrderedDict.fromkeys('zyxwc' + string.ascii_lowercase)))
+        self.assertEqual(cat(lt.forward(l) for l in string.ascii_lowercase),
+            'zyxwcabdefghijklmnopqrstuv')
+        self.assertEqual(cat(lt.backward(l) for l in string.ascii_lowercase),
+            'fgehijklmnopqrstuvwxyzdcba')
+
+
+class PlugboardTest(unittest.TestCase):
+    def setUp(self):
+        self.pb = Plugboard('ua pf rq so ni ey bg hl tx zj'.upper())
+
+    def test_maps(self):
+        self.assertEqual(self.pb.forward_map, 
+            [20, 6, 2, 3, 24, 15, 1, 11, 13, 25, 10, 7, 12, 
+            8, 18, 5, 17, 16, 14, 23, 0, 21, 22, 19, 4, 9])
+        self.assertEqual(self.pb.forward_map, self.pb.backward_map)
+
+    def test_transform(self):
+        self.assertEqual(cat(self.pb.forward(l) 
+                for l in string.ascii_lowercase),
+            'ugcdypblnzkhmisfrqoxavwtej')
+        self.assertEqual(cat(self.pb.backward(l) 
+                for l in string.ascii_lowercase),
+            'ugcdypblnzkhmisfrqoxavwtej')
+
+
+class ReflectorTest(unittest.TestCase):
+    def setUp(self):
+        self.ref = Reflector(reflector_b_spec)
+
+    def test_maps(self):
+        self.assertEqual(self.ref.forward_map, 
+            [24, 17, 20, 7, 16, 18, 11, 3, 15, 23, 13, 6, 14, 
+            10, 12, 8, 4, 1, 5, 25, 2, 22, 21, 9, 0, 19])
+        self.assertEqual(self.ref.forward_map, self.ref.backward_map)
+
+    def test_transform(self):
+        self.assertEqual(cat(self.ref.forward(l) 
+                for l in string.ascii_lowercase),
+            'yruhqsldpxngokmiebfzcwvjat')
+        self.assertEqual(cat(self.ref.backward(l) 
+                for l in string.ascii_lowercase),
+            'yruhqsldpxngokmiebfzcwvjat')
+
+
+class SimpleWheelTest(unittest.TestCase):
+    def test_init1(self):
+        rotor_1_transform = list(zip(string.ascii_lowercase, 
+            'EKMFLGDQVZNTOWYHXUSPAIBRCJ'.lower()))
+        wheel_1 = SimpleWheel(rotor_1_transform, raw_transform=True)
+        self.assertEqual(cat(wheel_1.forward(l) 
+                for l in string.ascii_lowercase),
+            'ekmflgdqvzntowyhxuspaibrcj')
+        self.assertEqual(cat(wheel_1.backward(l) 
+                for l in string.ascii_lowercase),
+            'uwygadfpvzbeckmthxslrinqoj')
+
+    def test_init2(self):
+        wheel_2 = SimpleWheel(wheel_ii_spec)
+        self.assertEqual(cat(wheel_2.forward(l) 
+                for l in string.ascii_lowercase),
+            'ajdksiruxblhwtmcqgznpyfvoe')
+        self.assertEqual(cat(wheel_2.backward(l) 
+                for l in string.ascii_lowercase),
+            'ajpczwrlfbdkotyuqgenhxmivs')
+
+    def test_advance(self):
+        wheel_3 = SimpleWheel(wheel_iii_spec)
+        wheel_3.set_position('a')
+        wheel_3.advance()
+        self.assertEqual(cat(wheel_3.forward(l) 
+                for l in string.ascii_lowercase),
+            'cegikboqswuymxdhvfzjltrpna')
+        self.assertEqual(cat(wheel_3.backward(l) 
+                for l in string.ascii_lowercase),
+            'zfaobrcpdteumygxhwivkqjnls')
+        self.assertEqual(wheel_3.position, 1)
+        self.assertEqual(wheel_3.position_l, 'b')
+
+        for _ in range(24): wheel_3.advance()
+
+        self.assertEqual(wheel_3.position, 25)
+        self.assertEqual(wheel_3.position_l, 'z')
+
+        self.assertEqual(cat(wheel_3.forward(l) 
+                for l in string.ascii_lowercase),
+            'pcegikmdqsuywaozfjxhblnvtr')
+        self.assertEqual(cat(wheel_3.backward(l) 
+                for l in string.ascii_lowercase),
+            'nubhcqdterfvgwoaizjykxmslp')
+
+        wheel_3.advance()
+        self.assertEqual(wheel_3.position, 0)
+        self.assertEqual(wheel_3.position_l, 'a')
+
+    
+        self.assertEqual(cat(wheel_3.forward(l) 
+                for l in string.ascii_lowercase),
+            'bdfhjlcprtxvznyeiwgakmusqo')
+        self.assertEqual(cat(wheel_3.backward(l) 
+                for l in string.ascii_lowercase),
+            'tagbpcsdqeufvnzhyixjwlrkom')
+
+
+class SimpleWheelTest(unittest.TestCase):
+    def test_init1(self):
+        wheel = Wheel(wheel_iii_spec, wheel_iii_pegs, position='b', 
+            ring_setting=1)
+        self.assertEqual(wheel.position, 1)
+        self.assertEqual(wheel.peg_positions, [20])
+        self.assertEqual(wheel.position_l, 'b')
+
+        wheel.advance()
+        self.assertEqual(wheel.position, 2)
+        self.assertEqual(wheel.peg_positions, [19])
+        self.assertEqual(wheel.position_l, 'c')
+
+    def test_init2(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        self.assertEqual(wheel.position, 25)
+        self.assertIn(11, wheel.peg_positions)
+        self.assertIn(24, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'b')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'xkqhwpvngzrcfoiaselbtymjdu')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'ptlyrmidoxbswhnfckquzgeavj')
+
+
+    def test_advance(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        wheel.advance()
+
+        self.assertEqual(wheel.position, 0)
+        self.assertIn(10, wheel.peg_positions)
+        self.assertIn(23, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'c')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'jpgvoumfyqbenhzrdkasxlictw')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'skxqlhcnwarvgmebjptyfdzuio')
+
+    def test_advance_23(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(23):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 22)
+        self.assertIn(1, wheel.peg_positions)
+        self.assertIn(14, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'y')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'mgxantkzsyqjcufirldvhoewbp')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'dymswobuplgraevzkqifntxcjh')
+
+    def test_advance_24(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(24):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 23)
+        self.assertIn(0, wheel.peg_positions)
+        self.assertIn(13, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'z')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'fwzmsjyrxpibtehqkcugndvaol')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'xlrvnatokfqzduyjphemswbigc')
+
+    def test_advance_25(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(25):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 24)
+        self.assertIn(25, wheel.peg_positions)
+        self.assertIn(12, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'a')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'vylrixqwohasdgpjbtfmcuznke')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'kqumzsnjepyctxiogdlrvahfbw')
+
+    def test_advance_26(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(26):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 25)
+        self.assertIn(24, wheel.peg_positions)
+        self.assertIn(11, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'b')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'xkqhwpvngzrcfoiaselbtymjdu')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'ptlyrmidoxbswhnfckquzgeavj')
+
+
+    def test_advance_27(self):
+        wheel = Wheel(wheel_vi_spec, wheel_vi_pegs, position='b', 
+            ring_setting=3)
+        for _ in range(27):
+            wheel.advance()
+
+        self.assertEqual(wheel.position, 0)
+        self.assertIn(23, wheel.peg_positions)
+        self.assertIn(10, wheel.peg_positions)
+        self.assertEqual(wheel.position_l, 'c')
+        self.assertEqual(cat(wheel.forward(l) 
+                for l in string.ascii_lowercase),
+            'jpgvoumfyqbenhzrdkasxlictw')
+        self.assertEqual(cat(wheel.backward(l) 
+                for l in string.ascii_lowercase),
+            'skxqlhcnwarvgmebjptyfdzuio')
+
+class EnigmaTest(unittest.TestCase):
+
+    def setUp(self):
+        self.enigma = Enigma(reflector_b_spec, 
+                wheel_i_spec, wheel_i_pegs, 
+                wheel_ii_spec, wheel_ii_pegs, 
+                wheel_iii_spec, wheel_iii_pegs, 
+                1, 1, 1, 
+                '')
+
+        # Setting sheet line 31 from http://www.codesandciphers.org.uk/enigma/enigma3.htm
+        # Enigma simulation settings are 
+        # http://enigma.louisedade.co.uk/enigma.html?m3;b;b153;AFTX;AJEU;AU-BG-EY-FP-HL-IN-JZ-OS-QR-TX
+        self.enigma31 = Enigma(reflector_b_spec, 
+                wheel_i_spec, wheel_i_pegs, 
+                wheel_v_spec, wheel_v_pegs, 
+                wheel_iii_spec, wheel_iii_pegs, 
+                6, 20, 24, 
+                'ua pf rq so ni ey bg hl tx zj')
+
+
+    def test_middle_advance(self):
+        self.enigma.set_wheels('a', 'a', 't')
+        self.assertEqual(self.enigma.wheel_positions, (0, 0, 19))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aat')
+        self.assertEqual(self.enigma.peg_positions, ([16], [4], [2]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase), 
+            'puvioztjdhxmlyeawsrgbcqknf')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 0, 20))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aau')
+        self.assertEqual(self.enigma.peg_positions, ([16], [4], [1]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'baigpldqcowfyzjehvtsxrkumn')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 0, 21))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aav')
+        self.assertEqual(self.enigma.peg_positions, ([16], [4], [0]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'mnvfydiwgzsoablrxpkutchqej')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 1, 22))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'abw')
+        self.assertEqual(self.enigma.peg_positions, ([16], [3], [25]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'ulfopcykswhbzvderqixanjtgm')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 1, 23))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'abx')
+        self.assertEqual(self.enigma.peg_positions, ([16], [3], [24]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'qmwftdyovursbzhxaklejicpgn')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 1, 24))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aby')
+        self.assertEqual(self.enigma.peg_positions, ([16], [3], [23]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'oljmzxrvucybdqasngpwihtfke')
+
+
+    def test_double_advance(self):
+        self.enigma.set_wheels('a', 'd', 't')
+        self.assertEqual(self.enigma.wheel_positions, (0, 3, 19))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'adt')
+        self.assertEqual(self.enigma.peg_positions, ([16], [1], [2]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'zcbpqxwsjiuonmldethrkygfva')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 3, 20))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'adu')
+        self.assertEqual(self.enigma.peg_positions, ([16], [1], [1]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'ehprawjbngotxikcsdqlzyfmvu')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 3, 21))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'adv')
+        self.assertEqual(self.enigma.peg_positions, ([16], [1], [0]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'eqzxarpihmnvjkwgbfuyslodtc')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (0, 4, 22))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'aew')
+        self.assertEqual(self.enigma.peg_positions, ([16], [0], [25]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'qedcbtpluzmhkongavwfirsyxj')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (1, 5, 23))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'bfx')
+        self.assertEqual(self.enigma.peg_positions, ([15], [25], [24]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'iwuedhsfazqxytvrkpgncoblmj')
+
+        self.enigma.advance()
+        self.assertEqual(self.enigma.wheel_positions, (1, 5, 24))
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'bfy')
+        self.assertEqual(self.enigma.peg_positions, ([15], [25], [23]))
+        self.assertEqual(cat(self.enigma.lookup(l) for l in string.ascii_lowercase),
+            'baknstqzrmcxjdvygiefwoulph')
+
+
+    def test_simple_encipher(self):
+        self.enigma.set_wheels('a', 'a', 'a')
+        ct = self.enigma.encipher('testmessage')
+        self.assertEqual(ct, 'olpfhnvflyn')
+
+        self.enigma.set_wheels('a', 'd', 't')
+        ct = self.enigma.encipher('testmessage')
+        self.assertEqual(ct, 'lawnjgpwjik')
+
+        self.enigma.set_wheels('b', 'd', 'q')
+        ct = self.enigma.encipher('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
+        self.assertEqual(ct, 
+            'kvmmwrlqlqsqpeugjrcxzwpfyiyybwloewrouvkpoztceuwtfjzqwpbqldttsr')
+        self.assertEqual(cat(self.enigma.wheel_positions_l), 'cha')
+
+
+    def test_advance_with_ring_settings(self):
+        self.enigma31.set_wheels('j', 'e', 'u')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 11, 24))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jev')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [21], [0]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'mvqjlyowkdieasgzcunxrbhtfp')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 12, 25))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jfw')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [20], [25]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'sjolzuyvrbwdpxcmtiaqfhknge')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 12, 0))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jfx')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [20], [24]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'qrxedkoywufmlvgsabpzjnicht')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 12, 1))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jfy')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [20], [23]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'hpsukliagqefwvtbjxcodnmrzy')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (4, 12, 2))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jfz')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [20], [22]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'zevnbpyqowrtxdifhkulscjmga')
+
+
+    def test_advance_with_ring_settings_2(self):
+        self.enigma31.set_wheels('i', 'd', 'z')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 3))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ida')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [21]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'ikhpqrvcambzjondefwyxgsutl')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 4))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idb')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [20]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'cdabskhgzwfmlqvunyexpojtri')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 5))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idc')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [19]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'pcbwiqhgemyvjsuaftnroldzkx')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 6))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idd')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [18]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'xcbfvdnouptmlghjzwykierasq')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 7))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ide')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [17]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'xfvglbdynuseriwqpmkzjcoaht')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 8))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idf')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [16]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'tfpqlbouynsewjgcdxkahzmriv')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 9))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idg')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [15]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'cjaunvlwtbygzexrspqidfhokm')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 10))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idh')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [14]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'yltxkrqvowebzpingfucshjdam')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 11))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idi')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [13]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'myktluzrnxceaiqsohpdfwvjbg')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 12))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idj')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [12]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'pynjrmiugdqxfcvakewzhoslbt')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 13))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idk')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [11]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'mwvedyplnoxhaijgrqtszcbkfu')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 14))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idl')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [10]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'qcbrfeutvoxpnmjladzhgiykws')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 15))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idm')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [9]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'dnoahryetsmukbcvwfjilpqzgx')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 16))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idn')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [8]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'nidcfehgbqsovalyjzkxwmutpr')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 17))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ido')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [7]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'joifxdulcarhzpbntkwqgysevm')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 18))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idp')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [6]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'ptnlsxvozmwdjchayuebrgkfqi')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 19))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idq')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [5]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'slwopzqnmxybihdeguavrtcjkf')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 20))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idr')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [4]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'hcbedwlamzogixkytsrqvufnpj')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 21))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ids')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [3]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'odxbjwzrmelkisavuhnyqpfctg')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 22))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idt')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [2]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'udgbfeclrwnhxksvtioqapjmzy')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 23))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idu')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [1]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'nrdczqxmowvshaiufblypkjgte')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 10, 24))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'idv')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [22], [0]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'hkifjdoacebqtzgulyvmpsxwrn')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 11, 25))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'iew')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [21], [25]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'yptzuhofqvnmlkgbixwcejsrad')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 11, 0))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'iex')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [21], [24]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'vkdcwhqfjibzsptngumoraeyxl')
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (3, 11, 1))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'iey')
+        self.assertEqual(self.enigma31.peg_positions, ([8], [21], [23]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'wenpbqrouxlkychdfgzvitajms')
+
+    def test_double_advance_with_ring_settings_2(self):
+        self.enigma31.set_wheels('a', 'y', 't')
+        self.assertEqual(self.enigma31.wheel_positions, (21, 5, 22))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ayt')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [1], [2]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (21, 5, 23))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ayu')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [1], [1]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (21, 5, 24))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'ayv')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [1], [0]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (21, 6, 25))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'azw')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [0], [25]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 0))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bax')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [24]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 1))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bay')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [23]))  
+
+        self.enigma31.set_wheels('a', 'z', 't')
+        self.assertEqual(self.enigma31.wheel_positions, (21, 6, 22))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'azt')
+        self.assertEqual(self.enigma31.peg_positions, ([16], [0], [2]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 23))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bau')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [1]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 7, 24))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bav')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [25], [0]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 8, 25))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bbw')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [24], [25]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 8, 0))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bbx')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [24], [24]))
+
+        self.enigma31.advance()
+        self.assertEqual(self.enigma31.wheel_positions, (22, 8, 1))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'bby')
+        self.assertEqual(self.enigma31.peg_positions, ([15], [24], [23]))
+
+
+    def test_encipher_with_ring(self):
+
+        self.enigma31.set_wheels('i', 'z', 'd')
+        ct = self.enigma31.encipher('verylongtestmessagewithanextrabitofmessageforgoodmeasure')
+        self.assertEqual(ct, 
+            'apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki')
+        self.assertEqual(self.enigma31.wheel_positions, (4, 9, 10))
+        self.assertEqual(cat(self.enigma31.wheel_positions_l), 'jch')
+        self.assertEqual(self.enigma31.peg_positions, ([7], [23], [14]))
+        self.assertEqual(cat(self.enigma31.lookup(l) for l in string.ascii_lowercase),
+            'mopnigfuesqwadbcktjrhylzvx')
+
+        self.enigma31.set_wheels('i', 'z', 'd')
+        pt = self.enigma31.decipher('apocwtjuikurcfivlozvhffkoacxufcekthcvodfqpxdjqyckdozlqki')
+        self.assertEqual(pt, 
+            'verylongtestmessagewithanextrabitofmessageforgoodmeasure')
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file