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 48 — The Monty Hall Problem

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

1
Y
1
N
QUIT

Run it with:

BIGBOOK_SEED=1 ek9 monty-hall-problem.ek9 < input.txt

It prints:

The Monty Hall Problem, by Al Sweigart al@inventwithpython.com
Behind one door is a car; behind the other two, goats. Swapping improves your odds!
+-----+ +-----+ +-----+
|  1  | |  2  | |  3  |
+-----+ +-----+ +-----+
Pick a door 1, 2, or 3 (or QUIT to stop):
Door 3 contains a goat!
Do you want to swap doors? Y/N
[ 1: CAR! ] [ 2: GOAT ] [ 3: GOAT ] 
Door 1 has the car!
Sorry, you lost.

Swapping:     0 wins, 1 losses, success rate 0.0%
Not swapping: 0 wins, 0 losses, success rate 0.0%

+-----+ +-----+ +-----+
|  1  | |  2  | |  3  |
+-----+ +-----+ +-----+
Pick a door 1, 2, or 3 (or QUIT to stop):
Door 2 contains a goat!
Do you want to swap doors? Y/N
[ 1: GOAT ] [ 2: GOAT ] [ 3: CAR! ] 
Door 3 has the car!
Sorry, you lost.

Swapping:     0 wins, 1 losses, success rate 0.0%
Not swapping: 0 wins, 1 losses, success rate 0.0%

+-----+ +-----+ +-----+
|  1  | |  2  | |  3  |
+-----+ +-----+ +-----+
Pick a door 1, 2, or 3 (or QUIT to stop):
Thanks for playing!

The program

