A1: Enigma

This assignment asks you to develop a software replica of the Enigma encryption machine used by the German military in World War II. Efforts to break the Enigma cipher led to the development of the first electronic computers at Bletchley Park, England. Using computers, the Allies were eventually able to break the Enigma code, giving them an intelligence edge that changed the balance of the war. The most famous of those computers is the Bombe, which was designed by Alan Turing (pictured to the right). Turing is arguably the founder of modern computer science.
This assignment also asks you to learn about some modern software development tools and methodologies.
Time: Get started right away and make steady progress each day. Consulting hours will become over-crowded near the deadline. The best plan is to finish early. Please track the time that you spend. I will ask you to report it. On the same assignment last semester, the mean hours-reported-worked was 11.1, and the standard deviation was 3.9. But the weight of the assignment that semester was 8% rather than the 4% it is worth this semester. By reducing assignment weights this semester, I hope to reduce the amount of time anyone feels compelled to spend. As you’ve seen in the syllabus, there’s no reason to stress over Excellent Scope, and even Good Scope can be skipped from time to time without serious consequences. You’re not going to fail the class because of assignments.
Collaboration policy: This assignment is to be completed as an individual. You may have only limited collaboration with other students as described in the syllabus.
What you’ll do: Implement half a dozen functions, and test them with OUnit; maintain your code in Git, a source control tool; and use test-driven development, an agile software development technique.
Objectives:
- Use OCaml data types.
- Model a stateful computation in the functional style.
- Construct an OUnit test suite.
- Become familiar with the basic Git operations of adding, committing, pushing, and pulling.
- Train yourself to write test cases before writing code.
Table of contents:
- FAQs
- Getting Started
- Step 0: Download the Release Code
- Step 1: Get Git Going
- Step 2: Academic Integrity
- Step 3: Index
- Step 4: Enigma Tutorial
- Step 5: Rotors
- Step 6: Reflector
- Step 7: Plugboard
- Step 8: Ciphering a character
- Step 9: Stepping
- Step 10: Ciphering a string
- Rubric
- Submission
FAQs
-
Q: May I change the names, types, or specifications of required functions?
A: No.
-
Q: May I add or remove the
reckeyword from a function definition without changing the function’s type?A: Yes.
-
Q: May I add helper functions?
A: Yes.
-
Q: Do those helper functions have to be nested inside the function that uses them?
A: No.
-
Q: May I use standard library functions—for example, functions from the
Listmodule? Or theStringandCharmodules?A: Yes, except for the deprecated functions in
String. Any time a library marks a function as deprecated you shouldn’t write new code that uses it. -
Q: May I use some other OPAM or third-party library that would require changing the Makefile or dune files?
A: No.
-
Q: May I use loops?
A: No.
-
Q: May I use imperative features?
A: No. See the Imperative Features Prohibitions page linked from the Canvas front page.
-
Q: Why am I getting a squiggle from VS Code in
src/enigma.mlon the second line, which is blank?A: You haven’t built the project yet. Run
make build. You might need to close and reopen the.mlfile (or make a trivial change, like typing then deleting a space) to make VS Code recognize the fact that you’ve built the project. -
Q: Why am I getting an “Unbound module” error for OUnit2 in
test/main.ml?A: You haven’t built the testing part of the project yet. Run
make test. Again, you might need to close and reopen the.mlfile. -
Q: I’m trying to use an online Enigma simulator for test cases but it doesn’t seem to get the right answers.
A: It’s probably because of stepping or turnovers. Read all about those later in this handout for the details.
Getting Started
Although the assignment could seem intimidating at first, just take it a step at a time and you’ll do great! The rest of the handout below gives you instructions on each step you should take. There is a series of functions to implement, each of which builds on the ones that came before. By the time you’ve implemented all the functions, you will have finished your Enigma simulator.
You are ready to get started on this assignment today! Later parts of the assignment will rely on material we haven’t covered as of the day the assignment is released, but we’re getting there soon. Those dependencies are identified at the top of each section where they matter.
The mathematics of the Enigma cipher itself could seem mysterious at first. But I’ve carefully designed the functions to break it down into smaller pieces, so that you can focus on each piece in isolation. I’ve also supplied many examples below that will help you to check your understanding. Developing software in the real world always challenges you to become a little bit of an expert in your client’s domain; think of learning about Enigma as practicing that. As a side benefit, you’ll come away from this assignment understanding an important artifact in the history of computer science.
A highly recommended arts and crafts project: As you proceed with building a simulation of the Enigma machine in this assignment, it could be useful to have a completed simulator at your side. The best Enigma simulator I can recommend is this one. It’s totally worth your while to assemble this simulator. It provides excellent insight into the physical machine, especially the wiring of the rotors and the reflector. To assemble it, you need to buy a tube of Pringles potato chips, print out the paper at 100% scaling (which can be tricky because it’s A4; to help with that I provide these close-ups [1] [2] of the important parts that should be printable on letter paper), then cut out the paper pieces and wrap them around the tube. This physical simulator offers considerably more insight than any virtual simulator I’ve found online.
Step 0: Download the Release Code
Download the release code from CMS. It is a ZIP file named a1.zip.
Identify a place where you’d like to store your 3110 work. For example, you
might store it in ~/cs3110. Recall that ~ is your home directory, which is
located at /home/<your_unix_username>. You could create that directory
by opening your Unix terminal and running this command:
$ mkdir ~/cs3110
Reminder: The $ before each command just indicates the prompt that you see in
the terminal. You should not type the $ yourself. If you’re using Zsh (which
is the default these days on Mac), your prompt might instead appear as %; that
is fine.
Move that ZIP file into your work directory:
-
Linux and Mac: You have it easy. Just move the ZIP file into your 3110 work directory through whatever means you normally would use.
-
Windows: You need to get that ZIP file into your WSL filesystem. Here are a few options, listed (I hope) from least to most annoying:
-
Option 1. Open the Windows File Explorer and navigate to
\\wsl$. There you will see your installed Ubuntu distributions. (If not, open an Ubuntu terminal, leave it running, and try again with File Explorer.) You can navigate to your WSL home directory at/home/<your_wsl_username>, then drag-and-drop the ZIP file into it. - Option 2. Let’s assume you saved the ZIP file in your Downloads folder on
your C drive under the name
a1.zip. In Ubuntu,cdinto the directory where you want to copy that file, then run this command:$ cp /mnt/c/Users/<your Windows user name>/Downloads/a1.zip .That will copy the ZIP file from Windows into WSL.
- Option 3. Open VS Code from WSL, making sure that the bottom left of the VS Code window has a green area that says “WSL: Ubuntu-20.04”. Use VS Code to open any folder of your choice inside your WSL home directory. Drag the ZIP file from wherever you saved it into the VS Code explorer pane, inside the folder you chose. That will copy the ZIP file into your WSL filesystem.
-
Extract the ZIP file. Do this from the terminal, not from a graphical
interface. Run unzip a1.zip from the directory containing the ZIP file. That
will create a subdirectory a1 with the release code. Now cd into that
directory. You should now see all the A1 release code. For example, if you run
ls, you will see a src directory, a Makefile, a dune file, etc. We’ll
call this directory your A1 root directory.
A common mistake is to extract the ZIP file using a graphical file browser
then using that browser to copy the files. If you do that, you might omit
copying some dotfiles: files whose names start with . and most of the time
are hidden from you by graphical tools. (VS Code’s explorer pane does show them,
though.) The most obvious symptom of that mistake will appear much later on,
when VS Code does not automatically format your code when you save it.
Finally, check your work by running make check. You should get output that
ends with the following line:
🐫 <><> Congratulations! You have passed [make check]. <><>
If instead you get errors, seek help immediately from a consultant.
This make check system is checking that your OCaml environment is good, and
that you have defined all the required functions with the correct types (which
the release code does). After completing each function in the rest of the
assignment, you should re-run make check to ensure that you haven’t changed
any required names or types.
The Makefile in the release code is what provides the make check command.
There are others it provides, too. Here is a summary of them:
make buildwill build your source code in thesrcdirectorymake utopwill launch utop with your code loaded alreadymake testwill run your OUnit test suitemake finalcheckis likemake checkbut with some additional checks before your final submission to CMSmake zipcreates the file you need to submit to CMSmake cleanclears away all compiled files
You’ll learn more about those throughout the rest of this handout.
Step 1: Get Git Going
If you already know Git, great! Otherwise, watch this 20 minute video tutorial:
For an interactive tutorial, you can also download and run the latest release of Git-it for your operating system (Linux, Mac, or Windows).
Do the following to configure your Git account:
-
Login to https://github.coecis.cornell.edu/, which is the Cornell-hosted GitHub site. Please do use the Cornell GitHub, not the public GitHub, for your assignments in this class. (You get unlimited private repos on the Cornell GitHub.)
- From the terminal, run the following commands:
$ git config --global user.name "Your Name" $ git config --global user.email "your_netid@cornell.edu"If you get an error message that says
command not found, you don’t have Git installed on your local machine. That actually should not happen on any platform if you’ve followed the install instructions for this class. - Again from the terminal, run this command:
$ ssh-keygenWhen prompted for a file location, press return to accept the default. If you get an error that says the file already exists, then at some point in the past you’ve already created a SSH keypair before. It’s up to you whether you want to re-use it here.
When prompted for a passphrase, enter a secret phrase (i.e., password) that you will remember. It is possible to use an empty passphrase, but there are security implications to that. So I won’t recommend it.
When key generation finishes, you will have two new files in
~/.ssh. One isid_rsa.pub. This is your public key. It is safe to post upload that file to servers on the internet, which we’re going to do in just a minute. The other file isid_rsa. This is your private key. Treat it like a password: no one other than you should ever have access to that file, and you should never upload it anywhere. Your private key file is encrypted with the passphrase you chose above as an extra layer of security. -
Back at the Cornell GitHub website, click on the dropdown arrow at the top right next to your profile picture (which is probably just an abstract piece of art if you haven’t already uploaded a pic). Click Settings. On the left, click “SSH and GPG keys”. Click the green “New SSH key” button.
Give the key a title of your choice. I typically use a name that reminds me of the machine where the private key is stored. In the key field, you need to copy and paste the contents of your public key file
~/.ssh/id_rsa.pub. One way to do that would be to run$ cat ~/.ssh/id_rsa.pubin the terminal, then copy-paste the output into the webpage. Make sure you don’t change or omit anything as you copy it. Click “Add SSH key”.
You should now see that public key listed among your keys.
All the above was a one-time setup for the first time you use Cornell GitHub. Now, we need to create a repo specifically for A1:
- In the terminal,
cdto your A1 root directory. From there, run this command to initialize your Git repo:$ git initYou will see in response:
Initialized empty Git repository in <your directory>Continuing in the terminal in that same directory, add the starter code into source control by running:
$ git add .There will be no response to that. Then commit the code:
$ git commit -m "Import release code"The response will be:
[main (root-commit) <a hex number>] Import release code <a few other lines>Now you have a working repo, but it isn’t being stored on the Cornell GitHub. Let’s link the two.
-
In your web browser, navigate to the Cornell GitHub. On the top right of the page, click the “+” icon, then select “New repository”. Give the repo a name, perhaps “cs3110-a1”, and make sure the radio button is set to Private, not Public. A public repo would share your source code with the entire Cornell community, which would violate the Academic Integrity policies of this course. Leave the boxes about
READMEand.gitignoreunchecked. Click “Create repository”. -
Under the heading “Quick setup”, click SSH. Then look for the section of the page that is headed “…push an existing repository…”. Inspect the commands there to make sure they look like the examples in the next paragraph. In particular, you should not see “https” anywhere in them. If you do, make sure you did click SSH at the top of the page.
Run those commands in your terminal from your
a1directory. They will look like the following, except that the first command will have different values for your NetID and repo name:$ git remote add origin git@github.coecis.cornell.edu:your_net_id/your_repo_name.git $ git branch -M main $ git push -u origin mainThe first two commands need to be run only once per repo and won’t produce any output. The last command will produce some output confirming that you pushed. Henceforth you’ll be able to run just
git pushinstead of the longer form of the command.The very first time you push to the Cornell GitHub you might get a warning that looks like the following:
The authenticity of host 'github.coecis.cornell.edu (128.253.51.124)' can't be established. ECDSA key fingerprint is SHA256:hWmDOUjbhOCzQTfXAdqznHGEKiKSfJzYeWgGsKOloLk. Are you sure you want to continue connecting (yes/no/[fingerprint])?As long as the fingerprint displayed to you matches the one above, everything is fine. SSH is just warning you that it’s the first time you’ve contacted that server. If the fingerprint were to differ, then either (i) Cornell has changed something about that server, or (ii) somehow an attacker is trying to steal your connection to the Cornell GitHub. The latter is admittedly a little far-fetched but nonetheless a technical possibility. The former is more likely and worth a public Ed post.
Congratulations! You now have a repository for this assignment.
Step 2: Academic Integrity
From your A1 root directory, open VS Code with code .. You should see a src
directory in the Explorer pane. Expand it, then open the enigma.ml file inside
it.
Review and complete the Academic Integrity statement provided in a comment at
the top of src/enigma.ml. Make sure to fill in your name and NetID. Update the
rest of the statement as needed throughout your work on this assignment.
Now let’s save the work that you just did in your repo. From your A1 root directory in the terminal, here’s how to do that:
-
Run
git status. You should see thatenigma.mlhas been modified. -
Run
git add src/enigma.mlto add that file to the staging area. -
Run
git commit. That will open the Vim editor, in which you can type a commit message. Pressito enter insert mode. Now type your commit message. -
Press the Escape key, then type
:wq, and press the Enter key. Escape puts Vim into normal mode, the colon moves you to the command-line mode, andwqsays to write the file you are editing to disk (i.e., save it) then quit. Vim will close. -
Back at the command line, type
git push. Check the GitHub website to see that your code is now saved there.
Alternatively, you might be able to use VS Code as an interface for committing
and pushing. Actually the committing part isn’t a problem, but pushing (and
pulling) could be. Because of a pre-existing issue in VS
Code, you won’t be able to push/pull if your SSH private key has a non-empty
passphrase. It’s frankly inconceivable not to support
passphrases, but nonetheless thanks to Microsoft that’s where we are. If you
want to change your passphrase to be empty, just run ssh-keygen -p and hit
Return when prompted for the new passphrase. Is there a security vulnerability
associated with that? Yes. It’s essentially equivalent to
storing your Cornell GitHub password in an un-encrypted file on your local hard
drive. If you trust that no attacker is going to get access to that file, then
you have nothing to worry about. Assuming you do wish to use VS Code, here’s
how:
-
Go to View -> SCM in VS Code. It’s also available as an icon on the left that looks like a tree with three nodes. It probably has a blue circle with a number in it, too.
-
You will see a list of Changes in the left-hand pane. You should see that
enigma.mlhas been changed since your last commit. -
Click the “+” icon next to Changes to add all the changed files to the staging area of work you want to commit.
-
In the Message textbox, note the key stroke identified to make the commit: probably Control+Enter on Windows or Command+Enter on Mac. In that textbox, type a brief description of the changes you made, such as
Complete AI statement. This will be recorded along with them as a explanation you can read and (if ever needed) could help identify a previous point in your development that you’d like to recover. Press the keystroke when you’re done. -
Click the three dots at the top-right of the left-hand pane in VS Code to open the Git menu. Select Push. This will push your changes up to Cornell GitHub, where they are safely backed up.
Step 3: Index
This step (and those that follow) relies on material up through and including OP section 3.3. We’ll cover that during Week 3, but you’re welcome to get a head start.
Your goal is to implement the function index : char -> int in the file
enigma.ml. But don’t rush in just yet. We’re going to use a methodology
called test-driven development (TDD):
- You write an automated unit test, and check that the test fails.
- Then you write code to make the test pass.
- Then you clean up your code as needed.
Working this way ensure that, by the time you’re done coding, you already have an automated test suite; it’s not just something you write at the end. It also capitalizes on your brain’s desire to be rewarded, because you keep getting the sweet, sweet satisfaction of a test case going from failing to passing. Finally, if you ever happen to break older code as you are writing new code, you will instantly be alerted by your older automated tests suddenly failing.
TDD is a hugely helpful skill to develop as a programmer. Please, practice it now, even if it seems a bit strange at first. Understand: we’re not doing this because you are going to code this way 100% of the time. We’re doing it so that when you get lost and need to code this way, you’ll have built up the skills to do so.
-
Open
src/enigma.mlin VS Code. Read the specification comment forindex. -
Open
test/main.mland locate the unit test forindex. Read it. Invent one new unit test for the function. Add that unit test to the suite.When you save that test file, you might notice that your code gets reformatted. That can cause changes to indentation, spacing, etc. This is intended behavior. We’re using a utility called
ocamlformatto apply some standard formatting conventions to your code. If the behavior is surprising, that’s okay. It is almost certainly doing the right thing. -
In the terminal, run
make test. That will run OUnit and report back about which tests fail versus pass. Verify that the unit tests forindexare currently failing. That’s good! It means you now have a concrete goal: get those two tests to pass. -
Implement
index. Hint 1: your solution should need only about one line of code. Hint 2: read the documentation of theChar.codefunction and think about how it could help. Runmaketo compile and run the test suite. Make sure that both the tests pass.As you are implementing
index, you might decide you want to do some interactive testing of it in utop. For that, runmake utopfrom the terminal. That will launch utop with your code already loaded. The functions fromenigma.mlwill all be available in a module namedEnigma. For example, you’ll need to writeEnigma.indexinstead of justindex. Or, if you runopen Enigma;;in utop, you can start typing justindexinstead ofEnigma.index. Modules are a topic we’ll cover in depth in a couple of weeks, but that’s enough for now.The specification of
indexincludes a precondition:Requires: [c] is an uppercase letter in A..Z.It is your choice whether you want to check whether the precondition holds. See the discussion of defensive programming in OP and PP for why that’s a good idea.Here’s a subtle but important point: Violation of a precondition means a function can do anything at all, even (theoretically) set the computer on fire. So there’s no way the autograder can safely test whether you’ve programmed defensively. On the other hand, if I restated the specification of
indexusing a postcondition, such testing would be possible. For example, I could instead have written:Raises: [Invalid_arg "index"] if [c] is not an uppercase letter in A..Z., with no precondition. Then your function would be required to raise that exception rather than setting on the computer on fire. That would make it safe to test your function. Indeed we’d even want to add unit tests for that exception. The moral of this story is: don’t confuse preconditions with postconditions. -
Add any other unit tests that you would like to have in the suite. Make sure all tests pass.
-
Read the Testing Rubric and Code Quality Rubric at the end of this handout. Consider whether you need to add more test cases. Also take some time to polish the code you just wrote before moving on. Don’t leave this to the end! Human tendencies being what they are, chances are if you don’t do it now, you’ll never do it.
-
Re-run
make checkto double-check that you haven’t changed the names and types of any required functions. Continue to do that after implementing each function below. -
Finally, commit and push your work back to GitHub using the same process as above when you completed the AI statement.
In real-world projects, commit messages are expected to be more than a few brief words. I’m not going to require that here. But, the following is a good template if you want to practice writing commit messages:
Implement Enigma.index
- Replaced release code implementation with my own
- Added a test case
The first line of a good commit message should be just a few words, with the first one capitalized. The words should complete the sentence “If you pulled this commit into your own copy of the repo, this commit would…” You’re informing the reader what value they’re getting from your commit. The second line should be blank. The remaining lines should supply details.
Congratulations! You’ve implemented and tested your first function. It was a admittedly a simple function, but you’ve accomplished so much more: working in a new development environment, using git, and practicing TDD. Take a break for a dance party! ┏(・o・)┛♪┗ (・o・) ┓
Step 4: Enigma Tutorial
Watch this 12 minute video, which introduces the Enigma machine:
(The video ends abruptly; you can watch the second part here. It explains how the Enigma cipher was broken. You don’t need to understand that to complete the assignment, but it’s fun!)
The Enigma was not just a single machine, but a family of closely related machines. So keep in mind that there might be details that differ from one video to another that you watch online, or even between a video and this writeup.
Substitution ciphers. The Enigma machine implemented a substitution cipher, which encrypts a message by substituting one character for another. Such ciphers go back at least as far as Julius Caesar, who used a simple substitution cipher to encrypt military orders. The Caesar cipher specified that each letter in the alphabet would be encrypted using the letter three positions later in the alphabet. For example, ‘A’ would be encrypted as ‘D’, ‘B’ would be encrypted as ‘E’, ‘C’ would be encrypted as ‘F’, and so on. The code wraps around at the end of the alphabet, so ‘X’, ‘Y’ and ‘Z’ would be encrypted as ‘A’, ‘B’, and ‘C’, respectively. Although the Caesar cipher was effective in its time (when few people could read at all), its simple pattern of encrypting letters seems pretty obvious today.
The Enigma cipher. The Enigma machine, shown below, implemented a more complex keyed substitution cipher. The key is a secret presumably known only to the message sender and receiver; it defines the mapping between a plaintext letter and a ciphertext letter. For the Caesar cipher, the key amounts to the “rotate by 3” operation. The key for the Enigma cipher involved many aspects of the physical configuration of the machine; we describe these below.
(Click on that image and any of the images below to link to the website where we found the image.)
After the machine was configured with the key, an operator pressed a letter on a typewriter-like keyboard, which caused a circuit to close and light up a letter on a lampboard aka lightboard, a kind of simple display screen. (Note the two different uses of “key” in the previous sentence: the first refers to a cryptographic key, which is a secret used as an input to encryption and decryption algorithms; the second is in the word “keyboard”, which refers to a button that is pressed.) To encrypt a message, the operator typed the plaintext letters, the ciphertext letters would light up, and the operator would record each ciphertext letter on paper. Likewise, to decrypt a message, the operator typed the ciphertext letters, the plaintext letters would light up, and the operator would record each plaintext letter on paper.
Here is a diagram showing more of the internals of the Enigma machine:
What the diagram calls a wheel is also called a rotor in literature about the Enigma. Henceforth, we will use the term rotor. The rotors were installed next to each other on a spindle, around which they rotated. Here is a picture of three rotors:
You’ll see the alphabet, in order, on each rotor in that picture. Sometimes rotors were labeled with the alphabet, and sometimes (as in the video linked above) they were instead labeled with the integers 1..26. It makes no difference to the operation of the machine which labeling scheme was used: ‘A’ corresponds to 01, ‘B’ to 02, and so forth.
In the wiring diagram above, there is another disc at the far left of the spindle that looks something like a rotor but in fact does not rotate; it is called the reflector. Here is a picture of a reflector:
As the diagram shows, when the operator typed a key on the keyboard, electrical current would flow through the plugboard to the rotors, through the reflector, back through the rotors and plugboard, and light up a letter on the lightboard. The reflector, wheels, and plugboard are all mechanisms that could be installed in different ways into the machine. The way in which they all were collectively installed determined the key that was being used for encryption or decryption.
Here is a brief description of each of the mechanisms; we go into greater detail in later parts of this writeup:
-
Each rotor implements its own, distinct substitution cipher by wires that connect the 26 contacts on its left side with the 26 contacts on its right side. The combination of many rotors together on the spindle creates an even more sophisticated substitution cipher. The original Enigma machines used at the beginning of World War II had three rotors, known by the Roman-numeral designations I, II, and III, which could be placed on the spindle in any order. Later, other rotors with different wirings were manufactured, as were machines that could accept up to five rotors on the spindle.
-
The reflector also implements a substitution cipher, though a less sophisticated one. It has 26 contacts only on its right side, but none on its left. Current that it receives from the right side is wired to be reflected back out that side, but in a different position. The reflector was always inserted at the leftmost side of the spindle. A keypress caused current to flow right-to-left through the rotors, through the reflector, then back through the rotors, left-to-right. The reflector’s substitution pattern caused the entire circuit to be symmetric, meaning that if ‘E’ mapped to ‘Q’ in a particular machine state, then ‘Q’ would likewise be mapped to ‘E’ in that same state. The minor advantage of this design was that the machine did not need separate modes of operation for decryption and encryption. But it turned out to be a major cryptographic weakness, enabling the code to be cracked.
-
The plugboard implements yet another substitution cipher, which simply swaps pairs of letters. Though the wiring of the rotors and reflector was pre-determined by how they were manufactured, the wiring of the plugboard could easily be changed by the operator by plugging cables into ports. The plugboard could have up to 13 cables inserted in it. A cable inserted between two letters caused those letters to be swapped. Even though it seems simpler than a rotor, the plugboard was a major source of cryptographic strength for the Enigma cipher.
-
The static wheel shown above is of no interest to us. (It is effectively a no-op.)
What made the Enigma cipher especially difficult to break was that just after the operator typed a letter, and just before that letter was enciphered, the rotors moved, changing the substitution pattern. Thus, the letter ‘E’ might get mapped to a ‘Q’ the first time it was enciphered, but to a completely different letter each successive time. The rightmost rotor stepped for every letter typed, and the other rotors stepped conditionally based on notches on the rotors: when a rotor stepped past a notch, not only did that rotor step, but also the rotor to its left. This stepping mechanism made the rotors work somewhat like a mechanical odometer.
When a rotor was installed, the operator could manually rotate it to be in any of the 26 possible orientations, based on the 26 labels appearing on the rotor (either ‘A’..’Z’ or 01..26). There was a window in the machine through which the operator could observe the top letter showing on each rotor. The letter that would be on top of the rotor at the point in time when the notch engaged was known as the turnover.
(Note: we are omitting one more Enigma mechanism, the ring setting. You can safely ignore it. Aficionados: assume the ring setting is always ‘A’ for each rotor.)
The key for the Enigma machine comprises all those physical settings: which rotors were installed, the order they were on the spindle, their initial top letters, and which pairs of letters were connected on the plugboard. An Enigma operator would set all those, then encipher a message. That message would be sent (perhaps by Morse code on a radio channel). The receiver would set their Enigma machine with the same key, type in the received ciphertext, and out would come the plaintext.
Check your understanding: Answer these questions. (There’s no need to turn in your answers to be graded.)
- Define the following terms: plaintext, ciphertext, key.
- Describe the physical design of a rotor. How does it differ from a reflector?
- How many rotors could be installed on the spindle? Were they always installed in the same order?
- How does the plugboard affect the cipher?
- What is the top letter of a rotor? What causes it to change? How often does it change?
Don’t worry if you are not yet 100% solid on how the Enigma cipher works. You now know enough to get started building your simulator, and we’ll fill in the gaps as we proceed.
Step 5: Rotors
Our goal in this step is to implement functions that model how current passes through rotors.
Please note that all the examples and unit tests in this section have been carefully double checked; they do not contain any typos.
Rotor wiring. Here is a picture of a disassembled rotor showing some of the internal wiring:
Rather than using complicated diagrams, the wiring of a rotor is often specified
in literature on the Enigma as a 26-character string that is a permutation of
the uppercase letters of the alphabet. For example, EKMFLGDQVZNTOWYHXUSPAIBRCJ
is a wiring specification. Let’s give that a formal definition:
A valid wiring specification is a 26-character uppercase string that is a permutation of the letters of the alphabet.
A wiring specification has nothing to do with the order in which letters are printed on the exterior of the rotor. (Indeed, you will note that all the pictures above show the letters in alphabetical order.) Rather, the specification describes the interior wiring.
To understand a wiring specification, first forget about the letters. They are
really just shorthand for a number—specifically, the index of that letter
in the alphabet. So wiring specification EKMFLGDQVZNTOWYHXUSPAIBRCJ is better
understood as the following list of numbers: 4, 10, 12, 5, 11, … (That’s why
I had you implement index.)
Next, let’s associate each of those numbers with its index in the list:
| index in list | index in alphabet |
|---|---|
| 0 | 4 |
| 1 | 10 |
| 2 | 12 |
| 3 | 5 |
| 4 | 11 |
| … | … |
Finally, think of that table as defining a function \(w\), such that \(w(0) = 4\), \(w(1) = 10\), \(w(2) = 12\), \(w(3) = 5\), \(w(4) = 11\), etc.
The function \(w\) we have thus constructed is what the wiring specification really denotes. A rotor has a left side and a right side, and each side has 26 contacts from which electrical current can enter or leave the rotor. If current enters the rotor from the right side at contact \(i\), it leaves the rotor from the left side at contact \(w(i)\). And if current enters the rotor from the left side at contact \(j\), it leaves the rotor from the right side at contact \(w^{-1}(j)\), where \(w^{-1}\) denotes the inverse of \(w\).
Rotor orientation. There are 26 fixed positions at which current can enter a rotor. (For example, if the plugboard is empty, typing ‘A’ causes current to enter the right-most rotor at position 0, ‘B’ at 1, etc.) But current entering at position \(i\) does not necessarily connect with contact \(i\) on the rotor, because rotors turn around the spindle. That rotation causes an offset between the positions and the contacts.
Recall that the operator can see on the top of the machine, for each rotor, one of the letters that appears as exterior label on the rotor. Also recall that those letters are printed on the rotor in alphabetical order. So the offset of the rotor can be can be determined by looking at the letter that is showing. We call that the top letter of the rotor. Here, for example, is a closeup of a three-rotor Enigma machine in which the top letters are currently RDK:
When a rotor is in its default orientation, with ‘A’ showing as the top letter, the offset of the rotor is 0: current entering at position 0 touches contact 0, position 1 touches contact 1, and so forth. But when ‘B’ is showing as the top letter, the offset of the rotor is 1: current entering from at position 0 touches contact 1, position 1 touches contact 2, and so forth.
Likewise, as current exits from a contact on the opposite side of the rotor, that contact is offset from the actual exit position. For example, when the offset is 1, current flowing to contact 1 exits at position 0, contact 2 exits at position 1, and so forth.
Note that the way the offset works is thus independent of the direction that current is flowing—which makes sense, as the offset is determined by the movement of the rotor around the spindle, not by the flow of current through the rotor.
Examples. Now that we know about how wiring and orientation affect how current flows through a rotor, let’s look at some examples. Here is where a 3D model is worth a million words: the Pringles-can model linked above makes these examples considerably easier to follow. Please note that these examples have been carefully checked to ensure there are no typos.
Example 1. Suppose the rotor has wiring specification
EKMFLGDQVZNTOWYHXUSPAIBRCJ, its top letter is ‘A’, current is flowing from
right to left, and current enters at position 0:
-
Current entering at right-hand position 0 flows to right-hand contact 0 of the rotor, because the rotor’s offset is 0.
-
Current entering right-hand contact 0 flows to left-hand contact 4, because \(w(0) = 4\) for this rotor’s wiring specification.
-
Current exiting left-hand contact 4 flows to left-hand position 4, because the rotor’s offset is 0.
Example 2. But now suppose that the rotor’s top letter is ‘B’. How would Example 1 change?
-
Current entering at right-hand position 0 flows to contact 1 of the rotor, because the rotor’s offset is now 1.
-
Current entering right-hand contact 1 flows to left-hand contact 10, because \(w(1) = 10\).
-
Current exiting left-hand contact 10 flows to left-hand position 9, because the rotor’s offset is 1.
Example 3. Now let’s redo Example 1, but with current flowing from left to right.
-
Current entering at left-hand position 0 flows to left-hand contact 0 of the rotor, because the rotor’s offset is 0.
-
Current entering left-hand contact 0 flows to right-hand contact 20, because \(w^{-1}(0) = 20\) for this rotor’s wiring specification. (That’s a fact we haven’t previously established, but you should be able to work it out yourself from the fact that ‘A’, whose index in the alphabet is 0, appears at position 20 in the wiring specification.)
-
Current exiting right-hand contact 20 flows to right-hand position 20, because the rotor’s offset is 0.
Example 4. Finally let’s redo Example 2, but with current flowing from left to right.
-
Current entering at left-hand position 0 flows to left-hand contact 1 of the rotor, because the rotor’s offset is 1.
-
Current entering left-hand contact 1 flows to right-hand contact 22, because \(w^{-1}(1) = 22\) for this rotor’s wiring specification.
-
Current exiting right-hand contact 22 flows to right-hand position 21, because the rotor’s offset is 1.
It’s time to write some code. Complete the functions map_r_to_l and
map_l_to_r in the starter code. Note that these functions do not perform any
stepping of the rotor; rather, they model how current passes through the rotor
when it’s in a particular orientation. We’ll implement stepping later.
Use test-driven development to implement these functions.
Begin by implementing map_r_to_l. Here is a suggested workflow:
-
Start by adding this very simple test: The wiring specification
ABCDEFGHIJKLMNOPQRSTUVWXYZwith top letter ‘A’ should cause current to flow from position 0 to position 0. Program a unit test, and verify that it fails. -
Now design the algorithm that you want to implement. Note that for this special wiring specification, you actually don’t have to implement anything to account for orientation. So don’t bother with that part of the algorithm yet.
-
Having sketched out the algorithm (verbally, on paper, on a white board, etc.), implement it. Hint 1: take a look at the
getandindexfunctions in theStringmodule. Hint 2: use your ownindexfunction that you implemented above.
- Verify that the unit test passes.
Now repeat that workflow for these progressively harder unit tests:
-
Example 1 above. You still don’t have to account for orientation. If your algorithm is correct, this will pass already.
-
The wiring specification
BACDEFGHIJKLMNOPQRSTUVWXYZwith top letters ‘A’, ‘B’, and ‘C’. That will at last require you to implement code to handle orientation. Hint: You will probably want to implement a modulo operator that differs from the built-in operatormodby always producing a positive remainder. -
Example 2 above. If your algorithms are correct, this will pass already.
Finally, develop map_l_to_r using the same workflow as before, but with
Examples 3 and 4. Hint: You might want to implement a function that is the
inverse of your index function.
Ideas for more unit tests. Finally, add some even harder unit tests. For example, here are the wiring specifications for the three standard rotors used by the 1930 Enigma I:
rotor I: EKMFLGDQVZNTOWYHXUSPAIBRCJ
rotor II: AJDKSIRUXBLHWTMCQGZNPYFVOE
rotor III: BDFHJLCPRTXVZNYEIWGAKMUSQO
Note: To head off any potential confusion, be aware that I, II, and III are the historical names of the rotors, not their order on the spindle. They could be installed in any order—e.g., I-II-III, III-II-I, III-I-II, etc.
You’ll recognize rotor I as the rotor we have used in our examples above. As test cases with those, you could try the following:
-
If
rotor_III = "BDFHJLCPRTXVZNYEIWGAKMUSQO", thenmap_r_to_l rotor_III 'O' 14should be17. -
If
rotor_I = "EKMFLGDQVZNTOWYHXUSPAIBRCJ", thenmap_l_to_r rotor_I 'F' 10should be14.
Rotors I, II, and III are not the only possible rotors. We will test your solution with other historical rotors and shiny new rotors. So, you should create test cases of your own based on other rotors.
Great work! You’ve now implemented a simulation of Enigma rotors. Finish up by
running make check, then committing and pushing your code to your repo.
This is the stopping point for satisfactory scope.
Step 6: Reflector
Reflectors are a lot like rotors. Reflector wiring is specified with the same kind of 26-character string as a rotor, and that string is used to define a function \(w\) as before: when current enters a reflector at input position \(i\), it exits at output position \(w(i)\). But reflectors must satisfy one additional property:
A valid reflector specification must be a valid wiring specification, and it also must hold that if \(w(i) = j\) then \(w(j) = i\).
It is this property that makes the wiring a reflection, in the sense that it swaps \(i\) and \(j\).
Beyond that property, reflectors are actually simpler than rotors. Unlike rotors, the reflector does not rotate, so we don’t have to worry about the orientation or offset of the reflector. Also, current flows only one direction through a reflector, so we don’t have to worry about right-to-left vs. left-to-right.
Use test-driven development to complete the function map_refl in the release
code. Make sure to keep writing failing unit tests first, then implementing code
to make those tests pass. As a first unit test, try
ABCDEFGHIJKLMNOPQRSTUVWXYZ, which it turns out is a valid reflector
specification. Hint: you can implement map_refl in just one line by using
map_r_to_l as a helper.
After that, here are the wiring specifications of the standard reflectors B and C from the 1930 Enigma I:
reflector B: YRUHQSLDPXNGOKMIEBFZCWVJAT
reflector C: FVPJIAOYEDRZXWGCTKUQSBNMHL
Use those to write more unit tests, which if all goes well will already pass.
Reflectors B and C are not the only possible reflectors. We will test your solution with historical reflectors and shiny new reflectors.
Finish up by running make check, then committing and pushing
your code to your repo.
Step 7: Plugboard
This step (and those that follow) additionally relies on material in OP section 3.4. We’ll cover that during Week 3, but you’re welcome to get a head start.
The plugboard swaps pairs of letters before and after current passes through the
rest of the machine. The operator configured the plugboard by inserting cables
between pairs of letters. We’ll represent a plugboard by a list of pairs of
characters. For example, here’s a plugboard in which two cables have been
inserted; one between ‘A’ and ‘Z’, the other between ‘X’ and ‘Y’: [('A', 'Z');
('X', 'Y')]. The order of characters in the pairs should not matter and the
order of pairs in the list should not matter. So that plugboard has an effect
that is equivalent to this one: [('Y', 'X'); ('Z', 'A')].
A valid plugboard is a list of pairs of characters in which only the letters ‘A’..’Z’ appear, and no letter appears more than once.
A valid plugboard therefore may have anywhere from 0 to 13 elements in its list.
The plugboard with no cables inserted would be represented by the empty list,
[].
Complete the function map_plug, continuing to use test-driven development. The
empty plugboard would be a good first test case, followed by a plugboard with
one cable inserted. Hint: write a recursive function that pattern-matches
against the input list.
After that, the two equivalent plugboards above would be good as a basis for several unit tests. Those tests could make sure that the two plugboards do indeed behave the same. Then you could try a plugboard with 13 cables inserted.
Finish up by running make check, then committing and pushing your code to your
repo.
Step 8: Ciphering a character
As before, all the examples and unit tests in this section have been carefully double checked; they do not contain any typos.
Now it’s time to assemble all the functions you’ve written and tested. We’ll first implement the ciphering of just a single character based on the current configuration of the Enigma machine, including the plugboard, the reflector, and the rotors and their orientations. But for now, we continue to leave out any stepping of the rotors.
The cipher algorithm:
-
The operator types a letter \(X\) as input. The design of Enigma limits it to the uppercase letters ‘A’..’Z’. Lowercase letters, numerals, punctuation, spaces, etc. are not supported.
-
(The machine would now step. But we omit stepping for now. We’ll come back to it in a later part of the assignment.)
-
The input letter \(X\) is potentially transformed to a different letter \(Y\) because of the plugboard. The result of that transformation becomes an electrical current at position \(p\), where \(p\) is the index of \(Y\) in the alphabet.
-
That current passes through the rotors, the reflector, and back through the rotors. At each of those, the output position from one rotor becomes the input position to the next. Eventually current exits the rotors at position \(q\).
-
Let \(Z\) be the letter whose index in the alphabet is \(q\). The plugboard potentially transforms \(Z\) into an output letter \(W\), which is the letter that lights up on the lampboard.
An example: Consider the following configuration: reflector B is installed; rotors I, II, and III are installed in that order from left to right; the top letter of every rotor is ‘A’; and the plugboard is empty. Then input letter ‘G’ would be enciphered to output letter ‘P’, as follows:
-
The input letter is ‘G’. The plugboard is empty, so no transformation occurs. Current begins flowing into the rotors at the position of the index of ‘G’, which is 6.
-
Rotor III with top letter ‘A’ maps current from right-hand position 6 to left-hand position 2.
-
Rotor II with top letter ‘A’ maps current from right-hand position 2 to left-hand position 3.
-
Rotor I with top letter ‘A’ maps current from right-hand position 3 to left-hand position 5.
-
Reflector B maps current from position 5 to position 18.
-
Rotor I with top letter ‘A’ maps current from left-hand position 18 to right-hand position 18.
-
Rotor II with top letter ‘A’ maps current from left-hand position 18 to right-hand position 4.
-
Rotor III with top letter ‘A’ maps current from left-hand position 4 to right-hand position 15.
-
Current flows out of the rotors at position 15, which is the index of ‘P’. The plugboard is empty, so no transformation occurs. The output letter is ‘P’.
Expanding on that example, the following table shows how any individual character would be enciphered in that machine configuration. You’ll see, for example, that input letter ‘G’ lines up with output letter ‘P’, as we just explained in detail:
input letter: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
V
output letter: UEJOBTPZWCNSRKDGVMLFAQIYXH
Data types. The release code defines a config type to represent the
configuration of the Enigma machine: the reflector, the rotors that are
installed and the order they are on the spindle, and the top letter of each
rotor.
A valid configuration is a configuration in which all the wiring specifications are valid, the plugboard is valid, and the turnovers and top letters are all in ‘A’..’Z’.
Note that there might be any number of rotors in a valid configuration: perhaps 0, perhaps the standard 3, perhaps more. And the rotors need not be distinct; there could be duplicates.
Here are a couple notes on the config type:
-
The
reflfield is the wiring specification of the reflector. -
The field named
turnoverin therotortype won’t actually be used in any meaningful way in this part of the assignment; you can ignore it or just set it to an arbitrary letter for now. -
The order of elements in the
rotorslist in theconfigtype represents the order in which the rotors are installed on the spindle, from left to right. So, the head of the list is the leftmost rotor on the spindle, and the last element of the list is the rightmost rotor on the spindle. For example, if there are two rotors installed, and if the left-most rotor on the spindle (i.e., the rotor next to the reflector) isr, and if the right-most rotor on the spindle (i.e., the rotor next to the static wheel) isq, then that configuration would be represented by the list[r; q].
Function to implement. Complete the function cipher_char that computes how
the Enigma machine ciphers a single letter. Note that this function still does
not perform any stepping of the rotors; rather, it models how the machine
transforms an input letter into an output letter when all the rotors are in a
particular orientation.
Continue to use test-driven development. Here are some ideas for unit tests:
-
When the plugboard is empty, the list of rotors is empty, and the reflector is wired as
ABCDEFGHIJKLMNOPQRSTUVWXYZ, thencipher_charwill behave like the identity function: ‘A’ ciphers to ‘A, ‘B’ to ‘B’, etc. -
The example worked above gives you many possible test cases.
-
Using the Pringles-can model, or simply pencil and paper, you could work out many more test cases yourself.
Hint: consider writing two helper functions,
map_rotors_r_to_l : oriented_rotor list -> int -> int
map_rotors_l_to_r : oriented_rotor list -> int -> int
which compute the output position that results when current enters and passes through an entire list of rotors. They will be recursive functions over the list. Can you factor out a common helper function from them so that there isn’t any duplicated code?
Caution: Something confusing will happen if you attempt to use an
online Enigma simulator to construct test cases for your cipher_char.
Those simulators implement stepping, but cipher_char omits it.
So you might have to use different top letters to get the results you expect
with those simulators. For the details, you’ll have to keep reading.
Finish up by running make check, then committing and pushing
your code to your repo.
Well done! You’ve accomplished a lot. Give yourself some time to recharge. Have you done anything from this list? Some of them are harder to do in the midst of pandemic, sadly.
This is the stopping point for good scope. It’s totally fine to end here. Steps 9 and 10 are worth only 5 points for the functions, and a couple more points for testing, but it will take a fair amount of work. So:
- If you’d rather work on other courses or just chill, that’s completely okay.
- If you’re having fun and want to dig deeper, that’s awesome.
- If you’re not sure, how about this: try implementing just “Rule 1” in Step 9 below, then skipping ahead to Step 10. Even without “Rule 2” and “Rule 3”, you’ll get some partial credit toward excellent scope. More importantly, you’ll also learn more about functional programming—in particular, how to model what seems like mutable state in an idiomatic functional way.
- You can always just write the test cases but not the functions, and get full credit for testing.
Let’s go!
Step 9: Stepping
The intuition of stepping is that the rotors behave mostly like the numbers on an odometer: the rightmost completes a full revolution, and in so doing, causes the next rotor to its left to take a single step, and so forth. But the stepping system on the Enigma is a little more complicated. Recall that each rotor has a turnover, which is the top letter that is showing when a notch is reached in the rotor. Those notches cause the rotors to step according to the following rules:
Rule 1: Just before each letter is enciphered, the rightmost rotor always steps.
Rule 2: Just before each letter is enciphered, if the top letter of any rotor \(r\) is its turnover and there exists another rotor \(r’\) directly to the left of \(r\), then both \(r\) and \(r’\) step. If there is no rotor to the left of \(r\) —that is, if \(r\) is the left-most rotor— then this rule does not apply. If the top letter of \(r’\) is also at its turnover just before enciphering, then this rule applies independently to \(r’\) too.
Rule 3: No rotor steps more than once per letter enciphered, even if the above rules could be construed as suggesting that a rotor would step twice.
Some Enigma machines or rotors were built with different stepping rules; in case of discrepancy, the rules above are in effect for this assignment.
Here are the turnovers of some of the historical rotors:
turnover of I: Q
turnover of II: E
turnover of III: V
Example 1. Suppose the installed rotors are III-II-I (i.e., III is the left-most rotor, then II, then I is the right-most), starting in orientations where the top letters are KDO. Then as each character is enciphered, they would step as follows: KDO, KDP, KDQ, KER, LFS, LFT, LFU, …
Example 2. Suppose the installed rotors are again III-II-I, starting with top letters VDP. Then as each character is enciphered, they would step as follows: VDP, VDQ, VER, WFS, WFT, …
There are no typos in those examples. Really. 😀
Function to implement. Complete the function step, which computes how the
Enigma machine steps just before enciphering a letter. Its type
config -> config is worth considering: instead of mutating state, we produce a
new value out of an old one. This is a hallmark of the functional style of
programming.
This function is the most algorithmically complex part of the assignment. Continue to use test-driven development. For unit tests, first implement just rule 1; you should have no trouble inventing some tests for that. Then add in rules 2 and 3, using the examples we supplied above as unit tests.
Finish up by running make check, then committing and pushing your code to your
repo.
Step 10: Ciphering a string
At last, it’s time to encipher an entire string. The order of operations as they occur on the Enigma machine is as follows:
- The operator types a letter.
- The machine immediately steps to a new configuration.
- The letter is enciphered according to that new configuration.
- The enciphered letter is lit on the lampboard.
- The operator repeats until finished.
Complete the function cipher, which computes how the Enigma machine ciphers a
string. Continue to use test-driven development.
It is up to you to invent test cases for this function. There are many online simulators that could help you, such as these: 1, 2. We don’t know of any that allow arbitrary rotor wirings or numbers of rotors, though. Just do your best!
Actually, here is one test case for you: use reflector B, rotors I, II, and III (installed in that order—I is leftmost, then II, then III is rightmost), with starting top letters F, U, and N, and a plugboard with a single cable connecting ‘A’ and ‘Z’. Encipher YNGXQ; it should produce the name of a pretty good programming language.
Finish up by running make check, then committing and pushing your code to your
repo.
YAAAAAAAY! All done!
Rubric
- 25 points: submitted and compiles
- 25 points: satisfactory scope
- 25 points: good scope
- 10 points: testing
- 10 points: code quality
- 5 points: excellent scope
Scope Rubric. Your solution earns points by passing the grading suite’s OUnit test cases on the functions identified above. Each test case is worth some number of points, so it’s definitely possible to get partial credit on individual functions that way. Sorry, but we can’t provide a more detailed breakdown in advance.
Testing Rubric. For this first assignment, your test suite needs to satisfy these requirements:
-
Each required function has at least five unit tests. These required tests may not be commented out, and it is okay if they do not pass. That magic number of five is really just about building proficiency with OUnit. Later in the semester we’ll introduce a more principled way of determining how many tests suffice.
It’s fine to use any of the test cases identified above in the handout or shipped in the release code as part of your five per function. The required functions are those that shipped in the release code with a
raise (Failure "Unimplemented"), namely,index,map_r_to_l,map_l_to_r,map_refl,map_plug,cipher_char,step, andcipher. That’s eight functions, so you need to write 40 total test cases. You should do your best to invent test cases that are not redundant, that explore boundary cases, etc., but the graders are not going to assess that on this assignment. You do not need to add tests for helper functions you introduce yourself. -
Each unit test has a descriptive name that gives the reader an idea of what the test is about that is more specific than just the name of the function being tested. The name of a test is the string bound to it with OUnit’s
>::operator. For example, the name of the provided test in the release code is"index of A is 0". That’s a good name; a name such as"index0"would not be sufficient. Think of the name as a comment that would help hypothetical future maintainers of the test suite to understand the test. For more examples, see this production testing code in a major OCaml library. None of the names there is particularly long, but they all give a hint as to what the test does. -
Each unit test is constructed by a helper function that minimizes code duplication. The
index_testfunction is an example of the sort of function you should write. -
The suite must successfully run to completion with
make test. It’s okay if the OUnit report contains some test cases that do not pass. But OUnit must run to completion and produce that report about how many test cases passed, failed, etc.
Your test suite will be graded independently from the rest of your
implementation. So you can gain or lose points for it regardless of which
required functions you complete. For example, if you don’t write cipher or
test cases for it, you will lose points both from the excellent scope autograder
and from the manual grading of your test cases. But you could provide those test
cases and get points for them even if you don’t write the function itself. Part
of the reason we grade this way is to incentivize you to write test cases before
writing (or finishing) a function.
Code Quality Rubric. Your code should adhere to the OCaml Community Guidelines. The graders will assess the following aspects of your source code:
-
The programmer’s name and NetID are supplied in the designated place in the Academic Integrity comment.
-
Identifier names are clear and consistent. They use
snake_case, notcamelCase. Either they should be very short and idiomatically indicative (e.g.,lstfor lists;handtfor head and tail), or they should be longer and self-documenting (e.g.,offsetorrotor; notfooorvar1). See “How to choose identifiers” and following paragraphs in the Community Guidelines. -
Individual functions should be at most about 10 or 20 lines and should never overflow what is visible on the screen. The human eye should be able to take in the entire function at once.
You are also responsible for the following aspects of your code quality, but, they will be handled for you automatically when VS Code and OCamlFormat cooperate to reformat your source code on save. If OCamlFormat overrides the OCaml Community Guidelines in any way, then OCamlFormat wins; we won’t penalize you for its choices.
-
White space (the space character and empty lines) is used effectively.
-
Indentation of all constructs follows the OCaml Community guidelines or OCamlFormat’s own choices.
-
Parentheses are used only where necessary. See “when to use parentheses within an expression” in the Community Guidelines for some hints.
-
No line is wider than 80 columns. Not only does the OCaml community agree on that, so does Google. But, see the exception to this rule below.
-
No tabs characters (ASCII character 9) are used, only spaces. Note that the tab character and the tab key are not the same thing. The configuration instructions we gave for VS Code set up the tab key to enter two spaces.
And here are a few more aspects of your code quality that the graders will not assess but you should still consider:
-
Any helper functions you add should have specification comments.
-
You should not use
List.hd,List.tl, orList.nth. These functions raise exceptions and are the cause of many coding errors. Though there are times their usage is reasonable, on A1 I strongly urge you to avoid them altogether, especiallynth. -
There should be no compiler warnings about pattern matching errors. Such warnings are almost certainly a result of buggy code that will cause you to lose points from the autograder.
-
A deeply nested chain of function applications such as
f4 (f3 (f2 (f1 x)))should be rewritten with the pipeline operator asx |> f1 |> f2 |> f3 |> f4. -
Deeply nested
matchandifexpressions should be avoided. Not only are they hard for a human to read, but they are also hard for a human to correctly parse. The way you understand it might not be the way OCaml does. Try adding abegin..endor parentheses around the inner expression. If OCamlFormat removes those delimiters, it’s fine. But if it doesn’t, there’s probably a mis-parse it’s trying to prevent.
As a special exception, you may violate the 80-column rule for long string literals in test cases. But even those can and should be avoided by using a multi-line string literal:
let s = "A camel is an even-toed ungulate in the genus Camelus that bears \
distinctive fatty deposits known as humps on its back. Camels have \
long been domesticated and, as livestock, they provide food (milk and \
meat) and textiles (fiber and felt from hair). Camels are working \
animals especially suited to their desert habitat and are a vital \
means of transport for passengers and cargo. There are three \
surviving species of camel. The one-humped dromedary makes up 94% of \
the world's camel population, and the two-humped Bactrian camel makes \
up 6%. The Wild Bactrian camel is a separate species and is now \
critically endangered."
(source: Wikipedia)
Submission
Set the hours_worked variable at the end of enigma.ml to the number of
person-hours you spent working on this assignment. Don’t include the time you
spent installing OCaml.
Run make zip to construct the ZIP file you need to submit on CMS. Our
autograder needs to be able to find the files you submit inside that ZIP without
any human assistance, so:
→ DO NOT use your operating system’s graphical file browser to construct the ZIP file. ←
Use only the make zip command we provide. Mal-constructed ZIP files will
receive a zero from the autograder. If CMS says your ZIP file is too large, it’s
probably because you did not use make zip to construct it; the file size limit
in CMS is plenty large for properly constructed ZIP files.
Ensure that your solution passes make finalcheck. That will do the same as
make check, as well as two other things. First, it will check to make sure
you’ve set hours_worked. Second, it will compute a short “fingerprint” of your
submission called an MD5 sum. That will be useful when you submit to CMS.
Windows users only: now you need to get your code out of the WSL filesystem
back into Windows, so that you can upload it to CMS with your web browser. You
have the same three options as at the beginning of the assignment: (i) \\wsl$
in the File Explorer, (ii) a cp command, or (iii) drag-and-drop from VS Code.
Now everybody: Submit your enigma.zip on CMS. Double-check that the
MD5 sum is what you expected. Re-download your submission from CMS and
double-check before the deadline that the contents of the file are what you
intended.
Late submissions: Please review the syllabus for policies about late submissions, slip days, extensions, etc.
Congratulations! You’ve cracked the Enigma!
Acknowledgement: Adapted from Prof. David Reed (Creighton University).