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 78 — Trick Questions

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

nine
all of them
an egg
purple
glass
a sponge

Run it with:

BIGBOOK_SEED=1 ek9 trick-questions.ek9 < input.txt

It prints:

Trick Questions, by Al Sweigart al@inventwithpython.com
Can you figure out the answers? (Enter QUIT to stop.)

Question 1 of 6 (Score: 0)
QUESTION: A farmer has 17 sheep and all but 9 run away. How many are left?
Correct! Nine.

Question 2 of 6 (Score: 1)
QUESTION: How many months have 28 days?
Correct! All of them.

Question 3 of 6 (Score: 2)
QUESTION: What has to be broken before you can use it?
Correct! An egg.

Question 4 of 6 (Score: 3)
QUESTION: What gets wetter the more it dries?
Nope. The answer is: A towel.

Question 5 of 6 (Score: 3)
QUESTION: A greenhouse is made of glass. What is it made of?
Correct! Glass.

Question 6 of 6 (Score: 4)
QUESTION: What is full of holes but still holds water?
Correct! A sponge.
Final score: 5 / 6

The program

#!ek9
defines module bigbook.trickquestions

  defines type

    List of Question
    shuffled of Question

  defines record

    //A question, the answer to show, and the lowercase keywords any one of which makes a reply
    //correct. A record keeps the three together, so nothing has to be split back out of a string.
    Question
      text as String: String()
      answer as String: String()
      accepts as List of String: List() of String

      Question()
        ->
          text as String
          answer as String
          accepts as List of String

        this.text: text
        this.answer: answer
        this.accepts: accepts

      default operator ?

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

      showQuestion()
        ->
          number as Integer
          total as Integer
          score as Integer
          question as Question

        stdout.println("")
        stdout.println(say.questionHeader(number, total, score))
        stdout.println(say.askQuestion(question.text))

      //The player's reply, trimmed. Unset when they QUIT or the input ends: either way there is
      //no reply to mark.
      nextReply()
        <- rtn as String: String()
        if line <- input.next()
          reply <- line.trim()
          if reply.upperCase() <> say.quit()
            rtn: reply

      showMarked()
        ->
          correct as Boolean
          question as Question

        stdout.println(correct <- say.correct(question.answer) : say.nope(question.answer))

      showFinalScore()
        ->
          score as Integer
          total as Integer

        stdout.println(say.finalScore(score, total))

      showGoodbye()
        stdout.println(say.goodbye())

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

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

  defines function

    //THE GAME: its questions, their order and its rule, 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 game's data: the questions, the answers shown and the keywords a reply is compared with.
    questionBank()
      <- rtn as List of Question: [
        Question("A farmer has 17 sheep and all but 9 run away. How many are left?", "Nine.", ["9", "nine"]),
        Question("What has to be broken before you can use it?", "An egg.", ["egg"]),
        Question("What gets wetter the more it dries?", "A towel.", ["towel"]),
        Question("A greenhouse is made of glass. What is it made of?", "Glass.", ["glass"]),
        Question("How many months have 28 days?", "All of them.", ["all", "12", "twelve"]),
        Question("What is full of holes but still holds water?", "A sponge.", ["sponge"])
      ]

    //A reply is correct when it contains any accepted keyword, so "an egg" and "eggs" both count.
    isCorrect() as pure
      ->
        question as Question
        reply as String
      <- rtn as Boolean: false

      for keyword in question.accepts
        if reply contains keyword
          rtn: true

  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 or types. The questions and their answers are not here:
  //they are the game's data, compared with what the player types.
  defines text for "en"

    Say

      introduction()
        `Trick Questions, by Al Sweigart al@inventwithpython.com
Can you figure out the answers? (Enter QUIT to stop.)`

      questionHeader()
        ->
          number as Integer
          total as Integer
          score as Integer
        `Question ${number} of ${total} (Score: ${score})`

      askQuestion()
        -> questionText as String
        `QUESTION: ${questionText}`

      quit()
        "QUIT"

      goodbye()
        "Thanks for playing!"

      correct()
        -> answer as String
        `Correct! ${answer}`

      nope()
        -> answer as String
        `Nope. The answer is: ${answer}`

      finalScore()
        ->
          score as Integer
          total as Integer
        `Final score: ${score} / ${total}`

  defines program

    //Trick Questions (Big Book project 78). A quiz of trick questions, asked in a random order. The
    //game decides; the Console asks and tells, in the player's words. The golden replays a seeded
    //game (seed.txt).
    TrickQuestions()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      console.showIntroduction()

      shuffle <- () is shuffled of Question as function
      questions <- shuffle(questionBank(), rng)
      total <- length questions
      score <- 0
      asked <- 0

      //An iterator rather than a for loop, because QUIT or the end of the input stops the quiz early.
      remaining <- questions.iterator()
      running <- true
      while running and remaining.hasNext() and not console.ended()
        question <- remaining.next()
        console.showQuestion(number: asked + 1, total: total, score: score, question: question)
        if reply <- console.nextReply()
          correct <- isCorrect(question, reply.lowerCase())
          console.showMarked(correct, question)
          if correct
            score += 1
          asked += 1
        else
          console.showGoodbye()
          running: false

      if asked == total
        console.showFinalScore(score, total)

About this port