changeset 15:7ea7da689dbd

Made Plugboards easier to construct. Can do it from a list of integer pairs, or from key sheet strings (both Wehrmacht and Kriegsmarine syntaxes are supported). Polished the EnigmaMachine.from_key_sheet function as well. Updated tests.
author Brian Neal <bgneal@gmail.com>
date Sun, 27 May 2012 18:51:40 -0500
parents 0fe1c4a11bad
children 2ce2e8c5a5be
files enigma/machine.py enigma/plugboard.py enigma/tests/test_plugboard.py
diffstat 3 files changed, 162 insertions(+), 76 deletions(-) [+]
line wrap: on
line diff
--- a/enigma/machine.py	Sun May 27 15:05:00 2012 -0500
+++ b/enigma/machine.py	Sun May 27 18:51:40 2012 -0500
@@ -51,37 +51,51 @@
         self.plugboard = plugboard
 
     @classmethod
-    def from_key_sheet(cls, rotors, ring_settings=(0, 0, 0), reflector='B',
-            plugboard_settings=''):
+    def from_key_sheet(cls, rotors='I II III', ring_settings=None,
+            reflector='B', plugboard_settings=None):
+
         """Convenience function to build an EnigmaMachine from the data as you
         might find it on a key sheet:
 
-        rotors: a list of strings naming the rotors from left to right;
-            e.g. ["I", "III", "IV"]
+        rotors: either a list of strings naming the rotors from left to right
+        or a single string:
+            e.g. ["I", "III", "IV"] or "I III IV"
 
-        ring_settings: an iterable of integers representing the ring settings to
-        be applied to the rotors in the rotors list
+        ring_settings: an iterable of integers or None representing the ring
+        settings to be applied to the rotors in the rotors list. None means all
+        ring settings are 0.
 
         reflector: a string that names the reflector to use
 
         plugboard: a string of plugboard settings as you might find on a key
-        sheet; e.g. 'PO ML IU KJ NH YT GB VF RE DC' 
+        sheet; e.g. 
+            'PO ML IU KJ NH YT GB VF RE DC' 
+        or
+            '18/26 17/4 21/6 3/16 19/14 22/7 8/1 12/25 5/9 10/15'
+
+            A value of None means no plugboard connections are made.
 
         """
         # validate inputs
+        if isinstance(rotors, str):
+            rotors = rotors.split()
+
         num_rotors = len(rotors)
         if num_rotors not in (3, 4):
             raise EnigmaError("invalid rotors list size")
 
+        if ring_settings is None:
+            ring_settings = [0] * num_rotors
+
         if num_rotors != len(ring_settings):
-            raise EnigmaError("please provide %d ring settings" % num_rotors)
+            raise EnigmaError("# of rotors doesn't match # of ring settings")
 
         # assemble the machine
         rotor_list = [create_rotor(r[0], r[1]) for r in zip(rotors, ring_settings)]
 
         return cls(rotor_list, 
                    create_reflector(reflector),
-                   Plugboard(plugboard_settings))
+                   Plugboard.from_key_sheet(plugboard_settings))
 
 
     def set_display(self, val):
--- a/enigma/plugboard.py	Sun May 27 15:05:00 2012 -0500
+++ b/enigma/plugboard.py	Sun May 27 18:51:40 2012 -0500
@@ -5,12 +5,12 @@
 """Contains the Plugboard class for simulating the plugboard component."""
 
 import collections
+from itertools import chain
 import string
 
 
-# Like the keyboard, the plugboard has plugs for each upper case letter of the
-# alphabet:
-PLUGBOARD_LABELS = string.ascii_uppercase
+# On Wehrmacht models, the plugs are labeled with upper case letters
+WEHRMACHT_LABELS = string.ascii_uppercase
 
 # The number of plugboard cables supplied with a machine:
 MAX_PAIRS = 10
@@ -21,25 +21,73 @@
 
 
 class Plugboard:
-    """The plugboard allows the operator to swap letters before and after the
+    """The plugboard allows the operator to swap letters before and after the 
     entry wheel. This is accomplished by connecting cables between pairs of
-    plugs that are marked with letters. Ten cables were issued with each
-    machine; thus up to 10 of these swappings could be used as part of a machine
-    setup.
+    plugs that are marked with letters (Wehrmacht) or numbers (Kriegsmarine).
+    Ten cables were issued with each machine; thus up to 10 of these swappings
+    could be used as part of a machine setup.
 
     Each cable swaps both the input and output signals. Thus if A is connected
     to B, A crosses to B in the keyboard to entry wheel direction and also in
-    the entry wheel to lamp direction.
+    the reverse entry wheel to lamp direction.
 
     """
+    def __init__(self, wiring_pairs=None):
+        """Configure the plugboard according to a list or tuple of integer
+        pairs, or None.
 
