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 33 — Hacking Minigame

This is project 33 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=1. The player's answers are piped in:

CAPTURE
PICTURE
MACHINE

Run it with:

BIGBOOK_SEED=1 ek9 hacking-minigame.ek9 < input.txt

It prints:

Hacking Minigame, by Al Sweigart al@inventwithpython.com
Find the password. Each guess tells you how many letters are in the right place.
Candidate passwords:
DIAGRAM CAPTURE PROGRAM JOURNEY PICTURE KITCHEN MACHINE PENGUIN MONITOR LIBRARY DIAMOND TEACHER 
Enter password (4 tries left):
Access Denied (2/7 correct)
Enter password (3 tries left):
Access Denied (2/7 correct)
Enter password (2 tries left):
A C C E S S   G R A N T E D

The program

#!ek9
defines module bigbook.hacking

  defines constant

    Tries <- 4
    WordLength <- 7
    CandidateCount <- 12

  defines type

    List of String
    shuffled of String

  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())

      //The candidates are the game's data, shown as they are, each followed by a space.
      showCandidates()
        -> candidates as List of String
        stdout.println(say.candidates())
        shown <- cat candidates | map with spaced | collect as String
        stdout.println(shown)

      //A guess that is one of the candidates, or unset when the input ends.
      nextGuess()
        ->
          triesLeft as Integer
          candidates as List of String
        <- rtn as String: String()

        while not rtn? and not input.ended()
          stdout.println(say.askPassword(triesLeft))
          if entry <- input.next()
            guess <- entry.upperCase().trim()
            if isCandidate(guess, candidates)
              rtn: guess
            else
              stdout.println(say.notACandidate())

      showGranted()
        stdout.println(say.granted())

      showDenied()
        -> inPlace as Integer
        stdout.println(say.denied(inPlace))

      showOutOfTries()
        -> secret as String
        stdout.println(say.outOfTries(secret))

      override operator ? as pure
        <- rtn as Boolean: true

  defines function

    //THE GAME: its rules and its words to guess, with no input or output.

    //The game's data: the words a secret is drawn from, which the player's guess is compared with.
    passwords() as pure
      <- rtn as List of String: ["MONITOR", "CONTAIN", "PROGRAM", "LIBRARY", "NETWORK", "MACHINE",
        "DIAGRAM", "PENGUIN", "JOURNEY", "TEACHER", "KITCHEN", "PICTURE", "DIAMOND", "HUSBAND", "CAPTURE"]

    //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 candidates on screen: the first few of the word list in a random order.
    candidatesFrom()
      ->
        words as List of String
        rng as Random
      <- rtn as List of String: List() of String

      shuffle <- () is shuffled of String as function
      rtn: cat shuffle(words, rng) | head CandidateCount | collect as List of String

    isCandidate() as pure
      ->
        guess as String
        candidates as List of String
      <- rtn as Boolean: candidates contains guess

    //The clue for a wrong guess: how many positions hold the same letter in both words. A
    //pure function may not count with +=, so it compares the first letters and adds the
    //count for the rest of both words.
    lettersInPlace() as pure
      ->
        secret as String
        guess as String
      <- rtn as Integer: 0

      if not secret.empty()
        firstMatches <- #< secret == #< guess <- 1 : 0
        rtn: firstMatches + lettersInPlace(secret.subString(1), guess.subString(1))

  defines function

    //THE FLOW of the tries, as the book's for loop takes them; the program calls it.

    //One round per try: the right guess ends the game, a wrong one gives a clue and plays the
    //next round with one try fewer, so the game ends without a flag to say it was won. The
    //end of input ends it too, with no guess.
    playRound()
      ->
        triesLeft as Integer
        secret as String
        candidates as List of String
        console as Console

      if triesLeft == 0
        console.showOutOfTries(secret)
      else if guess <- console.nextGuess(triesLeft, candidates)
        if guess == secret
          console.showGranted()
        else
          console.showDenied(lettersInPlace(secret, guess))
          playRound(triesLeft: triesLeft - 1, secret: secret, candidates: candidates, console: console)

  defines function

    spaced() as pure
      -> word as String
      <- rtn as String: `${word} `

    //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. The passwords are not here: they are the game's data.
  defines text for "en"

    Say

      introduction()
        `Hacking Minigame, by Al Sweigart al@inventwithpython.com
Find the password. Each guess tells you how many letters are in the right place.`

      candidates()
        "Candidate passwords:"

      askPassword()
        -> triesLeft as Integer
        `Enter password (${triesLeft} tries left):`

      notACandidate()
        "Please enter one of the candidate passwords."

      granted()
        "A C C E S S   G R A N T E D"

      denied()
        -> inPlace as Integer
        `Access Denied (${inPlace}/${WordLength} correct)`

      outOfTries()
        -> secret as String
        `Out of tries. Secret password was ${secret}.`

  defines program

    //Hacking Minigame (Big Book project 33). The Fallout password game: find the secret word among
    //the candidates; each wrong guess says how many letters are in the right place. The game
    //decides; the Console asks and tells, in the player's words. The golden replays a seeded game
    //(seed.txt).
    Hacking()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      candidates <- candidatesFrom(passwords(), rng)
      secret <- candidates.getOrDefault(rng.next(0, length candidates - 1), String())

      console.showIntroduction()
      console.showCandidates(candidates)
      playRound(triesLeft: Tries, secret: secret, candidates: candidates, console: console)

About this port