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 68 — Sliding Tile Puzzle
This is project 68 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:
W
QUIT
Run it with:
BIGBOOK_SEED=1 ek9 slidingtilepuzzle.ek9 < input.txt
It prints:
Sliding Tile Puzzle, by Al Sweigart al@inventwithpython.com
Use the WASD keys to move the tiles
back into their original order:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15
+----+----+----+----+
| | | | |
| | 2 | 3 | 4 |
| | | | |
+----+----+----+----+
| | | | |
| 11 | 1 | 5 | 7 |
| | | | |
+----+----+----+----+
| | | | |
| 13 | 6 | 15 | 9 |
| | | | |
+----+----+----+----+
| | | | |
| 10 | 12 | 14 | 8 |
| | | | |
+----+----+----+----+
(W)
Enter WASD (or QUIT): (A) ( ) ( )
+----+----+----+----+
| | | | |
| 11 | 2 | 3 | 4 |
| | | | |
+----+----+----+----+
| | | | |
| | 1 | 5 | 7 |
| | | | |
+----+----+----+----+
| | | | |
| 13 | 6 | 15 | 9 |
| | | | |
+----+----+----+----+
| | | | |
| 10 | 12 | 14 | 8 |
| | | | |
+----+----+----+----+
(W)
Enter WASD (or QUIT): (A) (S) ( )
The program
#!ek9 defines module bigbook.slidingtile defines constant //The board is Size squares a side, numbered 0 to Last in each direction. Size <- 4 Last <- 3 Scramble <- 200 //The board's own marks, data like the tiles: the corners, rules and edges of the grid, the //inside of a square, the blank square, and how wide a tile's number is drawn. Corner <- "+" CellRule <- "----" Edge <- "|" CellSpace <- " " Blank <- " " LabelWidth <- 2 //A key the prompt does not offer, because that slide is not possible. NoKey <- " " defines type //A numbered tile is 1 to 15 and nothing else. The blank square is an UNSET Tile, so //there is no " " sentinel in the board: the blank text appears only when it is drawn. Tile as Integer constrain as >= 1 and <= 15 //The four slides, named for the way a TILE moves into the blank (W, A, S, D). An //enumeration iterates in declaration order, so possibleSlides lists them in the book's order. Slide Up Left Down Right Dict of (Coord, Tile) DictEntry of (Coord, Tile) List of Slide defines record //A board position used as a Dict key, compared by value (the shape of lib/Coord.ek9). Coord x as Integer: Integer() y as Integer: Integer() default Coord() as pure //Integer(px) snapshots the argument: a key holding the caller's object would change //when the caller's variable changed, and the Dict would no longer find it. 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 GAME's state: the tiles, and where the blank square is. Board tiles as Dict of (Coord, Tile): Dict() of (Coord, Tile) blank as Coord: Coord(Last, Last) //A new board is the solved puzzle. Board() for y in 0 ... Last for x in 0 ... Last at <- Coord(x, y) tiles += DictEntry(at, homeTile(at)) //The tile at a position; unset for the blank square. tileAt() as pure -> at as Coord <- rtn as Tile: tiles.getOrDefault(at, Tile()) //A slide is possible when there is a tile on the far side of the blank; an off-board //source is simply not in the Dict, so there is no edge arithmetic to get wrong. canSlide() as pure -> slide as Slide <- rtn as Boolean: tiles contains sourceOf(blank, slide) possibleSlides() <- rtn as List of Slide: List() of Slide for slide in Slide if canSlide(slide) rtn += slide //The caller checks canSlide first, as the book's makeMove assumes a valid move. slideTile() -> direction as Slide source <- sourceOf(blank, direction) tiles += DictEntry(blank, tiles.getOrDefault(source, Tile())) tiles += DictEntry(source, Tile()) blank: source isSolved() <- rtn as Boolean: blank == Coord(Last, Last) for y in 0 ... Last for x in 0 ... Last at <- Coord(x, y) if at <> blank and tiles.getOrDefault(at, Tile()) <> homeTile(at) rtn: false default operator ? //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 tiles a row at a time, with the //grid sized to Size. showBoard() -> board as Board border <- gridLine(Corner, CellRule) spacer <- gridLine(Edge, CellSpace) stdout.println("") stdout.println(border) for y in 0 ... Last stdout.println(spacer) line <- Edge for x in 0 ... Last line += ` ${tileText(board.tileAt(Coord(x, y)))} ${Edge}` stdout.println(line) stdout.println(spacer) stdout.println(border) //Asks until the player picks a possible slide. QUIT, or the end of the input, gives back //an UNSET Slide, which the caller reads as "stop playing". nextSlide() -> board as Board <- rtn as Slide: Slide() quitting <- false while not rtn? and not quitting and not input.ended() showPrompt(board) if line <- input.next() response <- line.upperCase().trim() if response == say.quit() quitting: true else if chosen <- slideOf(response) then board.canSlide(chosen) rtn: chosen showWon() stdout.println(say.won()) ended() as pure <- rtn as Boolean: input.ended() //The prompt's words, then a key for each slide the board allows, laid out as the keys sit //on a keyboard: the Up key above the Down key, wherever the words end. private showPrompt() -> board as Board label <- say.askSlide() left <- keyBox(board, Slide.Left) labelWidth <- length label leftWidth <- length left upIndent <- String("").rightPadded(labelWidth + leftWidth + 2, ' ') stdout.println(`${upIndent}${keyBox(board, Slide.Up)}`) stdout.println(`${label} ${left} ${keyBox(board, Slide.Down)} ${keyBox(board, Slide.Right)}`) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: its rules, with no input or output. //Where the tile that slides in a direction comes from: the neighbour of the blank on //the opposite side. It may be off the board, and then the board simply has no tile there. sourceOf() as pure -> blank as Coord slide as Slide <- rtn as Coord: Coord() switch slide case Slide.Up rtn: Coord(blank.x, blank.y + 1) case Slide.Left rtn: Coord(blank.x + 1, blank.y) case Slide.Down rtn: Coord(blank.x, blank.y - 1) case Slide.Right rtn: Coord(blank.x - 1, blank.y) default rtn: Coord() //The key for each slide: the game's own data, as the player types it. keyOf() as pure -> slide as Slide <- rtn as String: String() switch slide case Slide.Up rtn: "W" case Slide.Left rtn: "A" case Slide.Down rtn: "S" case Slide.Right rtn: "D" default rtn: String() //Anything that is not one of the four keys parses to an UNSET Slide. slideOf() as pure -> key as String <- rtn as Slide: (key == "W" <- Slide.Up : key == "A" <- Slide.Left : key == "S" <- Slide.Down : key == "D" <- Slide.Right : Slide()) //The tile that belongs at a position when the puzzle is solved: 1 to 15 reading across, //and no tile (the blank) in the last square. homeTile() as pure -> at as Coord <- rtn as Tile: Tile() number <- (at.y * Size) + at.x + 1 if at.x <> Last or at.y <> Last rtn: Tile(number) //The book's getNewPuzzle: make random possible slides from the solved state, so the //scrambled puzzle is always solvable. scrambled() -> rng as Random <- rtn as Board: Board() for move in 1 ... Scramble slides <- rtn.possibleSlides() rtn.slideTile(slides.getOrDefault(rng.next(0, length slides - 1), Slide())) defines function //The Console's drawing: computed output, with no words in it. //A line across the grid: the mark between the squares, and what fills each square. The //line starts as a COPY of the mark: '+=' changes the String it is applied to, and that must //not be the caller's mark. gridLine() -> mark as String fill as String <- rtn as String: String(mark) for x in 0 ... Last rtn += `${fill}${mark}` //Two characters wide, as the book's labels are, so every column of the grid lines up. tileText() as pure -> tile as Tile <- rtn as String: Blank if tile? number <- $tile rtn: number.rightPadded(LabelWidth) //A slide's key in brackets, or an empty pair when the board does not allow that slide. keyBox() as pure -> board as Board slide as Slide <- rtn as String: String() key <- board.canSlide(slide) <- keyOf(slide) : NoKey rtn: `(${key})` //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 or types. The board and the prompt's keys are not here: //the Console draws them from the game's tiles and slides. The solved order in the //introduction is the book's fixed picture, so it is part of the words. defines text for "en" Say introduction() `Sliding Tile Puzzle, by Al Sweigart al@inventwithpython.com Use the WASD keys to move the tiles back into their original order: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15` askSlide() "Enter WASD (or QUIT):" quit() "QUIT" won() "You won!" defines program //Sliding Tile Puzzle (Big Book project 68). The classic 4x4 15-puzzle: slide the tiles back //into order. The game decides; the Console asks and tells, in the player's words. The golden //replays a seeded game (seed.txt). SlidingTile() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) rng <- randomSource() console.showIntroduction() board <- scrambled(rng) running <- true while running and not console.ended() console.showBoard(board) if board.isSolved() console.showWon() running: false else if slide <- console.nextSlide(board) board.slideTile(slide) else running: false
About this port
- Faithful to the book: the 4x4 15-puzzle; the intro showing the goal order; the board drawn
as a bordered grid; the WASD prompt shows a key only when that slide is possible; QUIT exits;
restoring 1..15 prints "You won!". The puzzle is 200 random possible slides from the solved
board (
getNewPuzzle), so it is always solvable. - Deliberate divergences: the "Press Enter to begin..." pause is omitted; the grid is this port's own layout (the book's exact spacing does not survive PDF extraction).
- Model:
Boardis a class holding aDict of (Coord, Tile)and the blank'sCoord. The book searches the grid for the blank on every move; here the blank's position is part of the state.Tileis an Integer constrained to 1..15, and the blank square is an UNSETTile, so the two-space blank text exists only intileText, where the Console draws the board.Slideis an enumeration (Up,Left,Down,Right).slideOfparses a key to aSlideor to unset, and the Console'snextSlidereturns unset for QUIT or the end of the input.- A slide is possible when the Dict holds a tile on the far side of the blank (
canSlide). An off-board position is simply not a key, so the book's four edge checks are not needed.