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 30 — Four in a Row
This is project 30 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:
1
2
1
2
1
2
1
Run it with:
ek9 four-in-a-row.ek9 < input.txt
It prints:
Four in a Row, by Al Sweigart al@inventwithpython.com
Two players take turns dropping tiles into one of seven columns, trying
to make four in a row horizontally, vertically, or diagonally.
1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|.......|
|.......|
+-------+
Player X, enter a column or QUIT:
1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|.......|
|X......|
+-------+
Player O, enter a column or QUIT:
1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|.......|
|XO.....|
+-------+
Player X, enter a column or QUIT:
1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|X......|
|XO.....|
+-------+
Player O, enter a column or QUIT:
1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|XO.....|
|XO.....|
+-------+
Player X, enter a column or QUIT:
1234567
+-------+
|.......|
|.......|
|.......|
|X......|
|XO.....|
|XO.....|
+-------+
Player O, enter a column or QUIT:
1234567
+-------+
|.......|
|.......|
|.......|
|XO.....|
|XO.....|
|XO.....|
+-------+
Player X, enter a column or QUIT:
1234567
+-------+
|.......|
|.......|
|X......|
|XO.....|
|XO.....|
|XO.....|
+-------+
Player X has won!
The program
#!ek9 defines module bigbook.fourinarow defines constant Width <- 7 Height <- 6 Connect <- 4 //The board's own marks, data like the tiles: an empty cell, and the edges of the drawing. Empty <- "." Edge <- '|' Corner <- "+" Rule <- '-' defines type //The two players are a closed set, and '$' gives the mark their tiles show. The marks are //the game's data, not words: they are drawn on the board and do not translate. Player X O List of Coord Dict of (Coord, Player) DictEntry of (Coord, Player) defines record //A board position, compared by value, so it can be a Dict key. Copied from //lib/Coord.ek9, which explains its two details: the fields default UNSET, and the //constructor SNAPSHOTS its arguments. Coord x as Integer: Integer() y as Integer: Integer() default Coord() as pure Coord() as pure -> px as Integer py as Integer x :=? Integer(px) y :=? Integer(py) operator $ as pure <- rtn as String: `(${x},${y})` default operator ? default operator <=> 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()) //The board is drawn, not worded: computed from the game's grid, a row at a time, with the //column numbers above it and the borders sized to Width. showBoard() -> board as Dict of (Coord, Player) columnNumbers <- for column in 1 ... Width | map with asText | collect as String border <- `${Corner}${String("").rightPadded(Width, Rule)}${Corner}` stdout.println("") stdout.println(` ${columnNumbers}`) stdout.println(border) for row in 0 ... Height - 1 line <- `${Edge}` for column in 0 ... Width - 1 line += cellText(board.getOrDefault(Coord(column, row), Player())) stdout.println(line + `${Edge}`) stdout.println(border) //The cell the player's tile lands in; unset when they QUIT or the input ends. The guard //on the reply is what stops the loop asking again once the input has ended. nextMove() -> player as Player board as Dict of (Coord, Player) <- rtn as Coord: Coord() quitting <- false while not rtn? and not quitting and not input.ended() stdout.println(say.askColumn(player)) if response <- input.next().upperCase().trim() column <- columnOf(response) if response == say.quit() quitting: true else if not column? stdout.println(say.notAColumn()) else if row <- landingRow(board, column) rtn: Coord(column, row) else stdout.println(say.columnFull()) showWinner() -> player as Player stdout.println(say.won(player)) showTie() stdout.println(say.tie()) showGoodbye() stdout.println(say.goodbye()) ended() as pure <- rtn as Boolean: input.ended() //The board holds only the tiles played, so an empty cell - or one off the board - reads //as an unset Player. The '.' exists only here, at the display edge. private cellText() -> occupant as Player <- rtn as String: occupant? <- $occupant : Empty override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: its rules, with no input or output. opponentOf() as pure -> player as Player <- rtn as Player: player == Player.X <- Player.O : Player.X isFull() as pure -> board as Dict of (Coord, Player) <- rtn as Boolean: length board == Width * Height //The column a reply names, counting from 0; unset unless the reply is 1 to Width. columnOf() as pure -> reply as String <- rtn as Integer: Integer() number <- Integer(reply) if number? and number >= 1 and number <= Width rtn: number - 1 //Where a tile dropped into 'column' lands: the lowest empty row, or unset when the //column is full. landingRow() as pure -> board as Dict of (Coord, Player) column as Integer <- rtn as Integer: Integer() for row in Height - 1 ... 0 if not rtn? and not (board contains Coord(column, row)) rtn: row //The steps from one cell of a line to the next: right, down, and the two diagonals //going down. Every line on the board runs one of these four ways from its first cell. lineSteps() as pure <- rtn as List of Coord: [Coord(1, 0), Coord(0, 1), Coord(1, 1), Coord(-1, 1)] //Whether 'player' holds Connect cells in a row from 'start'. A cell that is empty or off //the board compares as unset, which the 'if' does not take - so no bounds check is needed. lineFrom() -> board as Dict of (Coord, Player) start as Coord step as Coord player as Player <- rtn as Boolean: false held <- 0 for distance in 0 ... Connect - 1 cell <- Coord(start.x + (step.x * distance), start.y + (step.y * distance)) if board.getOrDefault(cell, Player()) == player held += 1 rtn: held == Connect isWinner() -> board as Dict of (Coord, Player) player as Player <- rtn as Boolean: false for column in 0 ... Width - 1 for row in 0 ... Height - 1 for step in lineSteps() rtn: rtn or lineFrom(board: board, start: Coord(column, row), step: step, player: player) defines function asText() as pure -> number as Integer <- rtn as String: $number //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 or types. The board and its marks are not here: the //Console draws them from the game's grid. defines text for "en" Say introduction() `Four in a Row, by Al Sweigart al@inventwithpython.com Two players take turns dropping tiles into one of seven columns, trying to make four in a row horizontally, vertically, or diagonally.` askColumn() -> player as Player `Player ${player}, enter a column or QUIT:` quit() "QUIT" goodbye() "Thanks for playing!" notAColumn() `Enter a number from 1 to ${Width}.` columnFull() "That column is full, select another one." won() -> player as Player `Player ${player} has won!` tie() "There is a tie!" defines program //Four in a Row (Big Book project 30). Two players drop X/O tiles into 7 columns; first to //four in a row (horizontal, vertical, or diagonal) wins. The game decides; the Console asks //and tells, in the player's words. FourInARow() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) console.showIntroduction() board <- Dict() of (Coord, Player) player <- Player.X running <- true while running and not console.ended() console.showBoard(board) if move <- console.nextMove(player, board) board += DictEntry(move, player) if isWinner(board, player) console.showBoard(board) console.showWinner(player) running: false else if isFull(board) console.showBoard(board) console.showTie() running: false else player: opponentOf(player) else //The player quit: the game says goodbye. At the end of the input it does not. console.showGoodbye() running: false
About this port
- Faithful to the book: the 7x6 grid with the
1234567guide and+-------+borders,.for empty; "Player X, enter a column or QUIT:"; "Enter a number from 1 to 7." and "That column is full, select another one."; a tile drops to the lowest empty row; four in a row horizontally, vertically or on either diagonal wins; "There is a tie!" when the board is full; QUIT prints "Thanks for playing!". - Model: the board is keyed by the
Coordrecord fromlib/Coord.ek9, compared by value. It holds only the tiles played: an empty cell is an absent key, read back as an unsetPlayer, and.appears only in the Console'scellText, at the display edge. The board is full when it holdsWidth * Heighttiles.