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 34 — Hangman and Guillotine

This is project 34 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:

E
A
N
T

Run it with:

BIGBOOK_SEED=1 ek9 hangman-and-guillotine.ek9 < input.txt

It prints:

Hangman, by Al Sweigart al@inventwithpython.com
 +--+
 |  |
    |
    |
    |
=====
The category is: Animals
Missed letters: none yet
_ _ _
Guess a letter.
 +--+
 |  |
 O  |
    |
    |
=====
The category is: Animals
Missed letters: E 
_ _ _
Guess a letter.
 +--+
 |  |
 O  |
    |
    |
=====
The category is: Animals
Missed letters: E 
A _ _
Guess a letter.
 +--+
 |  |
 O  |
    |
    |
=====
The category is: Animals
Missed letters: E 
A N _
Guess a letter.
Yes! The secret word is: ANT
You have won!

The program

#!ek9
defines module bigbook.hangman

  defines constant

    //The game's data: the category its words are drawn from.
    Category <- "Animals"

  defines type

    List of String
    List of Character
    Set of Character

    //Why a guess is not accepted, in the order the book checks.
    GuessProblem
      NotOneCharacter
      AlreadyGuessed
      NotALetter

  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 gallows for the misses so far, then what the player knows: the category, the missed
      //letters in the order they were guessed, and the word with a blank for each letter not found.
      showGallows()
        ->
          secret as String
          missed as List of Character
          correct as Set of Character

        cat gallowsFor(length missed) > stdout
        stdout.println(say.category(Category))
        if missed.empty()
          stdout.println(say.noneMissed())
        else
          letters <- cat missed | map with followedBySpace | collect as String
          stdout.println(say.missed(letters))
        stdout.println(blanksFor(secret, correct))

      //Asks until the game accepts a letter; unset only if the input ends first.
      nextGuess()
        ->
          missed as List of Character
          correct as Set of Character
        <- rtn as Character: Character()

        while not rtn? and not input.ended()
          stdout.println(say.askLetter())
          if entry <- input.next()
            guess <- entry.upperCase().trim()
            if problem <- problemWith(guess, missed, correct)
              showProblem(problem)
            else
              rtn: guess.first()

      showWon()
        -> secret as String
        stdout.println(say.won(secret))

      showLost()
        -> secret as String
        stdout.println(say.lost(secret))

      ended() as pure
        <- rtn as Boolean: input.ended()

      private showProblem()
        -> problem as GuessProblem
        switch problem
          case GuessProblem.NotOneCharacter
            stdout.println(say.notOneLetter())
          case GuessProblem.AlreadyGuessed
            stdout.println(say.alreadyGuessed())
          case GuessProblem.NotALetter
            stdout.println(say.notALetter())
          default
            //problemWith returns a set problem or none; the compiler requires this branch (E07320).
            stdout.println(say.notALetter())

      //The word as far as it is known: each letter not yet guessed is a blank.
      private blanksFor()
        ->
          secret as String
          correct as Set of Character
        <- rtn as String: String()

        shown <- List() of String
        for letter in secret
          shown += correct.contains(letter) <- $letter : "_"
        rtn: cat shown | join with spaced | collect as String

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

  defines function

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

    //The picture IS data, as the book's HANGMAN_PICS is: one picture per number of misses, so the
    //number of pictures sets how many misses a player has, and a different picture needs no change
    //to the logic. It does not translate, so it is not text.
    gallowsStages() as pure
      <- rtn as List of List of String: [
        [" +--+", " |  |", "    |", "    |", "    |", "====="],
        [" +--+", " |  |", " O  |", "    |", "    |", "====="],
        [" +--+", " |  |", " O  |", " |  |", "    |", "====="],
        [" +--+", " |  |", " O  |", "/|  |", "    |", "====="],
        [" +--+", " |  |", " O  |", "/|\\ |", "    |", "====="],
        [" +--+", " |  |", " O  |", "/|\\ |", "/   |", "====="],
        [" +--+", " |  |", " O  |", "/|\\ |", "/ \\ |", "====="]
      ]

    gallowsFor() as pure
      -> misses as Integer
      <- rtn as List of String: gallowsStages().getOrDefault(misses, List() of String)

    mostMisses() as pure
      <- rtn as Integer: length gallowsStages() - 1

    //The game's data: the words the secret is drawn from, which the player's letters are compared with.
    animals() as pure
      <- rtn as List of String: ["ANT", "BEAR", "CAT", "DOG", "FROG", "GOAT", "HAWK", "LION", "MOLE",
        "MOOSE", "OWL", "PANDA", "RAT", "SEAL", "SWAN", "TIGER", "WOLF", "ZEBRA"]

    secretWord()
      -> rng as Random
      <- rtn as String: String()
      words <- animals()
      rtn: words.getOrDefault(rng.next(0, length words - 1), String())

    //Unset when the guess is a letter not tried before; otherwise the first thing wrong with it.
    problemWith() as pure
      ->
        guess as String
        missed as List of Character
        correct as Set of Character
      <- rtn as GuessProblem: GuessProblem()

      letter <- guess.first()
      if length guess <> 1
        rtn: GuessProblem.NotOneCharacter
      else if missed.contains(letter) or correct.contains(letter)
        rtn: GuessProblem.AlreadyGuessed
      else if not letter.isAlpha()
        rtn: GuessProblem.NotALetter

    isSolved() as pure
      ->
        secret as String
        correct as Set of Character
      <- rtn as Boolean: true

      for letter in secret
        if not correct.contains(letter)
          rtn: false

  defines function

    followedBySpace() as pure
      -> letter as Character
      <- rtn as String: `${letter} `

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

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

    Say

      introduction()
        "Hangman, by Al Sweigart al@inventwithpython.com"

      category()
        -> category as String
        `The category is: ${category}`

      noneMissed()
        "Missed letters: none yet"

      //The letters come each followed by a space, as the book prints them.
      missed()
        -> letters as String
        `Missed letters: ${letters}`

      askLetter()
        "Guess a letter."

      notOneLetter()
        "Please enter a single letter."

      alreadyGuessed()
        "You have already guessed that letter. Choose again."

      notALetter()
        "Please enter a LETTER."

      won()
        -> secret as String
        `Yes! The secret word is: ${secret}
You have won!`

      lost()
        -> secret as String
        `You have run out of guesses!
The word was "${secret}"`

  defines program

    //Hangman (Big Book project 34). Guess the letters of a secret animal before the gallows is
    //complete. The game decides; the Console asks and tells, in the player's words. The golden
    //replays a seeded game (seed.txt).
    Hangman()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      secret <- secretWord(rng)

      console.showIntroduction()

      //Missed letters are shown in the order they were guessed, so they are a List; the correct
      //ones are only ever asked "is this letter in?", so they are a Set.
      missed <- List() of Character
      correct <- Set() of Character
      running <- true
      while running and not console.ended()
        console.showGallows(secret, missed, correct)
        if guess <- console.nextGuess(missed, correct)
          if secret contains guess
            correct += guess
            if isSolved(secret, correct)
              console.showWon(secret)
              running: false
          else
            missed += guess
            if length missed == mostMisses()
              console.showGallows(secret, missed, correct)
              console.showLost(secret)
              running: false

About this port