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 75 — Three-Card Monte
This is project 75 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=4. The player's answers are piped in:
MIDDLE
Run it with:
BIGBOOK_SEED=4 ek9 threecardmonte.ek9 < input.txt
It prints:
Three-Card Monte, by Al Sweigart al@inventwithpython.com
Find the red lady (the Queen of Hearts)! Keep an eye on how
the cards move.
Here are the cards:
___ ___ ___
|Q | |10 | |7 |
| ♥ | | ♦ | | ♣ |
|__Q| |_10| |__7|
swapping right and left...
swapping middle and left...
swapping middle and right...
swapping left and right...
swapping middle and left...
swapping middle and left...
swapping right and left...
swapping right and middle...
swapping right and middle...
swapping right and middle...
swapping middle and right...
swapping right and middle...
swapping middle and right...
swapping left and right...
swapping left and right...
swapping middle and right...
Which card has the Queen of Hearts? (LEFT MIDDLE RIGHT)
___ ___ ___
|10 | |7 | |Q |
| ♦ | | ♣ | | ♥ |
|_10| |__7| |__Q|
You lost!
Thanks for playing, sucker!
The program
#!ek9 defines module bigbook.threecardmonte defines constant NumberOfSwaps <- 16 defines type //Rank and suit are closed sets, as in Blackjack: a card is a record of the two, so there //is no "rank|suit" string to split at every use and no "Q|♥" string to compare against. Suit Hearts Diamonds Spades Clubs Rank Two Three Four Five Six Seven Eight Nine Ten Jack Queen King Ace //The three places on the table. The player's answer parses into one of these names; the //Console words each one for the swap messages. Position Left Middle Right List of Rank List of Suit List of Position List of Swap Dict of (Rank, String) Dict of (Position, Card) DictEntry of (Position, Card) defines record //Fields default UNSET: Card() is "no card yet", which is what getRandomCard loops on. Card rank as Rank: Rank() suit as Suit: Suit() default Card() as pure Card() as pure -> cardRank as Rank cardSuit as Suit rank :=? cardRank suit :=? cardSuit default operator ? default operator == //One move of the dealer's hands: the cards at two positions change places. Swap first as Position: Position() second as Position: Position() default Swap() as pure Swap() as pure -> firstPosition as Position secondPosition as Position first :=? firstPosition second :=? secondPosition 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()) showDealt() -> table as Dict of (Position, Card) stdout.println(say.hereAreTheCards()) showCards(table) showSwap() -> swap as Swap stdout.println(say.swapping(positionWord(swap.first), positionWord(swap.second))) //Asks until the answer is one of the three positions; unset only if the input ends first. nextGuess() <- rtn as Position: Position() stdout.println("") while not rtn? and not input.ended() stdout.println(say.askGuess()) rtn: positionOf(input.next()) //The cards are drawn, not worded: rows built side by side from the table, so the cards //print next to each other. The bottom row pads with '_' on the left, as the book's //rjust(2, '_') does. The book keeps FIVE rows and fills four, so its cards end with an //empty fifth row: a blank line, printed here too. showCards() -> table as Dict of (Position, Card) labels <- rankLabels() top <- "" rankRow <- "" suitRow <- "" bottom <- "" for position in Position card <- table.getOrDefault(position, Card()) label <- labels.getOrDefault(card.rank, "") top += " ___ " rankRow += `|${label.rightPadded(2)} | ` suitRow += `| ${suitGlyph(card.suit)} | ` bottom += `|_${label.leftPadded(2, '_')}| ` stdout.println(top) stdout.println(rankRow) stdout.println(suitRow) stdout.println(bottom) stdout.println("") showOutcome() -> won as Boolean stdout.println(won <- say.won() : say.lost()) private positionWord() -> position as Position <- rtn as String: position == Position.Left <- say.left() : position == Position.Middle <- say.middle() : say.right() override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: its cards, its rules and its draws, with no input or output. queenOfHearts() as pure <- rtn as Card: Card(Rank.Queen, Suit.Hearts) //The marks a card shows are the game's data, not words: they are drawn, and do not translate. rankLabels() as pure <- rtn as Dict of (Rank, String): { Rank.Two: "2", Rank.Three: "3", Rank.Four: "4", Rank.Five: "5", Rank.Six: "6", Rank.Seven: "7", Rank.Eight: "8", Rank.Nine: "9", Rank.Ten: "10", Rank.Jack: "J", Rank.Queen: "Q", Rank.King: "K", Rank.Ace: "A" } suitGlyph() as pure -> suit as Suit <- rtn as String: "" switch suit case Suit.Hearts rtn: "♥" case Suit.Diamonds rtn: "♦" case Suit.Spades rtn: "♠" case Suit.Clubs rtn: "♣" default rtn: "" //The book's six swaps, in the book's order: the pick is an index into this list, so the //order is part of what a seeded game replays. allSwaps() as pure <- rtn as List of Swap: [ Swap(Position.Left, Position.Middle), Swap(Position.Middle, Position.Right), Swap(Position.Left, Position.Right), Swap(Position.Middle, Position.Left), Swap(Position.Right, Position.Middle), Swap(Position.Right, Position.Left) ] randomSwap() -> rng as Random <- rtn as Swap: Swap() swaps <- allSwaps() rtn: swaps.getOrDefault(rng.next(0, length swaps - 1), Swap()) //A new table with the two cards changed over; the table passed in is left as it was. swapped() as pure -> table as Dict of (Position, Card) swap as Swap <- rtn as Dict of (Position, Card): Dict() of (Position, Card) atFirst <- table.getOrDefault(swap.first, Card()) atSecond <- table.getOrDefault(swap.second, Card()) rtn: table + DictEntry(swap.first, atSecond) + DictEntry(swap.second, atFirst) //Enumeration names parse exactly ("Middle"), so the reply is put in that case first. //Anything else - including the end of the input - parses to an UNSET Position. positionOf() as pure -> reply as String <- rtn as Position: Position(reply.trim().lowerCase().title()) isQueenAt() as pure -> table as Dict of (Position, Card) guess as Position <- rtn as Boolean: table.getOrDefault(guess, Card()) == queenOfHearts() //Faithful to the book's code rather than to its comment: the comment says "not the //Queen of Hearts", but the test rejects every Queen and every heart. Kept, so the cards //dealt are the ones the book's program deals. getRandomCard() -> rng as Random <- rtn as Card: Card() ranks <- cat Rank | collect as List of Rank suits <- cat Suit | collect as List of Suit while not rtn? rank <- ranks.getOrDefault(rng.next(0, length ranks - 1), Rank()) suit <- suits.getOrDefault(rng.next(0, length suits - 1), Suit()) if rank <> Rank.Queen and suit <> Suit.Hearts rtn: Card(rank, suit) //The Queen of Hearts goes to a random position and the two other cards fill the rest, left //to right. An Iterator's next() is only allowed once hasNext() has been checked (E08030). dealTable() -> rng as Random <- rtn as Dict of (Position, Card): Dict() of (Position, Card) otherCards <- [getRandomCard(rng), getRandomCard(rng)] others <- otherCards.iterator() positions <- cat Position | collect as List of Position queenAt <- positions.getOrDefault(rng.next(0, length positions - 1), Position()) for position in Position if position == queenAt rtn += DictEntry(position, queenOfHearts()) else if others.hasNext() rtn += DictEntry(position, others.next()) 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 cards' ranks and suits are not here: they are the //game's marks, and the Console draws them. defines text for "en" Say introduction() `Three-Card Monte, by Al Sweigart al@inventwithpython.com Find the red lady (the Queen of Hearts)! Keep an eye on how the cards move. ` hereAreTheCards() "Here are the cards:" left() "left" middle() "middle" right() "right" swapping() -> fromPosition as String toPosition as String `swapping ${fromPosition} and ${toPosition}...` askGuess() "Which card has the Queen of Hearts? (LEFT MIDDLE RIGHT)" won() `You won! Thanks for playing!` lost() `You lost! Thanks for playing, sucker!` defines program //Three-Card Monte (Big Book project 75). Follow the Queen of Hearts through the swaps, then //guess where she is. The game decides; the Console asks and tells, in the player's words. The //golden replays a seeded game (seed.txt). ThreeCardMonte() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) rng <- randomSource() console.showIntroduction() table <- dealTable(rng) console.showDealt(table) for swapNumber in 1 ... NumberOfSwaps swap <- randomSwap(rng) console.showSwap(swap) table: swapped(table, swap) if guess <- console.nextGuess() console.showCards(table) console.showOutcome(isQueenAt(table, guess))
About this port
- Faithful to the book: the Queen of Hearts among two random cards, the ASCII cards (the book
keeps five rows and fills four, so every display ends with a blank fifth row; printed since
2026-10-15), 16 announced swaps ("swapping left and middle..."), a LEFT/MIDDLE/RIGHT guess
that is asked again until it is one of the three, then the reveal ("You won!" / "You lost!"
...). The "Press Enter" pause and the screen-clear (
print('\n' * 60), 61 blank lines; the port prints one) are omitted for a text golden, and so is the book's 0.8 stime.sleepafter each swap (16 swaps, 12.8 s a game), which would add that much to every run of the gate. - Model: the table is a
Dict of (Position, Card); a swap isswapped(table, swap), a pure function returning the new table. The six swaps are a list in the book's order: the random pick is an index into it, so the order is part of what the seeded golden replays. Enumeration names parse exactly, sopositionOfputs the reply into that case first; anything else, or the end of the input, is unset and the Console asks again.