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