annotate enigma/plugboard.py @ 28:067205259796

Correct name of command-line argument for key file.
author Brian Neal <bgneal@gmail.com>
date Tue, 29 May 2012 17:21:16 -0500
parents abf2132ad338
children 160e1bd59965
rev   line source
bgneal@12 1 # Copyright (C) 2012 by Brian Neal.
bgneal@12 2 # This file is part of Py-Enigma, the Enigma Machine simulation.
bgneal@12 3 # Py-Enigma is released under the MIT License (see License.txt).
bgneal@12 4
bgneal@16 5 """Contains the Plugboard class for simulating the plugboard (Steckerbrett)
bgneal@16 6 component of the Enigma Machine.
bgneal@16 7
bgneal@16 8 """
bgneal@12 9
bgneal@12 10 import collections
bgneal@15 11 from itertools import chain
bgneal@12 12 import string
bgneal@12 13
bgneal@12 14
bgneal@16 15 # On Heer & Luftwaffe (?) models, the plugs are labeled with upper case letters
bgneal@20 16 HEER_LABELS = string.ascii_uppercase
bgneal@12 17
bgneal@12 18 # The number of plugboard cables supplied with a machine:
bgneal@12 19 MAX_PAIRS = 10
bgneal@12 20
bgneal@12 21
bgneal@12 22 class PlugboardError(Exception):
bgneal@12 23 pass
bgneal@12 24
bgneal@12 25
bgneal@12 26 class Plugboard:
bgneal@15 27 """The plugboard allows the operator to swap letters before and after the
bgneal@12 28 entry wheel. This is accomplished by connecting cables between pairs of
bgneal@16 29 plugs that are marked with letters (Heer & Luftwaffe models) or numbers
bgneal@16 30 (Kriegsmarine). Ten cables were issued with each machine; thus up to 10 of
bgneal@16 31 these swappings could be used as part of a machine setup.
bgneal@12 32
bgneal@12 33 Each cable swaps both the input and output signals. Thus if A is connected
bgneal@12 34 to B, A crosses to B in the keyboard to entry wheel direction and also in
bgneal@15 35 the reverse entry wheel to lamp direction.
bgneal@12 36
bgneal@12 37 """
bgneal@15 38 def __init__(self, wiring_pairs=None):
bgneal@15 39 """Configure the plugboard according to a list or tuple of integer
bgneal@15 40 pairs, or None.
bgneal@12 41
bgneal@15 42 A value of None or an empty list/tuple indicates no plugboard
bgneal@16 43 connections are to be used (i.e. a straight mapping).
bgneal@12 44
bgneal@15 45 Otherwise wiring_pairs must be an iterable of integer pairs, where each
bgneal@15 46 integer is between 0-25, inclusive. At most 10 such pairs can be
bgneal@15 47 specified. Each value represents an input/output path through the
bgneal@15 48 plugboard. It is invalid to specify the same path more than once in the
bgneal@15 49 list.
bgneal@15 50
bgneal@15 51 If an invalid wiring_pairs parameter is given, a PlugboardError is
bgneal@15 52 raised.
bgneal@15 53
bgneal@15 54 """
bgneal@15 55 # construct wiring mapping table with default 1-1 mappings
bgneal@15 56 self.wiring_map = list(range(26))
bgneal@15 57
bgneal@15 58 # use settings if provided
bgneal@15 59 if not wiring_pairs:
bgneal@15 60 return
bgneal@15 61
bgneal@15 62 if len(wiring_pairs) > MAX_PAIRS:
bgneal@15 63 raise PlugboardError('Please specify %d or less pairs' % MAX_PAIRS)
bgneal@15 64
bgneal@15 65 # ensure a path occurs at most once in the list
bgneal@15 66 counter = collections.Counter(chain.from_iterable(wiring_pairs))
bgneal@15 67 path, count = counter.most_common(1)[0]
bgneal@15 68 if count != 1:
bgneal@15 69 raise PlugboardError('duplicate connection: %d' % path)
bgneal@15 70
bgneal@15 71 # make the connections
bgneal@15 72 for pair in wiring_pairs:
bgneal@15 73 m = pair[0]
bgneal@15 74 n = pair[1]
bgneal@15 75 if not (0 <= m < 26) or not (0 <= n < 26):
bgneal@15 76 raise PlugboardError('invalid connection: %s' % str(pair))
bgneal@15 77
bgneal@15 78 self.wiring_map[m] = n
bgneal@15 79 self.wiring_map[n] = m
bgneal@15 80
bgneal@15 81 @classmethod
bgneal@15 82 def from_key_sheet(cls, settings=None):
bgneal@15 83 """Configure the plugboard according to a settings string as you may
bgneal@15 84 find on a key sheet.
bgneal@15 85
bgneal@16 86 Two syntaxes are supported, the Heer/Luftwaffe and Kriegsmarine styles:
bgneal@15 87
bgneal@16 88 In the Heer syntax, the settings are given as a string of
bgneal@15 89 alphabetic pairs. For example: 'PO ML IU KJ NH YT GB VF RE DC'
bgneal@15 90
bgneal@15 91 In the Kriegsmarine syntax, the settings are given as a string of number
bgneal@15 92 pairs, separated by a '/'. Note that the numbering uses 1-26, inclusive.
bgneal@15 93 For example: '18/26 17/4 21/6 3/16 19/14 22/7 8/1 12/25 5/9 10/15'
bgneal@12 94
bgneal@12 95 To specify no plugboard connections, settings can be None or an empty
bgneal@12 96 string.
bgneal@12 97
bgneal@12 98 A PlugboardError will be raised if the settings string is invalid, or if
bgneal@12 99 it contains more than MAX_PAIRS pairs. Each plug should be present at
bgneal@12 100 most once in the settings string.
bgneal@12 101
bgneal@12 102 """
bgneal@15 103 if not settings:
bgneal@15 104 return cls(None)
bgneal@12 105
bgneal@15 106 wiring_pairs = []
bgneal@15 107
bgneal@15 108 # detect which syntax is being used
bgneal@15 109 if settings.find('/') != -1:
bgneal@15 110 # Kriegsmarine syntax
bgneal@15 111 pairs = settings.split()
bgneal@15 112 for p in pairs:
bgneal@15 113 try:
bgneal@15 114 m, n = p.split('/')
bgneal@15 115 m, n = int(m), int(n)
bgneal@15 116 except ValueError:
bgneal@20 117 raise PlugboardError('invalid pair: %s' % p)
bgneal@12 118
bgneal@15 119 wiring_pairs.append((m - 1, n - 1))
bgneal@15 120 else:
bgneal@16 121 # Heer/Luftwaffe syntax
bgneal@15 122 pairs = settings.upper().split()
bgneal@12 123
bgneal@15 124 for p in pairs:
bgneal@15 125 if len(p) != 2:
bgneal@20 126 raise PlugboardError('invalid pair: %s' % p)
bgneal@12 127
bgneal@15 128 m = p[0]
bgneal@15 129 n = p[1]
bgneal@20 130 if m not in HEER_LABELS or n not in HEER_LABELS:
bgneal@20 131 raise PlugboardError('invalid pair: %s' % p)
bgneal@12 132
bgneal@15 133 wiring_pairs.append((ord(m) - ord('A'), ord(n) - ord('A')))
bgneal@12 134
bgneal@15 135 return cls(wiring_pairs)
bgneal@12 136
bgneal@12 137 def signal(self, n):
bgneal@12 138 """Simulate a signal entering the plugboard on wire n, where n must be
bgneal@12 139 an integer between 0 and 25.
bgneal@12 140
bgneal@12 141 Returns the wire number of the output signal (0-25).
bgneal@12 142
bgneal@12 143 Note that since the plugboard always crosses pairs of wires, it doesn't
bgneal@12 144 matter what direction (keyboard -> entry wheel or vice versa) the signal
bgneal@12 145 is coming from.
bgneal@12 146
bgneal@12 147 """
bgneal@12 148 return self.wiring_map[n]