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 9 — Carrot in a Box

Carrot in a Box is a two-player bluffing game. One of two boxes secretly holds a carrot; the first player peeks inside their box and tells the second player whatever they like about what they saw; the second player then decides whether to swap boxes before the carrot's true location is revealed. Whoever ends up holding the carrot's box wins.

Adapted from project 9 of Al Sweigart's The Big Book of Small Python Projects (No Starch Press, 2021), under CC BY-NC-SA 4.0.

A sample run

Run with BIGBOOK_SEED=1 ek9 carrot-in-a-box.ek9 < input.txt, where input.txt supplies the two players' names and player 2's answer to the swap question:

Alice
Bob
NO

None of these three lines is echoed back verbatim — they only appear woven into the sentences the program builds from them, such as "Alice, you have the RED box." The full output:

Carrot in a Box, by Al Sweigart al@inventwithpython.com

A bluffing game for two human players. One box has a carrot in it.
To win, you must end up with the box that has the carrot.

Human player 1, enter your name:
Human player 2, enter your name:
HERE ARE TWO BOXES:
  +--------+   +--------+
  |  RED   |   |  GOLD  |
  |  BOX   |   |  BOX   |
  +--------+   +--------+
Alice, you have the RED box. Bob, you have the GOLD box.
Bob, close your eyes -- Alice is about to peek!

Alice, here is the inside of your RED box:
  (carrot!)

Alice, say one of the following to Bob:
  1) There is a carrot in my box.
  2) There is not a carrot in my box.

Bob, do you want to swap boxes with Alice? YES/NO

HERE ARE THE TWO BOXES:
  +--------+   +--------+
  |  RED   |   |  GOLD  |
  |  BOX   |   |  BOX   |
  +--------+   +--------+
The carrot was in the RED box.
Alice is the winner!
Thanks for playing!

With seed 1 the carrot is in the RED box; Bob keeps his GOLD box, so Alice wins.

The program