-    def __init__(self, settings=''):
-        """Configure the plugboard according to a settings string:
+        A value of None or an empty list/tuple indicates no plugboard
+        connections are to be used (a straight mapping).
 
-        settings - a string consisting of pairs of letters separated by
-        whitespace. This is the format used in the key sheets (code books) to
-        specify daily settings for the Enigma Machine.
-        E.g. 'PO ML IU KJ NH YT GB VF RE DC' 
+        Otherwise wiring_pairs must be an iterable of integer pairs, where each
+        integer is between 0-25, inclusive. At most 10 such pairs can be
+        specified. Each value represents an input/output path through the
+        plugboard. It is invalid to specify the same path more than once in the
+        list.
+
+        If an invalid wiring_pairs parameter is given, a PlugboardError is
+        raised.
+
+        """
+        # construct wiring mapping table with default 1-1 mappings
+        self.wiring_map = list(range(26))
+
+        # use settings if provided
+        if not wiring_pairs:
+            return
+
+        if len(wiring_pairs) > MAX_PAIRS:
+            raise PlugboardError('Please specify %d or less pairs' % MAX_PAIRS)
+
+        # ensure a path occurs at most once in the list
+        counter = collections.Counter(chain.from_iterable(wiring_pairs))
+        path, count = counter.most_common(1)[0]
+        if count != 1:
+            raise PlugboardError('duplicate connection: %d' % path)
+
+        # make the connections
+        for pair in wiring_pairs:
+            m = pair[0]
+            n = pair[1]
+            if not (0 <= m < 26) or not (0 <= n < 26):
+                raise PlugboardError('invalid connection: %s' % str(pair))
+
+            self.wiring_map[m] = n
+            self.wiring_map[n] = m
+
+    @classmethod
+    def from_key_sheet(cls, settings=None):
+        """Configure the plugboard according to a settings string as you may
+        find on a key sheet.
+
+        Two syntaxes are supported, the Wehrmacht and Kriegsmarine styles:
+
+        In the Wehrmacht syntax, the settings are given as a string of
+        alphabetic pairs. For example: 'PO ML IU KJ NH YT GB VF RE DC' 
+
+        In the Kriegsmarine syntax, the settings are given as a string of number
+        pairs, separated by a '/'. Note that the numbering uses 1-26, inclusive.
+        For example: '18/26 17/4 21/6 3/16 19/14 22/7 8/1 12/25 5/9 10/15'
 
         To specify no plugboard connections, settings can be None or an empty
         string.
@@ -49,42 +97,39 @@
         most once in the settings string.
 
         """
-        # construct wiring mapping table with default 1-1 mappings
-        self.wiring_map = list(range(len(PLUGBOARD_LABELS)))
+        if not settings:
+            return cls(None)
 
-        # use settings if provided
-        self.settings = []
-        pairs = settings.split() if settings is not None else []
+        wiring_pairs = []
+        
+        # detect which syntax is being used
+        if settings.find('/') != -1:
+            # Kriegsmarine syntax
+            pairs = settings.split()
+            for p in pairs:
+                try:
+                    m, n = p.split('/')
+                    m, n = int(m), int(n)
+                except ValueError:
+                    raise PlugboardError('invalid pair: %s' % p)
 
-        if len(pairs) > MAX_PAIRS:
-            raise PlugboardError('too many connections')
-        elif len(pairs) == 0:
-            return      # we are done, no mappings to perform
+                wiring_pairs.append((m - 1, n - 1))
+        else:
+            # Wehrmacht syntax
+            pairs = settings.upper().split()
 
-        # convert to upper case
-        pairs = [pair.upper() for pair in pairs]
+            for p in pairs:
+                if len(p) != 2:
+                    raise PlugboardError('invalid pair: %s' % p)
 
-        # validate pairings
-        for pair in pairs:
-            if len(pair) != 2:
-                raise PlugboardError('invalid pair length: %s' % pair)
-            for c in pair:
-                if c not in PLUGBOARD_LABELS:
-                    raise PlugboardError('invalid letter: %s' % c)
-        
-        # validate each letter appears at most once
-        counter = collections.Counter(''.join(pairs))
-        letter, count = counter.most_common(1)[0]
-        if count != 1:
-            raise PlugboardError('duplicate connection: %s' % letter)
+                m = p[0]
+                n = p[1]
+                if m not in WEHRMACHT_LABELS or n not in WEHRMACHT_LABELS:
+                    raise PlugboardError('invalid pair: %s' % p)
 
-        # settings seems valid, make the internal wiring changes now:
-        for pair in pairs:
-            m, n = ord(pair[0]) - ord('A'), ord(pair[1]) - ord('A')
-            self.wiring_map[m] = n
-            self.wiring_map[n] = m
+                wiring_pairs.append((ord(m) - ord('A'), ord(n) - ord('A')))
 
