changeset 12:42858648f8b5

Created Plugboard class and tests.
author Brian Neal <bgneal@gmail.com>
date Sun, 27 May 2012 13:40:58 -0500
parents 845896830342
children 3fbdc7005075
files enigma/plugboard.py enigma/tests/test_plugboard.py
diffstat 2 files changed, 167 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/enigma/plugboard.py	Sun May 27 13:40:58 2012 -0500
@@ -0,0 +1,100 @@
+# Copyright (C) 2012 by Brian Neal.
+# This file is part of Py-Enigma, the Enigma Machine simulation.
+# Py-Enigma is released under the MIT License (see License.txt).
+
+"""Contains the Plugboard class for simulating the plugboard component."""
+
+import collections
+import string
+
+
+# Like the keyboard, the plugboard has plugs for each upper case letter of the
+# alphabet:
+PLUGBOARD_LABELS = string.ascii_uppercase
+
+# The number of plugboard cables supplied with a machine:
+MAX_PAIRS = 10
+
+
+class PlugboardError(Exception):
+    pass
+
+
+class Plugboard:
+    """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.
+
+    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.
+
+    """
+
+    def __init__(self, settings=''):
+        """Configure the plugboard according to a settings string:
+
+        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' 
+
+        To specify no plugboard connections, settings can be None or an empty
+        string.
+
+        A PlugboardError will be raised if the settings string is invalid, or if
+        it contains more than MAX_PAIRS pairs. Each plug should be present at
+        most once in the settings string.
+
+        """
+        # construct wiring mapping table with default 1-1 mappings
+        self.wiring_map = list(range(len(PLUGBOARD_LABELS)))
+
+        # use settings if provided
+        self.settings = []
+        pairs = settings.split() if settings is not None else []
+
+        if len(pairs) > MAX_PAIRS:
+            raise PlugboardError('too many connections')
+        elif len(pairs) == 0:
+            return      # we are done, no mappings to perform
+
+        # convert to upper case
+        pairs = [pair.upper() for pair in pairs]
+
+        # 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)
+
+        # 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
+
+        self.settings = ' '.join(pairs)
+
+    def signal(self, n):
+        """Simulate a signal entering the plugboard on wire n, where n must be
+        an integer between 0 and 25.
+
+        Returns the wire number of the output signal (0-25).
+
+        Note that since the plugboard always crosses pairs of wires, it doesn't
+        matter what direction (keyboard -> entry wheel or vice versa) the signal
+        is coming from.
+
+        """
+        return self.wiring_map[n]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/enigma/tests/test_plugboard.py	Sun May 27 13:40:58 2012 -0500
@@ -0,0 +1,67 @@
+# Copyright (C) 2012 by Brian Neal.
+# This file is part of Py-Enigma, the Enigma Machine simulation.
+# Py-Enigma is released under the MIT License (see License.txt).
+
+"""Tests for the Plugboard class."""
+
+import unittest
+
+from ..plugboard import Plugboard, PlugboardError
+
+
+class PlugboardTestCase(unittest.TestCase):
+
+    def test_bad_settings(self):
+
+        # too many
+        self.assertRaises(PlugboardError, Plugboard,
+                settings='AB CD EF GH IJ KL MN OP QR ST UV')
+
+        # duplicate
+        self.assertRaises(PlugboardError, Plugboard,
+                settings='AB CD EF GH IJ KL MN OF QR ST')
+
+        self.assertRaises(PlugboardError, Plugboard,
+                settings='AB CD EF GH IJ KL MN FP QR ST')
+
+        # invalid
+        self.assertRaises(PlugboardError, Plugboard,
+                settings='A2 CD EF GH IJ KL MN FP QR ST')
+        self.assertRaises(PlugboardError, Plugboard,
+                settings='AB CD EF *H IJ KL MN FP QR ST')
+        self.assertRaises(PlugboardError, Plugboard,
+                settings='ABCD EF GH IJKLMN OP')
+        self.assertRaises(PlugboardError, Plugboard, settings='A-D EF GH OP')
+        self.assertRaises(PlugboardError, Plugboard, settings='A')
+
+    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 ')
+
+    def test_default_wiring(self):
+
+        p = Plugboard()
+        for n in range(26):
+            self.assertEqual(n, p.signal(n))
+      
+    def test_wiring(self):
+
+        p = Plugboard(settings='AB CD EF GH IJ KL MN OP QR ST')
+        for n in range(26):
+
+            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(n, p.signal(n))