Mercurial > public > enigma
changeset 0:2426d1d52025
Initial commit. Started a Rotor class.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Fri, 25 May 2012 19:38:22 -0500 |
parents | |
children | 9a403915a740 |
files | .hgignore enigma/__init__.py enigma/rotors/__init__.py enigma/rotors/rotor.py |
diffstat | 2 files changed, 203 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Fri May 25 19:38:22 2012 -0500 @@ -0,0 +1,3 @@ +syntax: glob +*.pyc +*.swp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/enigma/rotors/rotor.py Fri May 25 19:38:22 2012 -0500 @@ -0,0 +1,200 @@ +"""rotor.py - this module contains the Rotor class for the Enigma simulation. + +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). + +""" +import string +import collections + + +class RotorError(Exception): + pass + + +# In specifying a wiring for a rotor, every letter must be included exactly +# once. This variable helps us enforce that: +WIRING_FREQ_SET = set((letter, 1) for letter in string.ascii_uppercase) + + +class Rotor: + """The Rotor class represents the Enigma Machine rotors (Walzen). + + A rotor has 26 circularly arranged pins on the right (entry) side and 26 + contacts on the left side. Each pin is connected to a single contact by + internal wiring, thus establishing a substitution cipher. We represent this + wiring by establishing a mapping from a pin to a contact (and vice versa for + the return path). Internally we number the pins and contacts from 0-25 in a + clockwise manner with 0 being the "top". + + An alphabetic or numeric ring is fastened to the rotor by the operator. The + labels of this ring are displayed to the operator through a small window on + the top panel. The ring can be fixed to the rotor in one of 26 different + positions; this is called the ring setting (Ringstellung). We will number + the ring settings from 0-25 where 0 means no offset (e.g. the letter "A" is + mapped to pin 0 on an alphabetic ring). A ring setting of 1 means the letter + "B" is mapped to pin 0. + + Each rotor can be in one of 26 positions on the axle, with position 0 where + pin/contact 0 is being indicated in the operator window. The rotor rotates + towards the operator by mechanical means during normal operation as keys are + being pressed during data entry. Position 1 is thus defined to be one step + from position 0. Likewise, position 25 is the last position before another + step returns it to position 0, completing 1 trip around the axle. + + Finally, a rotor has a "stepping" or "turnover" parameter. Physically this + is implemented by putting a notch on the alphabet ring and it controls when + the rotor will "kick" the rotor to its left, causing the neighbor rotor to + rotate. Most rotors had one notch, but some Kriegsmarine rotors had 2 + notches and thus rotated twice as fast. + + Note that due to the system of ratchets and pawls, the middle rotor (in a 3 + rotor Enigma) can "double-step". The middle rotor will advance on the next + step of the first rotor a second time in a row, if the middle rotor is in + its own turnover position. + + Note that we allow the stepping parameter to be None. This indicates the + rotor does not rotate. This allows us to model the entry wheel and receivers + as stationary rotors. + + """ + + def __init__(self, model_name, wiring, ring_setting=0, stepping=None, + alpha_labels=True): + """Establish rotor characteristics: + + model_name - e.g. "I", "II", "III", "Beta", "Gamma" + + wiring - this should be a string of 26 alphabetic characters that + represents the internal wiring transformation of the signal as it enters + from the right side. This is the format used in various online + resources. For example, for the Wehrmacht & Luftwaffe Enigma type I + rotor the mapping is "EKMFLGDQVZNTOWYHXUSPAIBRCJ". + + ring_setting - this should be an integer from 0-25, inclusive, which + indicates the Ringstellung. A value of 0 means there is no offset; e.g. + the letter "A" is fixed to pin 0. A value of 1 means "B" is mapped to + pin 0. + + stepping - this is the stepping or turnover parameter. It can be a + simple string such as "R". This will indicate that when the rotor + transitions from "Q" to "R" (by observing the operator window), the + rotor will "kick" the rotor to its left, causing it to rotate. If the + rotor has more than one notch, this parameter should be a list of letter + positions, e.g. ['A', 'N']. + + alpha_labels - when True, the letters A-Z are used for the rotor ring + labels. If False, numeric string labels (01-26) are used. + + """ + self.name = model_name + self.wiring_str = wiring.upper() + self.ring_setting = ring_setting + self.stepping_parms = stepping + self.alpha_labels = alpha_labels + self.pos = None # not installed in an Enigma machine yet + + # check wiring length + if len(self.wiring_str) != 26: + raise RotorError("invalid wiring length") + + # check wiring format; must contain A-Z + for c in self.wiring_str: + if c not in string.ascii_uppercase: + raise RotorError("invalid wiring: %s" % wiring) + + # check wiring format; ensure every letter appears exactly once + input_set = set(collections.Counter(self.wiring_str).items()) + if input_set != WIRING_FREQ_SET: + raise RotorError("invalid wiring frequency") + + if not isinstance(ring_setting, int) or not (0 <= ring_setting < 26): + raise RotorError("invalid ring_setting") + + # Create two lists to describe the internal wiring. Two lists are used + # to do fast lookup from both entry (from the right) and exit (from the + # left). + + self.entry_map = [ord(pin) - ord('A') for pin in self.wiring_str] + + self.exit_map = [0] * 26 + for i, v in enumerate(self.entry_map): + self.exit_map[v] = i + + # configure ring labels + if alpha_labels: + self.ring_labels = string.ascii_uppercase + else: + self.ring_labels = ['{:02d}'.format(n) for n in range(1, 27)] + + # for mapping window display values to indices + self.display_map = dict(zip(self.ring_labels, range(26))) + + def set_display(self, val): + """Spin the rotor such that the string val appears in the operator + window. + + This sets the internal position of the rotor on the axle and thus + rotates the pins and contacts accordingly. + + A value of 'A' for example puts the rotor in position 0, assuming an + internal ring setting of 0 and alphabetic labels. + + If the rotor is using alphabetic ring labels, val must be a string in + 'A' - 'Z'. + + If the rotor is not using alphabetic ring labels, val must be a string + of the form '01' - '26'. + + """ + s = val.upper() + if s not in self.display_map: + raise RotorError("bad display value %s" % s) + + index = self.display_map[s] + + self.pos = (index - self.ring_setting) % 26 + + def get_display(self): + """Returns what is currently being displayed in the operator window.""" + + index = (self.pos + self.ring_setting) % 26 + return self.ring_labels[index] + + def signal_in(self, n): + """Simulate a signal entering the rotor from the right at a given pin + position n. + + n must be an integer between 0 and 25. + + Returns the contact number of the output signal (0-25). + + """ + # determine what pin we have at that position due to rotation + pin = (n + self.pos) % 26 + + # run it through the internal wiring + contact = self.entry_map[pin] + + # turn back into a position due to rotation + return (contact - self.pos) % 26 + + def signal_out(self, n): + """Simulate a signal entering the rotor from the left at a given + contact position n. + + n must be an integer between 0 and 25. + + Returns the pin number of the output signal (0-25). + + """ + # determine what contact we have at that position due to rotation + contact = (n + self.pos) % 26 + + # run it through the internal wiring + pin = self.exit_map[contact] + + # turn back into a position due to rotation + return (pin - self.pos) % 26 +