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 80 — Vigenère Cipher
This is project 80 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 player's answers are piped in:
e
KEY
Hello, World!
Run it with:
ek9 vigenerecipher.ek9 < input.txt
It prints:
Vigenère Cipher, by Al Sweigart al@inventwithpython.com
The Vigenère cipher is a polyalphabetic substitution cipher that was
powerful enough to remain unbroken for centuries.
Do you want to (e)ncrypt or (d)ecrypt?
Please specify the key to use.
It can be a word or any combination of letters:
Enter the message to encrypt.
Encrypted message:
Rijvs, Uyvjn!
The program
#!ek9 defines module bigbook.vigenerecipher defines constant Symbols <- "ABCDEFGHIJKLMNOPQRSTUVWXYZ" defines type CipherMode Encrypt Decrypt List of Integer 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; unset only if the input ends first. nextKey() <- rtn as String: String() while not rtn? and not input.ended() stdout.println(say.askKey()) rtn: keyFrom(input.next()) //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 itself is computed, not worded: it is shown as the cipher made it. showTranslated() -> mode as CipherMode translated as String stdout.println(say.translatedHeading(mode)) stdout.println(translated) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: the cipher, with no input or output. //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 letters only, in capitals; matches is a whole-string match, so this accepts //only a non-empty run of letters, and anything else leaves the key unset. keyFrom() as pure -> reply as String <- rtn as String: String() if key <- reply.trim().upperCase() then key matches /[A-Z]+/ rtn: key //One shift per key letter, already signed for the direction, so translate only adds. keyShifts() -> key as String mode as CipherMode <- rtn as List of Integer: List() of Integer direction <- mode == CipherMode.Encrypt <- 1 : -1 for letter in key rtn += Symbols.indexOf(letter) * direction isSymbol() as pure -> character as Character <- rtn as Boolean: Symbols.indexOf(character.upperCase())? //A letter moves by shift and keeps its case; anything else passes through unchanged. shiftedLetter() as pure -> character as Character shift as Integer <- rtn as String: $character if position <- Symbols.indexOf(character.upperCase()) wrapped <- (position + shift) mod length Symbols letter <- Symbols.subString(wrapped, wrapped + 1) rtn: character.isUpperCase() <- letter : letter.lowerCase() //The key advances only on letters, so spaces and punctuation do not use up key letters. translate() -> message as String key as String mode as CipherMode <- rtn as String: "" shifts <- keyShifts(key, mode) keyIndex <- 0 for character in message rtn += shiftedLetter(character, shifts.getOrDefault(keyIndex mod length shifts, 0)) if isSymbol(character) keyIndex += 1 defines function //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() `Vigenère Cipher, by Al Sweigart al@inventwithpython.com The Vigenère cipher is a polyalphabetic substitution cipher that was powerful enough to remain unbroken for centuries.` 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. It can be a word or any combination of letters:` askMessageToEncrypt() "Enter the message to encrypt." askMessageToDecrypt() "Enter the message to decrypt." //"Encrypt" + "ed": the enumeration's own name builds the heading, for either mode. translatedHeading() -> mode as CipherMode `${mode}ed message:` defines program //Vigenère Cipher (Big Book project 80). Encrypts or decrypts a message with a key of letters. //The game decides; the Console asks and tells, in the player's words. VigenereCipher() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) console.showIntroduction() mode <- console.nextMode() key <- console.nextKey() if mode? and key? if message <- console.nextMessage(mode) console.showTranslated(mode, translate(message, key, mode))
About this port
- Behaviour: interactive, as in the book: (e)ncrypt or (d)ecrypt, a letters-only key, a
message, then an "Encrypted message:" or "Decrypted message:" heading. The golden encrypts
Hello, World!with keyKEYtoRijvs, Uyvjn!; decrypting that givesHello, World!back. - Idioms:
CipherModeis an enumeration parsed bymodeOf(unset for any other reply).- The key is checked with one regular expression,
key matches /[A-Z]+/(matchesis a whole-string match), in the guard ofkeyFrom, instead of a loop over its characters. keyShiftsturns the key into shifts already signed for the direction, so the per-letter work is one addition. Positions come fromSymbols.indexOf, which is unset for anything that is not a letter: the same guard finds the position and lets punctuation through.modis never negative, so one expression wraps both encryption and decryption.- The key advances only on letters, as in the book. Case is kept with
isUpperCase(). - Every input loop also stops at the end of input (
LineInput).
- Omitted: the optional
pyperclipclipboard copy.