Github Classroom Link: https://classroom.github.com/a/x79VhtcS
Like Wordle, on this assignment graphics are handled for you. Write your code in EnigmaConstants.py
or in files you create yourself and import.
This project is a 6 milestone project and has a reputation for being difficult. I struggled to understand the instructions and will try to explain things as best I can. You should consult heavily with Jed Rembold and Eric Roberts' instructions if mine are unclear, and ask questions as soon as you get stuck. I did better with Eric Roberts', but think most students do better with Jed Rembold's.
There are demos of each milestone embedded in Eric Roberts' instructions. You will want to use them. I would not have been able to do the assignment without them.
There is one core insight on this assignment, which is the notion of treating a string of 26 characters as a cipher. This is not an easy concept to describe in text or, to my mind, images. At any time if you are confused or stuck, think about:
While that is all there is, that is a novel style of thinking to many people and will correspond to novel ways to work with strings in Python. I used helper functions that I tested extensively to handle all of these transformations.
EnigmaModel.py
EnigmaConstants.py
EnigmaView.py
EnigmaRotor.py
EnigmaModel.py
should a show an image as soon as you download the starter code, but will be totally non-interactive.
key_pressed
, key_released
, is_key_down
[Show]key_pressed
, key_released
, is_key_down
[Hide]Make the keyboard active so that pressing the mouse down on a key, the view updates that key (to red).
In EnigmaModel.py
there is a class called EnigmaModel
which contains nine methods. For this milestone, you will update three of them.
key_pressed
key_pressed
is basically a listener, like "click_action". In it, you will want to take the provided argument letter
and store it.
The most obvious place to store the letter is within the EnigmaModel
class. The most obvious way to do this is to create a new attribute, such as self.letter
.
I tested key_pressed
by printing letter
along with a note that the print was coming from inside the key_pressed
function. When testing your code, you will want to click on one of the letter keys in the bottom half of the graphics window.
key_released
key_released
is also a listener. I used it to change the stored key value to None
to note that a key was no longer depressed.
is_key_down
is_key_down
is a method that takes a letter and returns a boolean. It should return True
if the provided letter is current depressed (perhaps, is equal to self.letter
).
You will be able to tell if is_key_down
and key_pressed
are working if when pressing a key on the lower part of the screen, it lights up (in red). You will be able to tell if key_released
is working if the lighting stops when the click action ends.
Basically, the graphics window will use this method to ask "is key down?" and light up a key accordingly.
is_lamp_on
[Show]is_lamp_on
[Hide]This task is temporary.
Make the "lamps" active so that pressing the mouse down on a key, the view updates the corresponding lamp (to yellow).
In EnigmaModel.py
there is a class called EnigmaModel
which contains nine methods. For this milestone, you will update one, two, or three of them. I updated three.
Light the lamp that corresponds to the depressed key, or not. My code was almost identical to the code in is_key_down
.
The most obvious way to do this is store a letter again. Also store a letter in key_pressed
within the EnigmaModel
class, such as self.lamp
. For this milestone, the letter and lamp values will be the same, but that will change in future milestones.
You may also need to update key_released
to turn the lamp back off.
get_rotor_letter
, rotor_clicked
[Show]get_rotor_letter
, rotor_clicked
[Hide]For this task you must create a EnigmaRotor
class is in a new file EnigmaRotor.py
Track and display the "offset" of the rotors so that by clicking on the rotors that advance by one letter.
EnigmaRotor
class.
get_rotor_letter
returns 'A' which corresponds to an offset of zero.
from EnigmaConstants import * # contains ALPHABET, other helpful values
def get_ith_letter(i:int)->str:
return ALPHABET[i % 26]
Once you can click on a rotor and see it change letter, you have done the externally visible work of this assignment. However, the way you store the ciphers and offsets will impact how you progress through future milestones. Make sure you understand the code you wrote and what it means before moving on!
Use a cipher to translate one letter to another.
forward
While ultimately encryption occurs on a key press, we can write code elsewhere, in functions or methods, and call those functions or methods within the key press method.
I implemented the first stage, and in fact first three stages of encryption, with a method I called "forward" inside of EnigmaRotor
def forward(self:EnigmaRotor, cipher:str) -> str:
We can think of a cipher like the following image:
This cipher is ROTOR_PERMUTATIONS[2]
and also called the fast rotor. It is the first rotor used in enigma to encrypt a letter.
Forward encryption with this cipher would take the letter "A", the 0-indexed letter of the alphabet, and return the letter "B", the 0-indexed letter of the cipher. The circled example the cipher would take "Q", the 16-indexed letter of the alphabet, and return "I", the 16-indexed letter of the cipher.
To me, this felt like I was applying a cipher to a letter, which felt like a function.
To encrypt a letter:
EnigmaRotor
class.
self.cipher = ROTOR_PERMUTATIONS[2]
On this milestone, I found it very helpful to be able to take a letter and determine it's index within the alphabet. There is a very easy way to do that in Python, using .index. Here is an example:
def get_letters_i(letter:str) -> int: # "Get letter's index "i" in the alphabet
return ALPHABET.index(letter)
You do not need to use this function! But using .index() will probably be very helpful.
In Milestone 1, you lit the lamp that corresponded to the pressed key.
Update that code, which may refer to self.lamp
in methodkey_pressed
within the EnigmaModel
class.
Rather than lighting the lamp that corresponds to the key, light the lamp that corresponds to what the resulting letter of forward encryption on the clicked letter. Use ROTOR_PERMUTATIONS[2]
.
Refer back to the cipher image:
Or look at the code itself:
"BDFHJLCPRTXVZNYEIWGAKMUSQO" # Permutation for fast rotor
You have implemented this milestone correct when you may click the "A" key and the "B" lamp lights up. Similarly, you may click the "Q" key and the "I" lamp lights up. Consult Eric Roberts' instructions for a complete demo.
There are three rotors corresponding to the three ciphers.
(2,1,0)
.
(2,1,0)
is easier to read than range(2, -1, -1)
.
Rather than lighting the lamp that corresponds to the key, light the lamp that corresponds to what the letter of the key becomes when forward encrypted using all three rotors.
Checking your code After doing the three rotors, you are ready for milestone 4. It is not that easy to check this milestone but I recommend using a debugging technique, such as the one taught in the debug lecture:
DEBUG = True
" at the beginning of EnigmaRotor.py
DEBUG and print()
" statements:
While these print statements may clutter your code, I encourage you to not to delete them. I needed them for a latter milestone. You may stop displaying them at any time by setting DEBUG = False
at the beginning of EnigmaRotor.py
or by changing them to comments.
To give you example of one thing to test, when pressing the key "A" I saw the following print out in the terminal:
Beginning loop with i = 2
before forward: letter = A letter's index in abcs = 0
after forward: letter = B letter's index in cipher = 0
Beginning loop with i = 1
before forward: letter = B letter's index in abcs = 1
after forward: letter = J letter's index in cipher = 1
Beginning loop with i = 0
before forward: letter = J letter's index in abcs = 9
after forward: letter = Z letter's index in cipher = 9
I computed the cipher index using .index, same as with the alphabet.
Implement reflection and reverse encryption
In EnigmaConstants.py
there is a cipher defined as follows:
REFLECTOR_PERMUTATION = "IXUHFEZDAOMTKQJWNSRLCYPBVG"
After passing "forward" through all three rotors, the thrice-encrypted letter is "reflected" through this cipher.
I performed reflection with a single call to "forward" over the reflector cipher after my loop going forward through the rotors.
I added debugging print statements before and after my reflector.
<snipped...>
after forward: letter = Z letter's index in cipher = 9
Reflecting letter = Z
Reflected letter = G
I considered this sub-milestone complete when I could click on "A" and see "G" light up in the graphics window.
To forward-encrypt through a rotor, we found an index in the alphabet and looked up that index in a cipher.
To reverse-encrypt through a rotor, we will find an index in the cipher and look up that index in the alphabet.
Note - you can test your reverse encryption on the reflector. The reflector is a special cipher that behaves the same way for forward and backward encryption.
>>> from EnigmaRotor import *
>>> refl = EnigmaRotor(REFLECTOR_PERMUTATION)
>>> refl.reverse("A")
'I'
>>> refl.forward("A")
'I'
Note that reverse-encryption will differ from forward-encryption on other rotors/ciphers.
>>> fast = EnigmaRotor(ROTOR_PERMUTATIONS[2])
>>> fast.forward("A")
'B'
>>> fast.reverse("A")
'T'
With a working reverse, the remaining stages of encryption simply require applying this function to the appropriate letters.
After reflection, the letter passes in a reverse direction through the rotors similarly to sub-milestone 3c.
There are three rotors corresponding to the three ciphers.
(0,1,2)
.
You have implemented this milestone correct when you may click the "A" key and the "R" lamp lights up. Similarly, you may click the "Q" key and the "P" lamp lights up.
Here is an example of "Q" to "P" encryption, visualized:
Consult Eric Roberts' instructions for a complete demo.
I found it helpful to print the following. This is from pressing "Q":
=== INPUT KEY Q ===
Forward loop with i = 2
before forward: letter = Q letter's index in abcs = 16
after forward: letter = I letter's index in cipher = 16
Forward loop with i = 1
before forward: letter = I letter's index in abcs = 8
after forward: letter = X letter's index in cipher = 8
Forward loop with i = 0
before forward: letter = X letter's index in abcs = 23
after forward: letter = R letter's index in cipher = 23
Reflecting letter = R
Reflected letter = S
Reverse loop with i = 0
before reverse: letter = S letter's index in cipher = 18
after reverse: letter = S letter's index in abcs = 18
Reverse loop with i = 1
before reverse: letter = S letter's index in cipher = 4
after reverse: letter = E letter's index in abcs = 4
Reverse loop with i = 2
before reverse: letter = E letter's index in cipher = 15
after reverse: letter = P letter's index in abcs = 15
Model the rotation of the rotors during encryption
rotate
Write the function rotate
:
def rotate(letter:str, offset:int) -> str:
Given a letter and a numerical offset, return the letter that is "offset" positions further in the alphabet, looping around.
For an offset of j.
get_ith_letter
to have a modulo operation.
This is perhaps more easily seen with an example.
>>> rotate("A",0)
'A'
>>> rotate("A",1)
'B'
>>> rotate("A",25)
'Z'
>>> rotate("Z",1)
'A'
>>> rotate("A",-1)
'Z'
>>> rotate("A",3)
'D'
I used get_letters_i
and get_ith_letter
to write rotate
.
rotate
in encryption.At an offset of zero, we can use ciphers to encrypt a letter against the alphabet. However, when rotors have a non-zero offset, this corresponds to a change in which indices match to which letter. I understand this as an application of rotate.
For every forward and reverse rotation (but NOT for the reflection) do the following:
If rotors are set all have offset zero, the code should work the same way as in previous milestone. Test this first.
After ensuring forward and reverse encryption still work correctly, I click a single time on the rightmost rotor - the fast rotor - such that the display reads "AAB" for rotor values.
Pressing "A" in this "AAB" arrangement should return "Z", shown by the "Z" lamp lighting up, rather than "R" as in the "AAA" configuration.
I wrote detailed debugging print statements. Here is an example of the "A" to "Z" encryption in the "AAB" configuration. Note the non-zero offset values.
=== INPUT KEY A ===
Forward loop with i = 2
rotating letter = A by offset = 1
before forward: letter = B letter's index in abcs = 1
after forward: letter = D letter's index in cipher = 1
rotating letter = D by offset = -1
Forward loop with i = 1
rotating letter = C by offset = 0
before forward: letter = C letter's index in abcs = 2
after forward: letter = D letter's index in cipher = 2
rotating letter = D by offset = 0
Forward loop with i = 0
rotating letter = D by offset = 0
before forward: letter = D letter's index in abcs = 3
after forward: letter = F letter's index in cipher = 3
rotating letter = F by offset = 0
Reflecting letter = F
Reflected letter = E
Reverse loop with i = 0
rotating letter = E by offset = 0
before reverse: letter = E letter's index in cipher = 0
after reverse: letter = A letter's index in abcs = 0
rotating letter = A by offset = 0
Reverse loop with i = 1
rotating letter = A by offset = 0
before reverse: letter = A letter's index in cipher = 0
after reverse: letter = A letter's index in abcs = 0
rotating letter = A by offset = 0
Reverse loop with i = 2
rotating letter = A by offset = 1
before reverse: letter = B letter's index in cipher = 0
after reverse: letter = A letter's index in abcs = 0
rotating letter = A by offset = -1
This is generated by 13 print statements. It took only a few minutes to write all 13.
These outputs are quite easy to evaluate against the demo provided by Eric Roberts' as the final, which includes a visualization of the rotor. Take advantage of these statements and that visualization to ensure your code works as expected.
advance
Write advance
which updates rotors when a key is pressed, likely as a method of the EnigmaModel
class that is called in the key_press
method prior to beginning encryption.
Every time a key is pressed, rotors should advance.
Rotors should advance prior to performing encryption. So when running EnigmaModel.py
for the first time, clicking "A" should return "Z" and not "R". Clicking "A" a second time should return "L".
When rotors advance on keypress, the Enigma project is complete.