Mercurial > public > enigma
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))