#!ek9
defines module bigbook.montyhall

  defines constant

    Doors <- 3

  defines class

    //THE GAME's state: how one strategy (always swap, or always stay) has done so far. It keeps
    //the counts; the Console words them.
    Tally
      wins as Integer: 0
      losses as Integer: 0

      add()
        -> won as Boolean
        if won
          wins += 1
        else
          losses += 1

      //Copies, because `+= 1` changes a count in place.
      winCount() as pure
        <- rtn as Integer: Integer(wins)

      lossCount() as pure
        <- rtn as Integer: Integer(losses)

      //A percentage to one decimal place; 0.0 before any game has been played.
      successRate() as pure
        <- rtn as Float: 0.0
        played <- wins + losses
        if played > 0
          percentage <- Float(wins) * 100.0 / Float(played)
          rtn: percentage.round(1)

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

    //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 doors are the game's picture, drawn as they are.
      showDoors()
        cat doorsPicture() > stdout

      //Asks until the player picks a door; unset when they type QUIT or the input ends.
      nextPick()
        <- rtn as Integer: Integer()
        quitting <- false
        while not rtn? and not quitting and not input.ended()
          stdout.println(say.askDoor())
          if entry <- input.next()
            response <- entry.upperCase().trim()
            if response == say.quit()
              quitting: true
            else
              rtn: doorOf(response)

      showGoat()
        -> door as Integer
        stdout.println(say.goatBehind(door))

      //Asks until the player answers Y or N; unset only if the input ends first.
      wantsSwap()
        <- rtn as Boolean: Boolean()
        while not rtn? and not input.ended()
          stdout.println(say.askSwap())
          if entry <- input.next()
            response <- entry.upperCase().trim()
            if response == say.yes()
              rtn: true
            else if response == say.no()
              rtn: false

      //Every door opened, then where the car was.
      showReveal()
        -> carDoor as Integer
        line <- ""
        for door in 1 ... Doors
          contents <- door == carDoor <- say.car() : say.goat()
          line += say.openedDoor(door, contents)
        stdout.println(line)
        stdout.println(say.carBehind(carDoor))

      showOutcome()
        -> won as Boolean
        stdout.println(won <- say.won() : say.lost())

      //The two labels are aligned to the longer one, measured, so they line up in any language.
      showTallies()
        ->
          swapping as Tally
          staying as Tally

        width <- length say.swapping() >? length say.notSwapping()
        stdout.println("")
        stdout.println(say.strategy(say.swapping().rightPadded(width), resultsOf(swapping)))
        stdout.println(say.strategy(say.notSwapping().rightPadded(width), resultsOf(staying)))
        stdout.println("")

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

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

      private resultsOf()
        -> tally as Tally
        <- rtn as String: say.results(tally.winCount(), tally.lossCount(), tally.successRate())

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

  defines function

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

    //This port's own picture of the three doors. It holds no words, so it is the game's data,
    //not text (as Hangman's gallows are).
    doorsPicture() as pure
      <- rtn as List of String: ["+-----+ +-----+ +-----+", "|  1  | |  2  | |  3  |", "+-----+ +-----+ +-----+"]

    //The car goes behind any door.
    carPlacement()
      -> rng as Random
      <- rtn as Integer: rng.next(1, Doors)

    //A door number the player typed; unset for anything that is not a door.
    doorOf() as pure
      -> response as String
      <- rtn as Integer: Integer()
      if door <- Integer(response) then door >= 1 and door <= Doors
        rtn: door

    //The host opens a door that is neither the player's nor the car's. Like the original, it
    //draws doors at random until one qualifies.
    goatDoor()
      ->
        rng as Random
        pick as Integer
        carDoor as Integer
      <- rtn as Integer: Integer()

      while not rtn?
        door <- rng.next(1, Doors)
        if door <> pick and door <> carDoor
          rtn: door

    //The one door that is neither picked nor opened: where a swap goes.
    otherDoor() as pure
      ->
        pick as Integer
        opened as Integer
      <- rtn as Integer: Integer()

      for door in 1 ... Doors
        if door <> pick and door <> opened
          rtn: door

  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. The doors' picture is not here: it is the game's data.
  defines text for "en"

    Say

      introduction()
        `The Monty Hall Problem, by Al Sweigart al@inventwithpython.com
Behind one door is a car; behind the other two, goats. Swapping improves your odds!`

      askDoor()
        "Pick a door 1, 2, or 3 (or QUIT to stop):"

      quit()
        "QUIT"

      goatBehind()
        -> door as Integer
        `Door ${door} contains a goat!`

      askSwap()
        "Do you want to swap doors? Y/N"

      yes()
        "Y"

      no()
        "N"

      car()
        "CAR!"

      goat()
        "GOAT"

      openedDoor()
        ->
          door as Integer
          contents as String
        `[ ${door}: ${contents} ] `

      carBehind()
        -> door as Integer
        `Door ${door} has the car!`

      won()
        "You won!"

      lost()
        "Sorry, you lost."

      swapping()
        "Swapping:"

      notSwapping()
        "Not swapping:"

      strategy()
        ->
          label as String
          counts as String
        `${label} ${counts}`

      results()
        ->
          wins as Integer
          losses as Integer
          rate as Float
        `${wins} wins, ${losses} losses, success rate ${rate}%`

      goodbye()
        "Thanks for playing!"

  defines program

    //Monty Hall Problem (Big Book project 48). Pick a door, see a goat, and choose whether to swap:
    //the tallies show that swapping wins about two games in three. The game decides; the Console
    //asks and tells, in the player's words. The golden replays a seeded game (seed.txt).
    MontyHall()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()
      swapping <- Tally()
      staying <- Tally()

      console.showIntroduction()
      running <- true
      while running and not console.ended()
        carDoor <- carPlacement(rng)
        console.showDoors()
        if pick <- console.nextPick()
          goat <- goatDoor(rng, pick, carDoor)
          console.showGoat(goat)
          swapped <- console.wantsSwap()
          //A Boolean is checked with `?`, not a guard: a guard would also need it to be true.
          if swapped?
            finalPick <- swapped <- otherDoor(pick, goat) : pick
            console.showReveal(carDoor)
            won <- finalPick == carDoor
            console.showOutcome(won)
            strategy <- swapped <- swapping : staying
            strategy.add(won)
            console.showTallies(swapping, staying)
        else
          running: false
      console.showGoodbye()

About this port