view enigma/rotors/rotor.py @ 7:39297f695cff

First version of EnigmaMachine class. Simple main to test operations.
author Brian Neal <bgneal@gmail.com>
date Sat, 26 May 2012 17:21:22 -0500
parents 7a90beffd8f2
children f6edbbd35b92
line wrap: on
line source
# 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).

"""rotor.py - this module contains the Rotor class for the Enigma simulation."""

import string
import collections

from . import RotorError


ALPHA_LABELS = string.ascii_uppercase
NUMERIC_LABELS = ['{:02d}'.format(n) for n in range(1, 27)]

# 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
    reflectors 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 should be an
        iterable, for example a string such as "Q". 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, a string of length 2 could
        be used, e.g. "ZM".  Another way to think of this parameter is that when
        a character in the stepping string is visible in the operator window, a
        notch is lined up with the pawl on the left side of the rotor.  This
        will allow the pawl to push up on the rotor to the left when the next
        key is depressed.

        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.alpha_labels = alpha_labels
        self.pos = 0

        # 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
        self.ring_labels = ALPHA_LABELS if alpha_labels else NUMERIC_LABELS

        # for mapping window display values to indices
        self.display_map = dict(zip(self.ring_labels, range(26)))

        # build step list: this is a list of positions where our notches are in
        # place to allow the pawls to move
        self.step_set = set()
        if stepping is not None:
            for pos in stepping:
                if pos in self.display_map:
                    self.step_set.add(self.display_map[pos])
                else:
                    raise RotorError("stepping: %s" % pos)

    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