1 """A set of ciphers with implementations for both enciphering and deciphering
2 them. See cipherbreak for automatic breaking of these ciphers
8 from itertools
import zip_longest
, cycle
, chain
9 from language_models
import unaccent
, sanitise
12 modular_division_table
= [[0]*26 for _
in range(26)]
16 modular_division_table
[b
][c
] = a
19 def every_nth(text
, n
, fillvalue
=''):
20 """Returns n strings, each of which consists of every nth character,
21 starting with the 0th, 1st, 2nd, ... (n-1)th character
23 >>> every_nth(string.ascii_lowercase, 5)
24 ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty']
25 >>> every_nth(string.ascii_lowercase, 1)
26 ['abcdefghijklmnopqrstuvwxyz']
27 >>> every_nth(string.ascii_lowercase, 26) # doctest: +NORMALIZE_WHITESPACE
28 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
29 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
30 >>> every_nth(string.ascii_lowercase, 5, fillvalue='!')
31 ['afkpuz', 'bglqv!', 'chmrw!', 'dinsx!', 'ejoty!']
33 split_text
= chunks(text
, n
, fillvalue
)
34 return [''.join(l
) for l
in zip_longest(*split_text
, fillvalue
=fillvalue
)]
36 def combine_every_nth(split_text
):
37 """Reforms a text split into every_nth strings
39 >>> combine_every_nth(every_nth(string.ascii_lowercase, 5))
40 'abcdefghijklmnopqrstuvwxyz'
41 >>> combine_every_nth(every_nth(string.ascii_lowercase, 1))
42 'abcdefghijklmnopqrstuvwxyz'
43 >>> combine_every_nth(every_nth(string.ascii_lowercase, 26))
44 'abcdefghijklmnopqrstuvwxyz'
46 return ''.join([''.join(l
)
47 for l
in zip_longest(*split_text
, fillvalue
='')])
49 def chunks(text
, n
, fillvalue
=None):
50 """Split a text into chunks of n characters
52 >>> chunks('abcdefghi', 3)
54 >>> chunks('abcdefghi', 4)
56 >>> chunks('abcdefghi', 4, fillvalue='!')
57 ['abcd', 'efgh', 'i!!!']
60 padding
= fillvalue
[0] * (n
- len(text
) % n
)
63 return [(text
+padding
)[i
:i
+n
] for i
in range(0, len(text
), n
)]
65 def transpose(items
, transposition
):
66 """Moves items around according to the given transposition
68 >>> transpose(['a', 'b', 'c', 'd'], (0,1,2,3))
70 >>> transpose(['a', 'b', 'c', 'd'], (3,1,2,0))
72 >>> transpose([10,11,12,13,14,15], (3,2,4,1,5,0))
73 [13, 12, 14, 11, 15, 10]
75 transposed
= [''] * len(transposition
)
76 for p
, t
in enumerate(transposition
):
77 transposed
[p
] = items
[t
]
80 def untranspose(items
, transposition
):
83 >>> untranspose(['a', 'b', 'c', 'd'], [0,1,2,3])
85 >>> untranspose(['d', 'b', 'c', 'a'], [3,1,2,0])
87 >>> untranspose([13, 12, 14, 11, 15, 10], [3,2,4,1,5,0])
88 [10, 11, 12, 13, 14, 15]
90 transposed
= [''] * len(transposition
)
91 for p
, t
in enumerate(transposition
):
92 transposed
[t
] = items
[p
]
95 def deduplicate(text
):
96 """If a string contains duplicate letters, remove all but the first. Retain
97 the order of the letters.
99 >>> deduplicate('cat')
101 >>> deduplicate('happy')
103 >>> deduplicate('cattca')
106 return list(collections
.OrderedDict
.fromkeys(text
))
109 def caesar_encipher_letter(accented_letter
, shift
):
110 """Encipher a letter, given a shift amount
112 >>> caesar_encipher_letter('a', 1)
114 >>> caesar_encipher_letter('a', 2)
116 >>> caesar_encipher_letter('b', 2)
118 >>> caesar_encipher_letter('x', 2)
120 >>> caesar_encipher_letter('y', 2)
122 >>> caesar_encipher_letter('z', 2)
124 >>> caesar_encipher_letter('z', -1)
126 >>> caesar_encipher_letter('a', -1)
128 >>> caesar_encipher_letter('A', 1)
130 >>> caesar_encipher_letter('é', 1)
133 letter
= unaccent(accented_letter
)
134 if letter
in string
.ascii_letters
:
135 if letter
in string
.ascii_uppercase
:
136 alphabet_start
= ord('A')
138 alphabet_start
= ord('a')
139 return chr(((ord(letter
) - alphabet_start
+ shift
) % 26) +
144 def caesar_decipher_letter(letter
, shift
):
145 """Decipher a letter, given a shift amount
147 >>> caesar_decipher_letter('b', 1)
149 >>> caesar_decipher_letter('b', 2)
152 return caesar_encipher_letter(letter
, -shift
)
154 def caesar_encipher(message
, shift
):
155 """Encipher a message with the Caesar cipher of given shift
157 >>> caesar_encipher('abc', 1)
159 >>> caesar_encipher('abc', 2)
161 >>> caesar_encipher('abcxyz', 2)
163 >>> caesar_encipher('ab cx yz', 2)
165 >>> caesar_encipher('Héllo World!', 2)
168 enciphered
= [caesar_encipher_letter(l
, shift
) for l
in message
]
169 return ''.join(enciphered
)
171 def caesar_decipher(message
, shift
):
172 """Decipher a message with the Caesar cipher of given shift
174 >>> caesar_decipher('bcd', 1)
176 >>> caesar_decipher('cde', 2)
178 >>> caesar_decipher('cd ez ab', 2)
180 >>> caesar_decipher('Jgnnq Yqtnf!', 2)
183 return caesar_encipher(message
, -shift
)
185 def affine_encipher_letter(accented_letter
, multiplier
=1, adder
=0,
187 """Encipher a letter, given a multiplier and adder
188 >>> ''.join([affine_encipher_letter(l, 3, 5, True) \
189 for l in string.ascii_uppercase])
190 'HKNQTWZCFILORUXADGJMPSVYBE'
191 >>> ''.join([affine_encipher_letter(l, 3, 5, False) \
192 for l in string.ascii_uppercase])
193 'FILORUXADGJMPSVYBEHKNQTWZC'
195 letter
= unaccent(accented_letter
)
196 if letter
in string
.ascii_letters
:
197 if letter
in string
.ascii_uppercase
:
198 alphabet_start
= ord('A')
200 alphabet_start
= ord('a')
201 letter_number
= ord(letter
) - alphabet_start
202 if one_based
: letter_number
+= 1
203 cipher_number
= (letter_number
* multiplier
+ adder
) % 26
204 if one_based
: cipher_number
-= 1
205 return chr(cipher_number
% 26 + alphabet_start
)
209 def affine_decipher_letter(letter
, multiplier
=1, adder
=0, one_based
=True):
210 """Encipher a letter, given a multiplier and adder
212 >>> ''.join([affine_decipher_letter(l, 3, 5, True) \
213 for l in 'HKNQTWZCFILORUXADGJMPSVYBE'])
214 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
215 >>> ''.join([affine_decipher_letter(l, 3, 5, False) \
216 for l in 'FILORUXADGJMPSVYBEHKNQTWZC'])
217 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
219 if letter
in string
.ascii_letters
:
220 if letter
in string
.ascii_uppercase
:
221 alphabet_start
= ord('A')
223 alphabet_start
= ord('a')
224 cipher_number
= ord(letter
) - alphabet_start
225 if one_based
: cipher_number
+= 1
227 modular_division_table
[multiplier
]
228 [(cipher_number
- adder
) % 26] )
229 if one_based
: plaintext_number
-= 1
230 return chr(plaintext_number
% 26 + alphabet_start
)
234 def affine_encipher(message
, multiplier
=1, adder
=0, one_based
=True):
235 """Encipher a message
237 >>> affine_encipher('hours passed during which jerico tried every ' \
238 'trick he could think of', 15, 22, True)
239 'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
241 enciphered
= [affine_encipher_letter(l
, multiplier
, adder
, one_based
)
243 return ''.join(enciphered
)
245 def affine_decipher(message
, multiplier
=1, adder
=0, one_based
=True):
246 """Decipher a message
248 >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg ' \
249 'jfaoe ls omytd jlaxe mh', 15, 22, True)
250 'hours passed during which jerico tried every trick he could think of'
252 enciphered
= [affine_decipher_letter(l
, multiplier
, adder
, one_based
)
254 return ''.join(enciphered
)
257 class Keyword_wrap_alphabet(Enum
):
258 """Ways of wrapping the alphabet for keyword-based substitution ciphers."""
264 def keyword_cipher_alphabet_of(keyword
,
265 wrap_alphabet
=Keyword_wrap_alphabet
.from_a
):
266 """Find the cipher alphabet given a keyword.
267 wrap_alphabet controls how the rest of the alphabet is added
270 >>> keyword_cipher_alphabet_of('bayes')
271 'bayescdfghijklmnopqrtuvwxz'
272 >>> keyword_cipher_alphabet_of('bayes', Keyword_wrap_alphabet.from_a)
273 'bayescdfghijklmnopqrtuvwxz'
274 >>> keyword_cipher_alphabet_of('bayes', Keyword_wrap_alphabet.from_last)
275 'bayestuvwxzcdfghijklmnopqr'
276 >>> keyword_cipher_alphabet_of('bayes', Keyword_wrap_alphabet.from_largest)
277 'bayeszcdfghijklmnopqrtuvwx'
279 if wrap_alphabet
== Keyword_wrap_alphabet
.from_a
:
280 cipher_alphabet
= ''.join(deduplicate(sanitise(keyword
) +
281 string
.ascii_lowercase
))
283 if wrap_alphabet
== Keyword_wrap_alphabet
.from_last
:
284 last_keyword_letter
= deduplicate(sanitise(keyword
))[-1]
286 last_keyword_letter
= sorted(sanitise(keyword
))[-1]
287 last_keyword_position
= string
.ascii_lowercase
.find(
288 last_keyword_letter
) + 1
289 cipher_alphabet
= ''.join(
290 deduplicate(sanitise(keyword
) +
291 string
.ascii_lowercase
[last_keyword_position
:] +
292 string
.ascii_lowercase
))
293 return cipher_alphabet
296 def keyword_encipher(message
, keyword
,
297 wrap_alphabet
=Keyword_wrap_alphabet
.from_a
):
298 """Enciphers a message with a keyword substitution cipher.
299 wrap_alphabet controls how the rest of the alphabet is added
302 1 : from the last letter in the sanitised keyword
303 2 : from the largest letter in the sanitised keyword
305 >>> keyword_encipher('test message', 'bayes')
307 >>> keyword_encipher('test message', 'bayes', Keyword_wrap_alphabet.from_a)
309 >>> keyword_encipher('test message', 'bayes', Keyword_wrap_alphabet.from_last)
311 >>> keyword_encipher('test message', 'bayes', Keyword_wrap_alphabet.from_largest)
314 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
315 cipher_translation
= ''.maketrans(string
.ascii_lowercase
, cipher_alphabet
)
316 return unaccent(message
).lower().translate(cipher_translation
)
318 def keyword_decipher(message
, keyword
, wrap_alphabet
=Keyword_wrap_alphabet
.from_a
):
319 """Deciphers a message with a keyword substitution cipher.
320 wrap_alphabet controls how the rest of the alphabet is added
323 1 : from the last letter in the sanitised keyword
324 2 : from the largest letter in the sanitised keyword
326 >>> keyword_decipher('rsqr ksqqbds', 'bayes')
328 >>> keyword_decipher('rsqr ksqqbds', 'bayes', Keyword_wrap_alphabet.from_a)
330 >>> keyword_decipher('lskl dskkbus', 'bayes', Keyword_wrap_alphabet.from_last)
332 >>> keyword_decipher('qspq jsppbcs', 'bayes', Keyword_wrap_alphabet.from_largest)
335 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
336 cipher_translation
= ''.maketrans(cipher_alphabet
, string
.ascii_lowercase
)
337 return message
.lower().translate(cipher_translation
)
340 def vigenere_encipher(message
, keyword
):
343 >>> vigenere_encipher('hello', 'abc')
346 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
347 pairs
= zip(message
, cycle(shifts
))
348 return ''.join([caesar_encipher_letter(l
, k
) for l
, k
in pairs
])
350 def vigenere_decipher(message
, keyword
):
353 >>> vigenere_decipher('hfnlp', 'abc')
356 shifts
= [ord(l
) - ord('a') for l
in sanitise(keyword
)]
357 pairs
= zip(message
, cycle(shifts
))
358 return ''.join([caesar_decipher_letter(l
, k
) for l
, k
in pairs
])
360 beaufort_encipher
=vigenere_decipher
361 beaufort_decipher
=vigenere_encipher
364 def transpositions_of(keyword
):
365 """Finds the transpostions given by a keyword. For instance, the keyword
366 'clever' rearranges to 'celrv', so the first column (0) stays first, the
367 second column (1) moves to third, the third column (2) moves to second,
370 If passed a tuple, assume it's already a transposition and just return it.
372 >>> transpositions_of('clever')
374 >>> transpositions_of('fred')
376 >>> transpositions_of((3, 2, 0, 1))
379 if isinstance(keyword
, tuple):
382 key
= deduplicate(keyword
)
383 transpositions
= tuple(key
.index(l
) for l
in sorted(key
))
384 return transpositions
386 def pad(message_len
, group_len
, fillvalue
):
387 """Returns the padding required to extend a message of message_len to an
388 even multiple of group_len, by adding repreated copies of fillvalue.
389 fillvalue can either be a character or a function that returns a character.
397 >>> pad(10, 4, lambda: '*')
400 padding_length
= group_len
- message_len
% group_len
401 if padding_length
== group_len
: padding_length
= 0
403 for _
in range(padding_length
):
404 if callable(fillvalue
):
405 padding
+= fillvalue()
410 def column_transposition_encipher(message
, keyword
, fillvalue
=' ',
411 fillcolumnwise
=False,
412 emptycolumnwise
=False):
413 """Enciphers using the column transposition cipher.
414 Message is padded to allow all rows to be the same length.
416 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True)
418 >>> column_transposition_encipher('hellothere', 'abcdef', fillcolumnwise=True, emptycolumnwise=True)
420 >>> column_transposition_encipher('hellothere', 'abcdef')
422 >>> column_transposition_encipher('hellothere', 'abcde')
424 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
426 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
428 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
430 >>> column_transposition_encipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
432 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=True)
434 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=True, emptycolumnwise=False)
436 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=True)
438 >>> column_transposition_encipher('hellothere', 'clever', fillcolumnwise=False, emptycolumnwise=False)
440 >>> column_transposition_encipher('hellothere', 'cleverly')
442 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue='!')
444 >>> column_transposition_encipher('hellothere', 'cleverly', fillvalue=lambda: '*')
447 transpositions
= transpositions_of(keyword
)
448 message
+= pad(len(message
), len(transpositions
), fillvalue
)
450 rows
= every_nth(message
, len(message
) // len(transpositions
))
452 rows
= chunks(message
, len(transpositions
))
453 transposed
= [transpose(r
, transpositions
) for r
in rows
]
455 return combine_every_nth(transposed
)
457 return ''.join(chain(*transposed
))
459 def column_transposition_decipher(message
, keyword
, fillvalue
=' ',
460 fillcolumnwise
=False,
461 emptycolumnwise
=False):
462 """Deciphers using the column transposition cipher.
463 Message is padded to allow all rows to be the same length.
465 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=True, emptycolumnwise=True)
467 >>> column_transposition_decipher('hlohreltee', 'abcde', fillcolumnwise=True, emptycolumnwise=False)
469 >>> column_transposition_decipher('htehlelroe', 'abcde', fillcolumnwise=False, emptycolumnwise=True)
471 >>> column_transposition_decipher('hellothere', 'abcde', fillcolumnwise=False, emptycolumnwise=False)
473 >>> column_transposition_decipher('heotllrehe', 'clever', fillcolumnwise=True, emptycolumnwise=True)
475 >>> column_transposition_decipher('holrhetlee', 'clever', fillcolumnwise=True, emptycolumnwise=False)
477 >>> column_transposition_decipher('htleehoelr', 'clever', fillcolumnwise=False, emptycolumnwise=True)
479 >>> column_transposition_decipher('hleolteher', 'clever', fillcolumnwise=False, emptycolumnwise=False)
482 transpositions
= transpositions_of(keyword
)
483 message
+= pad(len(message
), len(transpositions
), '*')
485 rows
= every_nth(message
, len(message
) // len(transpositions
))
487 rows
= chunks(message
, len(transpositions
))
488 untransposed
= [untranspose(r
, transpositions
) for r
in rows
]
490 return combine_every_nth(untransposed
)
492 return ''.join(chain(*untransposed
))
494 def scytale_encipher(message
, rows
, fillvalue
=' '):
495 """Enciphers using the scytale transposition cipher.
496 Message is padded with spaces to allow all rows to be the same length.
498 >>> scytale_encipher('thequickbrownfox', 3)
500 >>> scytale_encipher('thequickbrownfox', 4)
502 >>> scytale_encipher('thequickbrownfox', 5)
503 'tubn hirf ecoo qkwx '
504 >>> scytale_encipher('thequickbrownfox', 6)
506 >>> scytale_encipher('thequickbrownfox', 7)
507 'tqcrnx hukof eibwo '
509 transpositions
= [i
for i
in range(rows
)]
510 return column_transposition_encipher(message
, transpositions
,
511 fillvalue
=fillvalue
, fillcolumnwise
=True, emptycolumnwise
=False)
513 def scytale_decipher(message
, rows
):
514 """Deciphers using the scytale transposition cipher.
515 Assumes the message is padded so that all rows are the same length.
517 >>> scytale_decipher('tcnhkfeboqrxuo iw ', 3)
519 >>> scytale_decipher('tubnhirfecooqkwx', 4)
521 >>> scytale_decipher('tubn hirf ecoo qkwx ', 5)
523 >>> scytale_decipher('tqcrnxhukof eibwo ', 6)
525 >>> scytale_decipher('tqcrnx hukof eibwo ', 7)
528 transpositions
= [i
for i
in range(rows
)]
529 return column_transposition_decipher(message
, transpositions
,
530 fillcolumnwise
=True, emptycolumnwise
=False)
533 class PocketEnigma(object):
534 """A pocket enigma machine
535 The wheel is internally represented as a 26-element list self.wheel_map,
536 where wheel_map[i] == j shows that the position i places on from the arrow
537 maps to the position j places on.
539 def __init__(self
, wheel
=1, position
='a'):
540 """initialise the pocket enigma, including which wheel to use and the
541 starting position of the wheel.
543 The wheel is either 1 or 2 (the predefined wheels) or a list of letter
546 The position is the letter pointed to by the arrow on the wheel.
549 [25, 4, 23, 10, 1, 7, 9, 5, 12, 6, 3, 17, 8, 14, 13, 21, 19, 11, 20, 16, 18, 15, 24, 2, 22, 0]
553 self
.wheel1
= [('a', 'z'), ('b', 'e'), ('c', 'x'), ('d', 'k'),
554 ('f', 'h'), ('g', 'j'), ('i', 'm'), ('l', 'r'), ('n', 'o'),
555 ('p', 'v'), ('q', 't'), ('s', 'u'), ('w', 'y')]
556 self
.wheel2
= [('a', 'c'), ('b', 'd'), ('e', 'w'), ('f', 'i'),
557 ('g', 'p'), ('h', 'm'), ('j', 'k'), ('l', 'n'), ('o', 'q'),
558 ('r', 'z'), ('s', 'u'), ('t', 'v'), ('x', 'y')]
560 self
.make_wheel_map(self
.wheel1
)
562 self
.make_wheel_map(self
.wheel2
)
564 self
.validate_wheel_spec(wheel
)
565 self
.make_wheel_map(wheel
)
566 if position
in string
.ascii_lowercase
:
567 self
.position
= ord(position
) - ord('a')
569 self
.position
= position
571 def make_wheel_map(self
, wheel_spec
):
572 """Expands a wheel specification from a list of letter-letter pairs
573 into a full wheel_map.
575 >>> pe.make_wheel_map(pe.wheel2)
576 [2, 3, 0, 1, 22, 8, 15, 12, 5, 10, 9, 13, 7, 11, 16, 6, 14, 25, 20, 21, 18, 19, 4, 24, 23, 17]
578 self
.validate_wheel_spec(wheel_spec
)
579 self
.wheel_map
= [0] * 26
581 self
.wheel_map
[ord(p
[0]) - ord('a')] = ord(p
[1]) - ord('a')
582 self
.wheel_map
[ord(p
[1]) - ord('a')] = ord(p
[0]) - ord('a')
583 return self
.wheel_map
585 def validate_wheel_spec(self
, wheel_spec
):
586 """Validates that a wheel specificaiton will turn into a valid wheel
589 >>> pe.validate_wheel_spec([])
590 Traceback (most recent call last):
592 ValueError: Wheel specification has 0 pairs, requires 13
593 >>> pe.validate_wheel_spec([('a', 'b', 'c')]*13)
594 Traceback (most recent call last):
596 ValueError: Not all mappings in wheel specificationhave two elements
597 >>> pe.validate_wheel_spec([('a', 'b')]*13)
598 Traceback (most recent call last):
600 ValueError: Wheel specification does not contain 26 letters
602 if len(wheel_spec
) != 13:
603 raise ValueError("Wheel specification has {} pairs, requires 13".
604 format(len(wheel_spec
)))
607 raise ValueError("Not all mappings in wheel specification"
609 if len(set([p
[0] for p
in wheel_spec
] +
610 [p
[1] for p
in wheel_spec
])) != 26:
611 raise ValueError("Wheel specification does not contain 26 letters")
613 def encipher_letter(self
, letter
):
614 """Enciphers a single letter, by advancing the wheel before looking up
615 the letter on the wheel.
617 >>> pe.set_position('f')
619 >>> pe.encipher_letter('k')
623 return self
.lookup(letter
)
624 decipher_letter
= encipher_letter
626 def lookup(self
, letter
):
627 """Look up what a letter enciphers to, without turning the wheel.
629 >>> pe.set_position('f')
631 >>> ''.join([pe.lookup(l) for l in string.ascii_lowercase])
632 'udhbfejcpgmokrliwntsayqzvx'
636 if letter
in string
.ascii_lowercase
:
638 (self
.wheel_map
[(ord(letter
) - ord('a') - self
.position
) % 26] +
639 self
.position
) % 26 +
645 """Advances the wheel one position.
647 >>> pe.set_position('f')
652 self
.position
= (self
.position
+ 1) % 26
655 def encipher(self
, message
, starting_position
=None):
656 """Enciphers a whole message.
658 >>> pe.set_position('f')
660 >>> pe.encipher('helloworld')
662 >>> pe.set_position('f')
664 >>> pe.encipher('kjsglcjoqc')
666 >>> pe.encipher('helloworld', starting_position = 'x')
669 if starting_position
:
670 self
.set_position(starting_position
)
673 transformed
+= self
.encipher_letter(l
)
677 def set_position(self
, position
):
678 """Sets the position of the wheel, by specifying the letter the arrow
681 >>> pe.set_position('a')
683 >>> pe.set_position('m')
685 >>> pe.set_position('z')
688 self
.position
= ord(position
) - ord('a')
692 if __name__
== "__main__":
694 doctest
.testmod(extraglobs
={'pe': PocketEnigma(1, 'a')})