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 77 — Tower of Hanoi
This is project 77 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:
AC
AC
XY
A
QUIT
Run it with:
ek9 tower-of-hanoi.ek9 < input.txt
It prints:
THE TOWER OF HANOI
Move all disks from tower A to another tower.
A larger disk may never rest on a smaller disk. Enter a move like AB, or QUIT.
#|# | |
##|## | |
###|### | |
####|#### | |
#####|##### | |
=========== =========== ===========
A B C
Enter a move (e.g. AB), or QUIT:
| | |
##|## | |
###|### | |
####|#### | |
#####|##### | #|#
=========== =========== ===========
A B C
Enter a move (e.g. AB), or QUIT:
Cannot put a larger disk on a smaller disk.
| | |
##|## | |
###|### | |
####|#### | |
#####|##### | #|#
=========== =========== ===========
A B C
Enter a move (e.g. AB), or QUIT:
Invalid move. Use two different letters from A, B, C (or QUIT).
| | |
##|## | |
###|### | |
####|#### | |
#####|##### | #|#
=========== =========== ===========
A B C
Enter a move (e.g. AB), or QUIT:
Enter exactly two letters, like AB.
| | |
##|## | |
###|### | |
####|#### | |
#####|##### | #|#
=========== =========== ===========
A B C
Enter a move (e.g. AB), or QUIT:
The program
#!ek9 defines module bigbook.hanoi defines constant TotalDisks <- 5 MoveLength <- 2 //The drawing's own marks, data like the tower letters: a disk's bars, the pole, the base, //and the gap between two towers (this port's own layout). Bar <- '#' Pole <- "|" Rail <- '=' TowerGap <- " " defines type //A move names its towers by letter; parsing a letter that is not A, B or C gives an UNSET //Tower, which is how a bad move is recognised. The letters are the game's data, not words. Tower A B C //Why an entry is not a move, in the order the book checks. MoveProblem NotTwoLetters NotTwoTowers EmptyTower LargerOnSmaller List of Integer List of String Dict of (Tower, List of Integer) defines class //THE GAME's state: the three stacks of disks. Each stack runs from the bottom disk to the top one, and a disk //is its size, so the rules of the puzzle are comparisons of sizes. Towers stacks as Dict of (Tower, List of Integer): Dict() of (Tower, List of Integer) //Every disk starts on tower A, largest at the bottom. Towers() start <- List() of Integer //A descending range yields a fresh Integer per step, so += needs no Integer() snapshot. for size in TotalDisks ... 1 start += size for tower in Tower stacks += DictEntry(tower, tower == Tower.A <- start : List() of Integer) disks() as pure -> tower as Tower <- rtn as List of Integer: stacks.getOrDefault(tower, List() of Integer) //The size of the top disk; unset when the tower is empty. topDisk() as pure -> tower as Tower <- rtn as Integer: Integer() stack <- disks(tower) rtn: stack.getOrDefault(length stack - 1, Integer()) //Unset when the entry is a move the rules allow; otherwise the first thing wrong with it. problemWith() as pure -> entry as String <- rtn as MoveProblem: MoveProblem() if length entry <> MoveLength rtn: MoveProblem.NotTwoLetters else source <- sourceIn(entry) target <- targetIn(entry) moving <- topDisk(source) resting <- topDisk(target) if not source? or not target? or source == target rtn: MoveProblem.NotTwoTowers else if not moving? rtn: MoveProblem.EmptyTower else if resting? and resting < moving rtn: MoveProblem.LargerOnSmaller move() -> source as Tower target as Tower disk <- topDisk(source) stacks += DictEntry(source, disks(source) - disk) stacks += DictEntry(target, disks(target) + disk) isSolved() as pure <- rtn as Boolean: length disks(Tower.B) == TotalDisks or length disks(Tower.C) == TotalDisks override operator ? as pure <- rtn as Boolean: true //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 towers are drawn, not worded: side by side, top level first, with a base under each //and its letter under its pole. showTowers() -> towers as Towers for level in TotalDisks - 1 ... 0 stdout.println(levelLine(towers, level)) base <- filled(poleWidth(), Rail) stdout.println(`${base}${TowerGap}${base}${TowerGap}${base}`) stdout.println(letterLine()) //A move the rules allow, as two letters ("AC"); unset when the player QUITs or the input //ends. The towers are drawn again before each try, as the book draws them. nextMove() -> towers as Towers <- rtn as String: String() quitting <- false while not rtn? and not quitting and not input.ended() showTowers(towers) stdout.println("") stdout.println(say.askMove()) if line <- input.next() entry <- line.upperCase().trim() if entry == say.quit() quitting: true else if problem <- towers.problemWith(entry) showProblem(problem, entry) else rtn: entry showSolved() stdout.println(say.solved()) ended() as pure <- rtn as Boolean: input.ended() private showProblem() -> problem as MoveProblem entry as String switch problem case MoveProblem.NotTwoLetters stdout.println(say.notTwoLetters()) case MoveProblem.NotTwoTowers stdout.println(say.notTwoTowers(towerLetters())) case MoveProblem.EmptyTower stdout.println(say.emptyTower(sourceIn(entry))) case MoveProblem.LargerOnSmaller stdout.println(say.largerOnSmaller()) default //problemWith returns a set problem or none; the compiler requires this branch (E07320). stdout.println(say.notTwoLetters()) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: its rules, with no input or output. //The towers a move names: "AC" moves the top disk from A to C. Unset for a letter that is //not a tower. sourceIn() as pure -> entry as String <- rtn as Tower: Tower(entry.subString(0, 1)) targetIn() as pure -> entry as String <- rtn as Tower: Tower(entry.subString(1, MoveLength)) defines function //The Console's drawing: computed output, with no words in it. //`width` copies of `fill`: the empty string padded out to the width. filled() as pure -> width as Integer fill as Character <- rtn as String: String() nothing <- "" rtn: nothing.rightPadded(width, fill) //The widest disk: TotalDisks bars on each side of the pole. A constant must be a literal, //so the width is computed here rather than restated beside TotalDisks. poleWidth() as pure <- rtn as Integer: TotalDisks + 1 + TotalDisks //One level of a tower: a disk of `size` (0 is the bare pole), centred on the pole. diskLevel() as pure -> size as Integer <- rtn as String: String() bars <- filled(size, Bar) disk <- `${bars}${Pole}${bars}` rtn: disk.center(poleWidth()) beside() as pure -> left as String right as String <- rtn as String: `${left}${TowerGap}${right}` //One level across all three towers. A level above a tower's top disk reads as size 0. levelLine() -> towers as Towers level as Integer <- rtn as String: String() cells <- List() of String for tower in Tower cells += diskLevel(towers.disks(tower).getOrDefault(level, 0)) rtn: cat cells | join with beside | collect as String //Each tower's letter under its pole. letterLine() <- rtn as String: "" indent <- filled(TotalDisks, ' ') first <- true for tower in Tower rtn += first <- `${indent}${tower}` : `${indent}${TowerGap}${indent}${tower}` first: false //The letters a move may use, for the words: "A, B, C". towerLetters() <- rtn as String: "" first <- true for tower in Tower rtn += first <- $tower : `, ${tower}` 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 player sees or types. The towers, their marks and their letters are //not here: the Console draws them from the game's stacks. defines text for "en" Say introduction() `THE TOWER OF HANOI Move all disks from tower A to another tower. A larger disk may never rest on a smaller disk. Enter a move like AB, or QUIT.` askMove() "Enter a move (e.g. AB), or QUIT:" quit() "QUIT" notTwoLetters() "Enter exactly two letters, like AB." notTwoTowers() -> letters as String `Invalid move. Use two different letters from ${letters} (or QUIT).` emptyTower() -> tower as Tower `Tower ${tower} is empty.` largerOnSmaller() "Cannot put a larger disk on a smaller disk." solved() "You have solved the puzzle! Well done!" defines program //Tower of Hanoi (Big Book project 77). Move the disks from tower A onto another tower, one at a //time, never resting a larger disk on a smaller one. The game decides; the Console asks and //tells, in the player's words. TowerOfHanoi() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) console.showIntroduction() towers <- Towers() running <- true while running and not console.ended() if entry <- console.nextMove(towers) towers.move(sourceIn(entry), targetIn(entry)) if towers.isSolved() console.showTowers(towers) console.showSolved() running: false else running: false
About this port
- Faithful to the book: an interactive game (TOTAL_DISKS = 5; towers A/B/C; the state is drawn each turn; the player types two-letter moves like
AB; illegal moves are rejected; win message "You have solved the puzzle! Well done!"). - Idioms:
Toweris an enumeration.Tower("X")is unset, so onenot source?test replaces the original's check that each letter is one of A, B, C, andfor tower in Towerwalks the towers when drawing them.- The game state is a
Towersclass over aDict of (Tower, List of Integer). ItsproblemWithisas pureand returns why a move is illegal (aMoveProblem), or unset when it is legal, so the Console asksif problem <- towers.problemWith(entry)and only a legal move reachesmove. topDiskis unset for an empty tower rather than a 0 sentinel; "is the tower empty?" isnot moving?.- A disk is its size and sizes are unique, so taking the top disk off is
stack - disk, and putting it on isstack + disk. - The starting tower is a descending range,
for size in TotalDisks ... 1, because a range binds a fresh Integer each step. Counting down withsize -= 1would change the very Integer already added to the list:-=mutates its receiver (ek9 -h Integershows it is notas pure). - Drawing uses
String.centerfor each disk,rightPaddedwith a fill character for runs of#,=and spaces, and ajoinstream for each level.