view m209/drum.py @ 1:8b51044f9c94

Added the Drum class and tests for it.
author Brian Neal <bgneal@gmail.com>
date Wed, 29 May 2013 19:45:52 -0500
parents
children 0a2a066fb18c
line wrap: on
line source
# Copyright (C) 2013 by Brian Neal.
# This file is part of m209, the M-209 simulation.
# m209 is released under the MIT License (see LICENSE.txt).

"""This module contains the Drum class for the M-209 simulation."""

from . import M209Error

class DrumError(M209Error):
    """Exception class for all drum errors"""
    pass


class Drum:
    """The Drum class represents the drum cage inside the M-209.

    The drum cage consists of 27 bars where each bar has 2 movable lugs. The
    lugs can be slid into positions numbered 1-6 and/or 2 neutral positions
    numbered 0.  As the drum rotates all 27 bars have a chance for their lugs to
    interact with the effective pins on the 6 key wheels. As each bar rotates
    past the effective pins on the 6 key wheels, if one or both lugs come into
    contact with any wheel's effective pins, the bar will quickly shift left.
    This causes the indicator disk to rotate once, thus causing the substitution
    cipher for the selected letter to change. Thus the drum, along with the key
    wheels, acts as a pseudo-random number generator, generating a number
    between 0 and 27, inclusive. Thus number is used to select which
    substitution cipher will be used for the current operator selected letter.

    Internally the bars are represented as a list of 1 or 2 tuples, with one
    entry for each bar that has 1 or more lugs not in neutral positions.  Bars
    that have both lugs in neutral positions do not have entries in the list.
    A bar that has 1 lug in a neutral position, and the other lug in position
    3 will have an entry in the list consisting of the 1-tuple (2, ). A bar that
    has one lug in position 2 and one in position 6 will have an entry in the
    list consisting of the 2-tuple (1, 5). We subtract one from the position for
    0-based indexing reasons.

    The order of the bars list is not relevant as we only need to simulate
    complete revolutions of the drum cage.

    """
    NUM_BARS = 27

    def __init__(self, lug_list=None):
        """Creates a Drum instance with the given lug list.

        If lug_list is None or empty, all lugs will be placed in neutral
        positions.

        If lug_list is present, it must be a list of 1 or 2-tuple integers,
        where each integer is between 0-5, inclusive, and represents a 0-based
        key wheel position. The list can not be longer than NUM_BARS items.

        """
        self.bars = []
        if lug_list:
            self.bars = lug_list
            self._validate_bars()

    @classmethod
    def from_key_list(cls, lug_list):
        """Creates a Drum instance from a string that might be found on a key
        list. The must consist of at most 27 whitespace separated pairs of
        integers separated by dashes. For example:
                '1-0 2-0 2-0 0-3 0-5 0-5 0-6 2-4 3-6'

        Each integer pair must be in the form 'm-n' where m & n are integers
        between 0 and 6, inclusive. Each integer represents a lug position where
        0 is a neutral position, and 1-6 correspond to key wheel positions. If
        m & n are both non-zero, they cannot be equal.

        If a string has less than 27 pairs of integers, it is assumed all
        remaining bars have both lugs in the neutral (0) positions.

        """
        bars = []
        lug_list = lug_list.split()
        for lug_pair in lug_list:
            try:
                m, n = [int(x) for x in lug_pair.split('-')]
            except ValueError:
                raise DrumError("Invalid lug pair {}".format(lug_pair))

            if m and n:
                bars.append((m - 1, n - 1))
            elif m and not n:
                bars.append((m - 1, ))
            elif not m and n:
                bars.append((n - 1, ))

        return cls(lug_list=bars)

    def rotate(self, pins):
        """Rotate the drum cage a complete revolution and return the number of
        times a bar was shifted to the left. The pins parameter must be a
        6-element list of Bools representing the current effective states of
        the 6 key wheels.

        """
        count = 0
        for lug_pair in self.bars:
            for index in lug_pair:
                if pins[index]:
                    count += 1
                    break

        return count

    def _validate_bars(self):
        """Internal function to validate the bars list. Raises DrumError if the
        list is invalid.

        A list is valid if all of these conditions are true:
            * it has NUM_BARS or less entries
            * each entry must be a 1 or 2 tuple of integers in the range 0-5,
              inclusive
            * if an entry is a 2-tuple, the two elements must not be equal to
              each other

        """
        if not isinstance(self.bars, list):
            raise DrumError("Type of lug_list must be list")
        if len(self.bars) > self.NUM_BARS:
            raise DrumError("Too many bars in lug list")

        for lug_pair in self.bars:
            error = False
            try:
                if len(lug_pair) == 1:
                    error = not (0 <= lug_pair[0] <= 5)
                elif len(lug_pair) == 2:
                    error = not (0 <= lug_pair[0] <= 5) or not (
                            0 <= lug_pair[1] <= 5) or (
                                    lug_pair[0] == lug_pair[1])
                else:
                    error = True
            except (ValueError, TypeError):
                error = True

            if error:
                raise DrumError("Invalid lug pair {}".format(lug_pair))