The Big Book of Small EK9 Programs
Adapted from Al Sweigart's The Big Book of Small Python Projects: 81 Easy Practice Programs (No Starch Press, 2021; copyright © 2021 Al Sweigart), which he makes freely available under a Creative Commons Attribution-NonCommercial-ShareAlike licence at inventwithpython.com/bigbookpython. Changes were made: each program was ported from Python to EK9 and remodelled in EK9's idioms.
Project 66 — Simple Substitution Cipher
This is project 66 of the book, ported to EK9. Its chapter is still being written; until then, this page shows the port itself: a run of it, the whole program, and how it keeps to the original.
A sample run
The program is random, so the run fixes it by setting BIGBOOK_SEED=3. The player's answers are piped in:
e
RANDOM
The quick brown fox jumps over the lazy dog.
Run it with:
BIGBOOK_SEED=3 ek9 simplesubstitutioncipher.ek9 < input.txt
It prints:
Simple Substitution Cipher, by Al Sweigart
Do you want to (e)ncrypt or (d)ecrypt?
Please specify the key to use.
Or enter RANDOM to have one generated for you.
Using key: UQKXDWRLMANBGFVPYSJTIECOZH
Enter the message to encrypt.
Tld yimkn qsvcf wvo aigpj veds tld buhz xvr.
The program
#!ek9 defines module bigbook.simplesubstitutioncipher defines constant Letters <- "ABCDEFGHIJKLMNOPQRSTUVWXYZ" defines type CipherMode Encrypt Decrypt List of Character shuffled of Character defines class //THE input idiom of this book, copied unchanged from lib/LineInput.ek9 (the reasons are //there): read with next(), and every input loop also stops when ended() is true. LineInput stdin as Stdin: Stdin() inputEnded as Boolean: false next() <- rtn as String: stdin.next() if not rtn? inputEnded: true ended() as pure <- rtn as Boolean: Boolean(inputEnded) override operator ? as pure <- rtn as Boolean: true //THE INPUT AND OUTPUT: everything the player sees or types goes through here, worded by Say. Console stdout as Stdout: Stdout() input as LineInput: LineInput() say as Say: Say("en") Console() -> words as Say say: words showIntroduction() stdout.println(say.introduction()) //Asks until the answer is a mode; unset only if the input ends first. nextMode() <- rtn as CipherMode: CipherMode() while not rtn? and not input.ended() stdout.println(say.askMode()) rtn: modeOf(input.next()) if not rtn? and not input.ended() stdout.println(say.notAMode()) //Asks until the answer is a key, or the word for a random one, which the game makes; //unset only if the input ends first. nextKey() -> rng as Random <- rtn as String: String() while not rtn? and not input.ended() stdout.println(say.askKey()) if entry <- input.next() if entry.trim().upperCase() == say.random() rtn: randomKey(rng) else rtn: keyFrom(entry) if not rtn? stdout.println(say.notAKey()) showKey() -> key as String stdout.println(say.usingKey(key)) //The message to translate; unset only if the input ends first. nextMessage() -> mode as CipherMode <- rtn as String: String() stdout.println(mode == CipherMode.Encrypt <- say.askMessageToEncrypt() : say.askMessageToDecrypt()) rtn: input.next() //The translation is computed, not worded: it is shown as the cipher made it. showTranslated() -> translated as String stdout.println(translated) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: the cipher, with no input or output. //shuffled: a new List in random order (see lib/Shuffle.ek9). Inlined because each //port is a single compilation unit. Uses the Random it is given. shuffled() of type T as open -> source as List of T rng as Random <- rtn as List of T: List() of T work as List of T: List() of T work += source for pass in source size as Integer: length work pick as Integer: rng.next(0, size - 1) remaining as List of T: List() of T cursor as Integer: 0 for element in work if cursor == pick rtn += element else remaining += element cursor += 1 work: remaining //The first letter of the reply picks the mode; any other reply leaves it unset. modeOf() as pure -> reply as String <- rtn as CipherMode: CipherMode() initial <- reply.trim().lowerCase().first() if initial == 'e' rtn: CipherMode.Encrypt else if initial == 'd' rtn: CipherMode.Decrypt //A key is the alphabet rearranged, so its letters in order are the alphabet itself: one //comparison rules out a missing, repeated or non-letter character. Anything else leaves //the key unset. keyFrom() as pure -> reply as String <- rtn as String: String() if key <- reply.trim().upperCase() then sortedLetters(key) == Letters rtn: key sortedLetters() as pure -> text as String <- rtn as String: cat text | sort | collect as String randomKey() -> rng as Random <- rtn as String: String() shuffle <- () is shuffled of Character as function letters <- cat Letters | collect as List of Character rtn: cat shuffle(letters, rng) | collect as String //The letter at a position in one alphabet becomes the letter at that position in the //other, keeping its case; indexOf is unset for a non-letter, which passes through. substitutedLetter() as pure -> character as Character fromLetters as String toLetters as String <- rtn as String: $character if position <- fromLetters.indexOf(character.upperCase()) letter <- toLetters.subString(position, position + 1) rtn: character.isUpperCase() <- letter : letter.lowerCase() //Decrypting is encrypting with the two alphabets swapped. translate() -> message as String key as String mode as CipherMode <- rtn as String: "" fromLetters <- mode == CipherMode.Encrypt <- Letters : key toLetters <- mode == CipherMode.Encrypt <- key : Letters for character in message rtn += substitutedLetter(character, fromLetters, toLetters) defines function //THE source of randomness, copied unchanged from lib/RandomSource.ek9: SystemRandom(), or //SeededRandom(seed) when BIGBOOK_SEED holds a number (run-and-diff.sh sets it from seed.txt). randomSource() <- rtn as Random: SeededRandom(Integer(EnvVars().get("BIGBOOK_SEED"))) ?: SystemRandom() //THE language of the words, copied unchanged from lib/Language.ek9: BIGBOOK_LANG when it //is set (fr, zh, ...), otherwise en. language() <- rtn as String: EnvVars().get("BIGBOOK_LANG") ?: "en" //THE WORDS: every word the player sees. defines text for "en" Say introduction() "Simple Substitution Cipher, by Al Sweigart" askMode() "Do you want to (e)ncrypt or (d)ecrypt?" notAMode() "Please enter the letter e or d." askKey() `Please specify the key to use. Or enter RANDOM to have one generated for you.` //The word the player types for a generated key, in capitals as the reply is compared. random() "RANDOM" notAKey() `The key must use each of the ${length Letters} letters exactly once.` usingKey() -> key as String `Using key: ${key}` askMessageToEncrypt() "Enter the message to encrypt." askMessageToDecrypt() "Enter the message to decrypt." defines program //Simple Substitution Cipher (Big Book project 66). Encrypts or decrypts a message with a key //the player types, or a random one. The game decides; the Console asks and tells, in the //player's words. The golden replays a seeded game (seed.txt). SimpleSubstitutionCipher() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) rng <- randomSource() console.showIntroduction() mode <- console.nextMode() key <- console.nextKey(rng) if mode? and key? console.showKey(key) if message <- console.nextMessage(mode) console.showTranslated(translate(message, key, mode))
About this port
- Behaviour: interactive, as in the book: (e)ncrypt or (d)ecrypt, a key (or
RANDOMto generate one), then the message. Case is kept and non-letters pass through. A typed key must use each of the 26 letters exactly once, and the prompt repeats until it does. - Idioms:
- A key is a 26-letter
String, the alphabet rearranged.substitutedLetterfinds a letter's position withindexOfin one alphabet and takes the letter at that position from the other, so decrypting is encrypting with the two alphabets swapped.indexOfis unset for a non-letter, so the same guard lets punctuation through. keyFromis one comparison: a key's letters, sorted with a stream, must equal the alphabet, which rules out a missing, repeated or non-letter character at once.- The random key shuffles the alphabet's characters with the inlined
shuffled of type T(lib/Shuffle.ek9) and streams them back into aStringwithcollect as String. CipherModeis an enumeration parsed bymodeOf.- Every input loop also stops at the end of input (
LineInput).
- A key is a 26-letter