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
- Faithful to the book: three doors (one car, two goats); pick a door; the host opens a goat door that is neither your pick nor the car (drawing doors at random until one qualifies, as the original does); you may swap; all doors open; win/lose recorded separately for swapping vs staying, with running success rates. QUIT ends.
- Idioms:
- The original's four counters become two
Tallyobjects, one per strategy; the counts are handed out as copies, because+= 1changes a count in place. - Where a swap goes is
otherDoor, the door that is neither picked nor opened, written as that rule (as pure) rather than the6 - pick - goatarithmetic trick. - A
Booleananswer is checked withswapped?, not a guard:if swapped <- console.wantsSwap()also needs the value to be true, so "N" would skip the round. - The only number named is
Doors; door numbers count from 1, which needs no name.
- The original's four counters become two
- Divergences: own minimal door art; "Press Enter" pauses omitted.