changeset 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 39b2db64fcf2
children c292c6b5e7ae
files m209/drum.py m209/tests/test_drum.py
diffstat 2 files changed, 256 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m209/drum.py	Wed May 29 19:45:52 2013 -0500
@@ -0,0 +1,141 @@
+# 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))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m209/tests/test_drum.py	Wed May 29 19:45:52 2013 -0500
@@ -0,0 +1,115 @@
+# 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).
+
+"""test_drum.py - Unit tests for the Drum class for the M-209 simulation."""
+
+import unittest
+
+from ..drum import Drum, DrumError
+
+
+class DrumTestCase(unittest.TestCase):
+
+    def test_invalid_drum(self):
+        self.assertRaises(DrumError, Drum, 5)
+        self.assertRaises(DrumError, Drum, "fail")
+
+        invalid_lug_list = [(0, 5)] * (Drum.NUM_BARS + 1)
+        self.assertRaises(DrumError, Drum, invalid_lug_list)
+
+        invalid_lug_list = [(0, 0)] * Drum.NUM_BARS
+        self.assertRaises(DrumError, Drum, invalid_lug_list)
+
+        invalid_lug_list = [(0, 1), (-1, 2), (3, 4)]
+        self.assertRaises(DrumError, Drum, invalid_lug_list)
+
+        invalid_lug_list = [(0, ), (1, 8), (3, 4)]
+        self.assertRaises(DrumError, Drum, invalid_lug_list)
+
+        invalid_lug_list = [(0, 1), (1, 'x'), (3, 4)]
+        self.assertRaises(DrumError, Drum, invalid_lug_list)
+
+        invalid_lug_list = [(0, 1), (1, 8, 1), (3, 4)]
+        self.assertRaises(DrumError, Drum, invalid_lug_list)
+
+        invalid_lug_list = [(0, ), (1, 5), (3, 3)]
+        self.assertRaises(DrumError, Drum, invalid_lug_list)
+
+    def test_valid_drum(self):
+        # These should not raise anything
+        drum = Drum()
+        self.assertEqual(0, len(drum.bars))
+
+        drum = Drum([])
+        drum = Drum(None)
+        drum = Drum(lug_list=[])
+        drum = Drum([(0, 1), (2, 3), (4, 5), (5, ), (1, )])
+
+        lug_list = [(0,)] * Drum.NUM_BARS
+        drum = Drum(lug_list)
+
+        lug_list = [(0, 2)] * Drum.NUM_BARS
+        drum = Drum(lug_list)
+        self.assertEqual(Drum.NUM_BARS, len(drum.bars))
+
+    def test_invalid_key_list(self):
+        key_list = "jkdlaj;af;"
+        self.assertRaises(DrumError, Drum.from_key_list, key_list)
+
+        key_list = "jkdla-;af;"
+        self.assertRaises(DrumError, Drum.from_key_list, key_list)
+
+        key_list = "-1--99"
+        self.assertRaises(DrumError, Drum.from_key_list, key_list)
+
+        key_list = "1-99"
+        self.assertRaises(DrumError, Drum.from_key_list, key_list)
+
+        key_list = "101-99"
+        self.assertRaises(DrumError, Drum.from_key_list, key_list)
+
+        key_list = "10-4"
+        self.assertRaises(DrumError, Drum.from_key_list, key_list)
+
+        key_list = "2-0 1-0 1-0 10-4 2-3"
+        self.assertRaises(DrumError, Drum.from_key_list, key_list)
+
+        key_list = "2-0 1-0 1-0 4-4 2-3"
+        self.assertRaises(DrumError, Drum.from_key_list, key_list)
+
+        key_list = "2-0" * (Drum.NUM_BARS + 1)
+        self.assertRaises(DrumError, Drum.from_key_list, key_list)
+
+    def test_valid_key_list(self):
+        key_list = "0-6 1-4 2-5"
+        drum = Drum.from_key_list(key_list)
+        self.assertEqual([(5, ), (0, 3), (1, 4)], drum.bars)
+
+        key_list = "2-4 " + ("0-6 1-4 " * 13).rstrip()
+        drum = Drum.from_key_list(key_list)
+
+        bars = [(1, 3)] + [(5, ), (0, 3)] * 13
+        self.assertEqual(bars, drum.bars)
+
+    def test_rotate(self):
+        # These are just simple tests to flush out syntax errors, etc. Higher
+        # level tests will verify the correct operation of the entire machine.
+
+        drum = Drum()
+        self.assertEqual(0, drum.rotate([False] * 6))
+
+        drum = Drum()
+        self.assertEqual(0, drum.rotate([True] * 6))
+
+        drum = Drum([(1, )] * Drum.NUM_BARS)
+        self.assertEqual(drum.NUM_BARS, drum.rotate([True] * 6))
+
+        drum = Drum([(0, 5)] * Drum.NUM_BARS)
+        self.assertEqual(drum.NUM_BARS, drum.rotate([True] * 6))
+
+        drum = Drum([(2, 4)] * 10)
+        self.assertEqual(10, drum.rotate([False, False, True, False, True, False]))
+
+        drum = Drum([(2, 4)] * 10)
+        self.assertEqual(10, drum.rotate([False, False, False, False, True, False]))