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]
|