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 43 — Mancala
This is project 43 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 player's answers are piped in:
A
G
QUIT
Run it with:
ek9 mancala.ek9 < input.txt
It prints:
Mancala, by Al Sweigart al@inventwithpython.com
Player 2
+------+------+------+------+------+------+
| G: 4 | H: 4 | I: 4 | J: 4 | K: 4 | L: 4 |
+------+------+------+------+------+------+
Store 2: 0 Store 1: 0
+------+------+------+------+------+------+
| A: 4 | B: 4 | C: 4 | D: 4 | E: 4 | F: 4 |
+------+------+------+------+------+------+
Player 1
Player 1, choose move: A-F (or QUIT)
Player 2
+------+------+------+------+------+------+
| G: 4 | H: 4 | I: 4 | J: 4 | K: 4 | L: 4 |
+------+------+------+------+------+------+
Store 2: 0 Store 1: 0
+------+------+------+------+------+------+
| A: 0 | B: 5 | C: 5 | D: 5 | E: 5 | F: 4 |
+------+------+------+------+------+------+
Player 1
Player 2, choose move: G-L (or QUIT)
Player 2
+------+------+------+------+------+------+
| G: 0 | H: 4 | I: 4 | J: 4 | K: 4 | L: 4 |
+------+------+------+------+------+------+
Store 2: 1 Store 1: 0
+------+------+------+------+------+------+
| A: 1 | B: 6 | C: 6 | D: 5 | E: 5 | F: 4 |
+------+------+------+------+------+------+
Player 1
Player 1, choose move: A-F (or QUIT)
Thanks for playing!
The program
#!ek9 defines module bigbook.mancala defines constant SeedsPerPit <- 4 //The board's own marks, data like the pit letters: the edges of the drawing, and where its //labels sit (this port's own layout). PitEdge <- "+------" Corner <- "+" Divider <- "|" PlayerLabelIndent <- 14 StoreLabelIndent <- 2 RightStoreColumn <- 35 defines type Player First Second //The twelve pits, lettered as the players type them, and the two stores. Pit A B C D E F G H I J K L StoreOne StoreTwo //Why a pit cannot be sown, in the order the book checks. PitProblem NotYourPit EmptyPit List of Pit Dict of (Pit, Pit) Dict of (Pit, Integer) defines record //What belongs to one player: the number shown on screen, their six pits in display order, //and their store. Side number as String: String() pits as List of Pit: List() of Pit store as Pit: Pit() Side() -> number as String pits as List of Pit store as Pit this.number: number this.pits: pits this.store: store 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 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()) //The board is drawn from the game's pits, with the edges sized to a side's pits. The labels //on it are words; where they sit is the drawing's. showBoard() -> board as Dict of (Pit, Integer) top <- sideOf(Player.Second) bottom <- sideOf(Player.First) edge <- edgeOf(top) playerIndent <- String("").rightPadded(PlayerLabelIndent, ' ') stdout.println(`${playerIndent}${say.player(top.number)}`) stdout.println(edge) stdout.println(pitRow(board, top)) stdout.println(edge) stdout.println(storeLine(board, top, bottom)) stdout.println(edge) stdout.println(pitRow(board, bottom)) stdout.println(edge) stdout.println(`${playerIndent}${say.player(bottom.number)}`) //The pit the player chooses, or unset when they QUIT or the input ends. nextMove() -> side as Side board as Dict of (Pit, Integer) <- rtn as Pit: Pit() quitting <- false while not rtn? and not quitting and not input.ended() stdout.println(say.askMove(side.number, pitRange(side))) if line <- input.next() reply <- line.trim().upperCase() //Constructing an enumeration from text gives an unset value for anything that is not a pit. pit <- Pit(reply) if reply == say.quit() quitting: true else if problem <- problemWith(side, board, pit) showProblem(problem) else rtn: pit showWinner() -> winner as Player stdout.println(say.won(sideOf(winner).number)) showTie() stdout.println(say.tie()) showGoodbye() stdout.println(say.goodbye()) ended() as pure <- rtn as Boolean: input.ended() private showProblem() -> problem as PitProblem switch problem case PitProblem.NotYourPit stdout.println(say.notYourPit()) case PitProblem.EmptyPit stdout.println(say.emptyPit()) default //problemWith returns a set problem or none; the compiler requires this branch (E07320). stdout.println(say.notYourPit()) //Each store's label and its seeds, the second player's store on the left. private storeLine() -> board as Dict of (Pit, Integer) left as Side right as Side <- rtn as String: String() indent <- String("").rightPadded(StoreLabelIndent, ' ') leftStore <- `${indent}${say.store(left.number)}${cell(board, left.store)}` rtn: `${leftStore.rightPadded(RightStoreColumn)}${say.store(right.number)}${cell(board, right.store)}` override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: its rules, with no input or output. sideOf() -> player as Player <- rtn as Side: Side() switch player case Player.First rtn: Side("1", [Pit.A, Pit.B, Pit.C, Pit.D, Pit.E, Pit.F], Pit.StoreOne) case Player.Second rtn: Side("2", [Pit.G, Pit.H, Pit.I, Pit.J, Pit.K, Pit.L], Pit.StoreTwo) default rtn: Side() opponentOf() as pure -> player as Player <- rtn as Player: Player() switch player case Player.First rtn: Player.Second case Player.Second rtn: Player.First default rtn: Player() //Seeds travel counterclockwise: along Player 1's pits to their store, back along Player 2's. nextPits() as pure <- rtn as Dict of (Pit, Pit): { Pit.A: Pit.B, Pit.B: Pit.C, Pit.C: Pit.D, Pit.D: Pit.E, Pit.E: Pit.F, Pit.F: Pit.StoreOne, Pit.StoreOne: Pit.L, Pit.L: Pit.K, Pit.K: Pit.J, Pit.J: Pit.I, Pit.I: Pit.H, Pit.H: Pit.G, Pit.G: Pit.StoreTwo, Pit.StoreTwo: Pit.A } oppositePits() as pure <- rtn as Dict of (Pit, Pit): { Pit.A: Pit.G, Pit.B: Pit.H, Pit.C: Pit.I, Pit.D: Pit.J, Pit.E: Pit.K, Pit.F: Pit.L, Pit.G: Pit.A, Pit.H: Pit.B, Pit.I: Pit.C, Pit.J: Pit.D, Pit.K: Pit.E, Pit.L: Pit.F } seedsIn() as pure -> board as Dict of (Pit, Integer) pit as Pit <- rtn as Integer: board.getOrDefault(pit, 0) seedsOnSide() -> board as Dict of (Pit, Integer) side as Side <- rtn as Integer: 0 for pit in side.pits rtn += seedsIn(board, pit) //The letters a player may choose from, such as "A-F". pitRange() as pure -> side as Side <- rtn as String: `${side.pits.getOrDefault(0, Pit())}-${side.pits.getOrDefault(length side.pits - 1, Pit())}` //Unset when the player may sow from the pit; otherwise what is wrong with it. problemWith() -> side as Side board as Dict of (Pit, Integer) pit as Pit <- rtn as PitProblem: PitProblem() if not pit? or not (side.pits contains pit) rtn: PitProblem.NotYourPit else if seedsIn(board, pit) == 0 rtn: PitProblem.EmptyPit newBoard() <- rtn as Dict of (Pit, Integer): Dict() of (Pit, Integer) for player in Player side <- sideOf(player) rtn += DictEntry(side.store, 0) for pit in side.pits rtn += DictEntry(pit, SeedsPerPit) //Sows the seeds from `start` and returns whose turn is next: the same player when the last //seed lands in their own store. The board is updated in place. sow() -> board as Dict of (Pit, Integer) player as Player start as Pit <- rtn as Player: opponentOf(player) side <- sideOf(player) opponentsStore <- sideOf(rtn).store next <- nextPits() //A copy, because `-=` changes the Integer it is applied to and the board still refers to it. remaining <- Integer(seedsIn(board, start)) board += DictEntry(start, 0) pit <- start while remaining > 0 pit: next.getOrDefault(pit, Pit()) if pit <> opponentsStore board += DictEntry(pit, seedsIn(board, pit) + 1) remaining -= 1 if pit == side.store rtn: player else if side.pits contains pit and seedsIn(board, pit) == 1 opposite <- oppositePits().getOrDefault(pit, Pit()) board += DictEntry(side.store, seedsIn(board, side.store) + seedsIn(board, opposite)) board += DictEntry(opposite, 0) //When either side has no seeds left the game is over, and the seeds still on the other //side go to that side's store. gameOver() -> board as Dict of (Pit, Integer) <- rtn as Boolean: false for player in Player if seedsOnSide(board, sideOf(player)) == 0 other <- sideOf(opponentOf(player)) board += DictEntry(other.store, seedsIn(board, other.store) + seedsOnSide(board, other)) for pit in other.pits board += DictEntry(pit, 0) rtn: true //Unset when the stores hold the same number of seeds. winnerOf() as pure -> board as Dict of (Pit, Integer) <- rtn as Player: Player() first <- seedsIn(board, Pit.StoreOne) second <- seedsIn(board, Pit.StoreTwo) if first > second rtn: Player.First else if second > first rtn: Player.Second defines function //The Console's drawing: computed output, with no words in it. cell() as pure -> board as Dict of (Pit, Integer) pit as Pit <- rtn as String: String() seeds <- $seedsIn(board, pit) rtn: seeds.leftPadded(2) pitRow() -> board as Dict of (Pit, Integer) side as Side <- rtn as String: "" for pit in side.pits rtn += `${Divider} ${pit}:${cell(board, pit)} ` rtn += Divider //A border with one segment per pit. edgeOf() -> side as Side <- rtn as String: "" for pit in side.pits rtn += PitEdge rtn += Corner //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 or type. The board is not here: the Console draws it //from the game's pits, and only its labels are words. defines text for "en" Say introduction() "Mancala, by Al Sweigart al@inventwithpython.com" player() -> number as String `Player ${number}` store() -> number as String `Store ${number}:` askMove() -> number as String letters as String `Player ${number}, choose move: ${letters} (or QUIT)` quit() "QUIT" notYourPit() "Please pick a letter on your side of the board." emptyPit() "Please pick a non-empty pit." won() -> number as String `Player ${number} has won!` tie() "There is a tie!" goodbye() "Thanks for playing!" defines program //Mancala (Big Book project 43). The two-player seed-sowing game: sow the seeds of a pit round //the board, and the most seeds in your store wins. The game decides; the Console asks and //tells, in the players' words. Mancala() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) console.showIntroduction() board <- newBoard() player <- Player.First running <- true while running and not console.ended() console.showBoard(board) if move <- console.nextMove(sideOf(player), board) player: sow(board, player, move) if gameOver(board) console.showBoard(board) if winner <- winnerOf(board) console.showWinner(winner) else console.showTie() running: false else //The player quit, or the input ended: as in the book, either way the game says goodbye. console.showGoodbye() running: false
About this port
- Faithful to the book: two players (pits A-F and G-L, stores 1 and 2), four seeds in each pit; sowing runs counterclockwise and skips the opponent's store; a last seed in your own store gives another turn; a last seed in one of your own empty pits captures the opposite pit into your store; when a side is empty the other side's seeds go to its store, and most seeds wins. QUIT stops. The board layout is original.
- Model:
PlayerandPitenumerations, and aSiderecord (the number shown on screen, the six pits, the store) thatsideOfgives for each player. The board is aDict of (Pit, Integer). Typing a pit is parsing an enumeration:Pit(reply)is unset for anything that is not a pit, so validation needs no list of letters.nextPitsandoppositePitsare the original's lookup tables, asDictliterals keyed byPit.