-        self.settings = ' '.join(pairs)
+        return cls(wiring_pairs)
 
     def signal(self, n):
         """Simulate a signal entering the plugboard on wire n, where n must be
--- a/enigma/tests/test_plugboard.py	Sun May 27 15:05:00 2012 -0500
+++ b/enigma/tests/test_plugboard.py	Sun May 27 18:51:40 2012 -0500
@@ -14,38 +14,62 @@
     def test_bad_settings(self):
 
         # too many
-        self.assertRaises(PlugboardError, Plugboard,
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
                 settings='AB CD EF GH IJ KL MN OP QR ST UV')
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
+                '18/26 17/4 21/6 3/16 19/14 22/7 8/1 12/25 5/9 10/15 2/20')
 
         # duplicate
-        self.assertRaises(PlugboardError, Plugboard,
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
                 settings='AB CD EF GH IJ KL MN OF QR ST')
 
-        self.assertRaises(PlugboardError, Plugboard,
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
                 settings='AB CD EF GH IJ KL MN FP QR ST')
 
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
+                '18/26 17/4 21/6 3/16 19/14 22/3 8/1 12/25')
+
         # invalid
-        self.assertRaises(PlugboardError, Plugboard,
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
                 settings='A2 CD EF GH IJ KL MN FP QR ST')
-        self.assertRaises(PlugboardError, Plugboard,
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
                 settings='AB CD EF *H IJ KL MN FP QR ST')
-        self.assertRaises(PlugboardError, Plugboard,
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
                 settings='ABCD EF GH IJKLMN OP')
-        self.assertRaises(PlugboardError, Plugboard, settings='A-D EF GH OP')
-        self.assertRaises(PlugboardError, Plugboard, settings='A')
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
+                settings='A-D EF GH OP')
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
+                settings='A')
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
+                settings='9')
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
+                '1*/26 17/4 21/6 3/16 19/14 22/3 8/1 12/25')
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
+                '1*/26 17/4 2A/6 3/16 19/14 22/3 8/1 12/25')
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
+                '100/2')
+        self.assertRaises(PlugboardError, Plugboard.from_key_sheet,
+                settings='T/C')
 
     def test_valid_settings(self):
 
         # these should be valid settings and should not raise
         p = Plugboard()
         p = Plugboard(None)
-        p = Plugboard(settings=None)
-        p = Plugboard(settings='')
-        p = Plugboard('')
-        p = Plugboard(settings='AB CD EF GH IJ KL MN OP QR ST')
-        p = Plugboard(settings='CD EF GH IJ KL MN OP QR ST')
-        p = Plugboard(settings='EF GH IJ KL MN OP QR ST')
-        p = Plugboard(settings=' GH ')
+        p = Plugboard(wiring_pairs=None)
+        p = Plugboard(wiring_pairs=[])
+        p = Plugboard([])
+        p = Plugboard.from_key_sheet('AB CD EF GH IJ KL MN OP QR ST')
+        p = Plugboard.from_key_sheet('CD EF GH IJ KL MN OP QR ST')
+        p = Plugboard.from_key_sheet('EF GH IJ KL MN OP QR ST')
+        p = Plugboard.from_key_sheet(' GH ')
+        p = Plugboard.from_key_sheet('18/26 17/4 21/6 3/16 19/14 22/7 8/1 12/25'
+                ' 5/9 10/15')
+        p = Plugboard.from_key_sheet('18/26 17/4')
+        p = Plugboard.from_key_sheet(' 18/26 ')
+        p = Plugboard.from_key_sheet()
+        p = Plugboard.from_key_sheet('')
+        p = Plugboard.from_key_sheet(None)
 
     def test_default_wiring(self):
 
@@ -54,14 +78,17 @@
             self.assertEqual(n, p.signal(n))
       
     def test_wiring(self):
+        settings =['AB CD EF GH IJ KL MN OP QR ST',
+                   '1/2 3/4 5/6 7/8 9/10 11/12 13/14 15/16 17/18 19/20']
 
-        p = Plugboard(settings='AB CD EF GH IJ KL MN OP QR ST')
-        for n in range(26):
+        for setting in settings:
+            p = Plugboard.from_key_sheet(setting)
+            for n in range(26):
 
-            if n < 20:
-                if n % 2 == 0:
-                    self.assertEqual(p.signal(n), n + 1)
+                if n < 20:
+                    if n % 2 == 0:
+                        self.assertEqual(p.signal(n), n + 1)
+                    else:
+                        self.assertEqual(p.signal(n), n - 1)
                 else:
-                    self.assertEqual(p.signal(n), n - 1)
-            else:
-                self.assertEqual(n, p.signal(n))
+                    self.assertEqual(n, p.signal(n))