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 31 — Guess the Number

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

fifty
50
99
75
97

Run it with:

BIGBOOK_SEED=1 ek9 guessthenumber.ek9 < input.txt

It prints:

Guess the Number, by Al Sweigart al@inventwithpython.com

I am thinking of a number between 1 and 100.
You have 10 guesses left. Take a guess.
Please enter a number between 1 and 100.
Your guess is too low.
You have 9 guesses left. Take a guess.
Your guess is too high.
You have 8 guesses left. Take a guess.
Your guess is too low.
You have 7 guesses left. Take a guess.
Yay! You guessed my number!

The program

#!ek9
defines module bigbook.guessthenumber

  defines constant

    Highest <- 100
    Guesses <- 10

  defines type

    //What a wrong guess is told: the game decides which, the Console words it.
    Hint
      TooLow
      TooHigh

  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 reply is a number; unset only if the input ends first.
      nextGuess()
        -> guessesLeft as Integer
        <- rtn as Integer: Integer()
        stdout.println(say.askGuess(guessesLeft))
        while not rtn? and not input.ended()
          rtn: guessFrom(input.next())
          if not rtn? and not input.ended()
            stdout.println(say.notANumber())

      showHint()
        -> hint as Hint
        stdout.println(hint == Hint.TooLow <- say.tooLow() : say.tooHigh())

      showResult()
        ->
          guessed as Boolean
          secret as Integer

        stdout.println(guessed <- say.guessedIt() : say.gameOver(secret))

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

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

  defines function

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

    //Integer(text) is unset when the text is not a number, so the parse is the validation:
    //no isdecimal() first.
    guessFrom() as pure
      -> entry as String
      <- rtn as Integer: Integer(entry)

    hintFor() as pure
      ->
        guess as Integer
        secret as Integer
      <- rtn as Hint: guess < secret <- Hint.TooLow : Hint.TooHigh

  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()
        `Guess the Number, by Al Sweigart al@inventwithpython.com

I am thinking of a number between 1 and ${Highest}.`

      askGuess()
        -> guessesLeft as Integer
        `You have ${guessesLeft} guesses left. Take a guess.`

      notANumber()
        `Please enter a number between 1 and ${Highest}.`

      tooLow()
        "Your guess is too low."

      tooHigh()
        "Your guess is too high."

      guessedIt()
        "Yay! You guessed my number!"

      gameOver()
        -> secret as Integer
        `Game over. The number I was thinking of was ${secret}`

  defines program

    //Guess the Number (Big Book project 31). Ten guesses at a number from 1 to 100. The game
    //decides; the Console asks and tells, in the player's words. The golden replays a seeded game
    //(seed.txt).
    GuessTheNumber()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      secret <- rng.next(1, Highest)
      console.showIntroduction()

      //The loop condition carries the book's 'break': it stops on the right guess.
      guessed <- false
      guessesLeft <- Guesses
      while guessesLeft > 0 and not guessed and not console.ended()
        if guess <- console.nextGuess(guessesLeft)
          guessed: guess == secret
          if not guessed
            console.showHint(hintFor(guess, secret))
        guessesLeft -= 1

      console.showResult(guessed, secret)