Mercurial > public > m209
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]))