#!ek9
defines module bigbook.carrot

  defines constant

    //The width of a box's front, where its name is painted.
    BoxWidth <- 8

  defines type

    Box
      Red
      Gold

  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 players see or type 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())

      //A player's name, as typed; unset if the input ends first.
      nextName()
        -> player as Integer
        <- rtn as String: String()
        stdout.println(say.askName(player))
        if entry <- input.next()
          rtn: entry.trim()

      private showBoxes()
        ->
          left as Box
          right as Box

        stdout.println(say.boxes(painted(left), painted(right)))

      showStart()
        ->
          first as String
          second as String

        stdout.println(say.hereAreTwoBoxes())
        showBoxes(Box.Red, Box.Gold)
        stdout.println(say.whoHasWhich(first, second))
        stdout.println(say.closeYourEyes(first, second))

      showInside()
        ->
          first as String
          second as String
          hasCarrot as Boolean

        stdout.println("")
        stdout.println(say.insideRedBox(first))
        stdout.println(hasCarrot <- say.carrot() : say.noCarrot())
        stdout.println("")
        stdout.println(say.bluffChoices(first, second))

      //Asks until the answer is a yes or a no; unset only if the input ends first.
      wantsToSwap()
        ->
          first as String
          second as String
        <- rtn as Boolean: Boolean()

        stdout.println("")
        stdout.println(say.askSwap(first, second))
        while not rtn? and not input.ended()
          rtn: yesOrNo(input.next(), say.yes(), say.no())
          if not rtn?
            stdout.println(say.notYesOrNo(second))

      showEnding()
        ->
          firstBox as Box
          carrotBox as Box
          winner as String

        stdout.println("")
        stdout.println(say.hereAreTheTwoBoxes())
        showBoxes(firstBox, otherBox(firstBox))
        stdout.println(say.carrotWasIn(nameOf(carrotBox)))
        stdout.println(say.winner(winner))

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

      //The name painted on a box, centred on its front: "  RED   ".
      private painted()
        -> box as Box
        <- rtn as String: String()
        name <- nameOf(box)
        rtn: name.center(BoxWidth)

      private nameOf()
        -> box as Box
        <- rtn as String: box == Box.Red <- say.red() : say.gold()

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

  defines function

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

    carrotBoxOf()
      -> rng as Random
      <- rtn as Box: rng.next(1, 2) == 1 <- Box.Red : Box.Gold

    otherBox() as pure
      -> box as Box
      <- rtn as Box: box == Box.Red <- Box.Gold : Box.Red

    //Player 1 starts with the RED box and has the GOLD one after a swap.
    firstPlayersBox() as pure
      -> swapped as Boolean
      <- rtn as Box: swapped <- Box.Gold : Box.Red

    //True for a reply starting with the yes word, false for one starting with the no word,
    //unset otherwise.
    yesOrNo() as pure
      ->
        reply as String
        yes as String
        no as String
      <- rtn as Boolean: Boolean()

      initial <- reply.trim().upperCase()
      if initial.startsWith(yes)
        rtn: true
      else if initial.startsWith(no)
        rtn: false

  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 players see.
  defines text for "en"

    Say

      introduction()
        `Carrot in a Box, by Al Sweigart al@inventwithpython.com

A bluffing game for two human players. One box has a carrot in it.
To win, you must end up with the box that has the carrot.
`

      askName()
        -> player as Integer
        `Human player ${player}, enter your name:`

      red()
        "RED"

      gold()
        "GOLD"

      //The two boxes, each painted with its name.
      boxes()
        ->
          left as String
          right as String
        `  +--------+   +--------+
  |${left}|   |${right}|
  |  BOX   |   |  BOX   |
  +--------+   +--------+`

      hereAreTwoBoxes()
        "HERE ARE TWO BOXES:"

      whoHasWhich()
        ->
          first as String
          second as String
        `${first}, you have the RED box. ${second}, you have the GOLD box.`

      closeYourEyes()
        ->
          first as String
          second as String
        `${second}, close your eyes -- ${first} is about to peek!`

      insideRedBox()
        -> first as String
        `${first}, here is the inside of your RED box:`

      carrot()
        "  (carrot!)"

      noCarrot()
        "  (no carrot!)"

      bluffChoices()
        ->
          first as String
          second as String
        `${first}, say one of the following to ${second}:
  1) There is a carrot in my box.
  2) There is not a carrot in my box.`

      askSwap()
        ->
          first as String
          second as String
        `${second}, do you want to swap boxes with ${first}? YES/NO`

      yes()
        "Y"

      no()
        "N"

      notYesOrNo()
        -> second as String
        `${second}, please enter "YES" or "NO".`

      hereAreTheTwoBoxes()
        "HERE ARE THE TWO BOXES:"

      carrotWasIn()
        -> box as String
        `The carrot was in the ${box} box.`

      winner()
        -> name as String
        `${name} is the winner!`

      goodbye()
        "Thanks for playing!"

  defines program

    //Carrot in a Box (Big Book project 9). A bluffing game for two people: player 1 peeks in the
    //box and may bluff, player 2 decides whether to swap, and whoever ends up with the carrot wins.
    //The game decides; the Console asks and tells, in the player's words. The golden replays a
    //seeded game (seed.txt).
    CarrotInABox()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      console.showIntroduction()
      first <- console.nextName(1)
      second <- console.nextName(2)
      //Unset names mean the input ended before the game could start.
      if first? and second?
        console.showStart(first, second)
        carrotBox <- carrotBoxOf(rng)
        console.showInside(first, second, carrotBox == Box.Red)

        swapped <- console.wantsToSwap(first, second)
        if swapped?
          firstBox <- firstPlayersBox(swapped)
          console.showEnding(firstBox, carrotBox, firstBox == carrotBox <- first : second)
          console.showGoodbye()

How the port works

The original's main() decides, asks, tells and words everything in the same lines. The port keeps those apart. The game is four small functions that take values and give back values: carrotBoxOf makes the one random choice, otherBox and firstPlayersBox say who holds which box, and yesOrNo reads a reply. Box is an enumeration with two members, Red and Gold, so the carrot's box, player 1's box after the swap and the winner are all Box values compared with ==; EK9 has no if-expression, so the port reaches for the ternary cond <- a : b wherever the original picks between two values (rng.next(1, 2) == 1 <- Box.Red : Box.Gold).

yesOrNo returns a tri-state Boolean: true for a reply starting with the yes word, false for one starting with the no word, and simply unset for anything else — there is no third sentinel value to invent. The swap question's loop, while not rtn? and not input.ended(), uses that unset state as its own continuation signal: it is both the original's "keep asking until a valid answer" while True/break loop and the reason the game can stop cleanly once the input runs out.

The Console is the one class through which everything the players see or type passes. It asks for the names and for the swap, asks the game whether a reply is a yes or a no, and turns a Box into the name painted on it, centred with center(BoxWidth). Say holds every word: the banner with its credit line, the box picture as one method that takes the two painted names, the bluffing choices as one multi-line String, and even the box names and the yes and no initials, so a translation changes only Say.

The program is the flow, laid out as the original's main(). It starts the game only when it has both names (if first? and second?), and it chooses the words with Say(language()) ?: Say("en"). There is no break, continue or return statement anywhere in the port: the guard loop and the ternaries do their work instead.