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 76 — Tic-Tac-Toe
This is project 76 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
4
2
5
3
Run it with:
ek9 tic-tac-toe.ek9 < input.txt
It prints:
Welcome to Tic-Tac-Toe!
| | 1 2 3
-+-+-
| | 4 5 6
-+-+-
| | 7 8 9
What is X's move? (1-9)
X| | 1 2 3
-+-+-
| | 4 5 6
-+-+-
| | 7 8 9
What is O's move? (1-9)
X| | 1 2 3
-+-+-
O| | 4 5 6
-+-+-
| | 7 8 9
What is X's move? (1-9)
X|X| 1 2 3
-+-+-
O| | 4 5 6
-+-+-
| | 7 8 9
What is O's move? (1-9)
X|X| 1 2 3
-+-+-
O|O| 4 5 6
-+-+-
| | 7 8 9
What is X's move? (1-9)
X|X|X 1 2 3
-+-+-
O|O| 4 5 6
-+-+-
| | 7 8 9
X has won the game!
Thanks for playing!
The program
#!ek9 defines module bigbook.tictactoe defines constant //The board is Width spaces a side, numbered 1 to Spaces reading across. Width <- 3 Spaces <- 9 //The board's own marks, data like the X and O tiles: an empty space, the lines between the //spaces, and the drawing's indent (this port's own layout). Empty <- " " Edge <- "|" Crossing <- "+" Rule <- "-" Indent <- " " 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 Dict of (Integer, Player) List of String 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, not worded: computed from the game's spaces a row at a time, with the //space numbers beside each row as a guide. showBoard() -> board as Dict of (Integer, Player) rules <- List() of String for column in 1 ... Width rules += Rule stdout.println("") for row in 0 ... Width - 1 if row > 0 stdout.println(`${Indent}${joined(rules, Crossing)}`) marks <- List() of String numbers <- List() of String for column in 1 ... Width space <- (row * Width) + column marks += cellText(board.getOrDefault(space, Player())) numbers += $space stdout.println(`${Indent}${joined(marks, Edge)} ${joined(numbers, " ")}`) //The space the player claims; unset only if the input ends first. nextMove() -> player as Player board as Dict of (Integer, Player) <- rtn as Integer: Integer() while not rtn? and not input.ended() stdout.println(say.askMove(player)) if reply <- input.next() rtn: spaceFrom(board, reply.trim()) 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 spaces claimed, so an empty space reads as an unset Player. The //blank mark 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. //The eight lines of three spaces that win: three rows, three columns, two diagonals. winningLines() as pure <- rtn as List of List of Integer: [ [1, 2, 3], [4, 5, 6], [7, 8, 9], [1, 4, 7], [2, 5, 8], [3, 6, 9], [1, 5, 9], [3, 5, 7] ] opponentOf() as pure -> player as Player <- rtn as Player: Player() switch player case Player.X rtn: Player.O case Player.O rtn: Player.X default rtn: Player() //The space a reply names, or unset when it is not a number from 1 to 9 or the space is taken. spaceFrom() as pure -> board as Dict of (Integer, Player) reply as String <- rtn as Integer: Integer() space <- Integer(reply) if space? and space >= 1 and space <= Spaces and not (board contains space) rtn: space ownsLine() as pure -> board as Dict of (Integer, Player) line as List of Integer player as Player <- rtn as Boolean: true for space in line if not (board contains space) or board.getOrDefault(space, Player()) <> player rtn: false hasWon() as pure -> board as Dict of (Integer, Player) player as Player <- rtn as Boolean: false for line in winningLines() if ownsLine(board, line, player) rtn: true isFull() as pure -> board as Dict of (Integer, Player) <- rtn as Boolean: length board == Spaces defines function //The Console's drawing: computed output, with no words in it. //The parts with the separator between each: "X|O| ", "1 2 3". Whether a part is the first //is a flag: it says what is meant, where rtn.empty() would ask about the text built so far. joined() -> parts as List of String separator as String <- rtn as String: "" first <- true for part in parts rtn += first <- part : `${separator}${part}` first: false //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. The board and its marks are not here: the Console draws //them from the game's spaces. defines text for "en" Say introduction() "Welcome to Tic-Tac-Toe!" askMove() -> player as Player `What is ${player}'s move? (1-${Spaces})` won() -> player as Player `${player} has won the game!` tie() "The game is a tie!" goodbye() "Thanks for playing!" defines program //Tic-Tac-Toe (Big Book project 76). Two players take turns to claim a space; three in a line //wins. The game decides; the Console asks and tells, in the players' words. TicTacToe() //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 (Integer, 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 hasWon(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) console.showGoodbye()
About this port
- Faithful to the book: "Welcome to Tic-Tac-Toe!"; the board drawn with the 1-9 space guide beside each row
(
X|O|X 1 2 3); the "What is X's move? (1-9)" prompt, repeated until the reply names a free space from 1 to 9; the eight winning lines; "X has won the game!", "The game is a tie!", "Thanks for playing!". - Model: the players are a
Playerenumeration (X,O), and the board is aDict of (Integer, Player)holding only the claimed spaces. An empty space is absent, so there is no blank mark to compare against, and "the board is full" islength board == Spaces.