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 81 — Water Bucket Puzzle
This is project 81 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:
F
5
P
5
3
E
3
P
5
3
F
5
P
5
3
Run it with:
ek9 water-bucket-puzzle.ek9 < input.txt
It prints:
Water Bucket Puzzle, by Al Sweigart al@inventwithpython.com
Try to get 4L of water into one of these
buckets:
8| |
7| |
6| |
5| | 5| |
4| | 4| |
3| | 3| | 3| |
2| | 2| | 2| |
1| | 1| | 1| |
+------+ +------+ +------+
8L 5L 3L
You can:
(F)ill the bucket
(E)mpty the bucket
(P)our one bucket into another
(Q)uit
>
Select a bucket 8, 5, 3, or QUIT:
Try to get 4L of water into one of these
buckets:
8| |
7| |
6| |
5| | 5|WWWWWW|
4| | 4|WWWWWW|
3| | 3|WWWWWW| 3| |
2| | 2|WWWWWW| 2| |
1| | 1|WWWWWW| 1| |
+------+ +------+ +------+
8L 5L 3L
You can:
(F)ill the bucket
(E)mpty the bucket
(P)our one bucket into another
(Q)uit
>
Select a bucket 8, 5, 3, or QUIT:
Select a bucket to pour into: 8, 5, or 3
Try to get 4L of water into one of these
buckets:
8| |
7| |
6| |
5| | 5| |
4| | 4| |
3| | 3| | 3|WWWWWW|
2| | 2|WWWWWW| 2|WWWWWW|
1| | 1|WWWWWW| 1|WWWWWW|
+------+ +------+ +------+
8L 5L 3L
You can:
(F)ill the bucket
(E)mpty the bucket
(P)our one bucket into another
(Q)uit
>
Select a bucket 8, 5, 3, or QUIT:
Try to get 4L of water into one of these
buckets:
8| |
7| |
6| |
5| | 5| |
4| | 4| |
3| | 3| | 3| |
2| | 2|WWWWWW| 2| |
1| | 1|WWWWWW| 1| |
+------+ +------+ +------+
8L 5L 3L
You can:
(F)ill the bucket
(E)mpty the bucket
(P)our one bucket into another
(Q)uit
>
Select a bucket 8, 5, 3, or QUIT:
Select a bucket to pour into: 8, 5, or 3
Try to get 4L of water into one of these
buckets:
8| |
7| |
6| |
5| | 5| |
4| | 4| |
3| | 3| | 3| |
2| | 2| | 2|WWWWWW|
1| | 1| | 1|WWWWWW|
+------+ +------+ +------+
8L 5L 3L
You can:
(F)ill the bucket
(E)mpty the bucket
(P)our one bucket into another
(Q)uit
>
Select a bucket 8, 5, 3, or QUIT:
Try to get 4L of water into one of these
buckets:
8| |
7| |
6| |
5| | 5|WWWWWW|
4| | 4|WWWWWW|
3| | 3|WWWWWW| 3| |
2| | 2|WWWWWW| 2|WWWWWW|
1| | 1|WWWWWW| 1|WWWWWW|
+------+ +------+ +------+
8L 5L 3L
You can:
(F)ill the bucket
(E)mpty the bucket
(P)our one bucket into another
(Q)uit
>
Select a bucket 8, 5, 3, or QUIT:
Select a bucket to pour into: 8, 5, or 3
Try to get 4L of water into one of these
buckets:
8| |
7| |
6| |
5| | 5| |
4| | 4|WWWWWW|
3| | 3|WWWWWW| 3|WWWWWW|
2| | 2|WWWWWW| 2|WWWWWW|
1| | 1|WWWWWW| 1|WWWWWW|
+------+ +------+ +------+
8L 5L 3L
Good job! You solved it in 6 steps!
The program
#!ek9 defines module bigbook.waterbucket defines constant Goal <- 4 //The picture's own marks, data like the levels: one level of a bucket, full or empty (the //same width, so the bucket walls line up), and a bucket's base. Water <- "WWWWWW" Air <- " " Base <- " +------+" //One bucket's column in the picture: "8|WWWWWW|". CellWidth <- 9 defines type //The three buckets. The original keys its Dict by the strings "8", "5" and "3" and turns a //key back into a size with int(); here a bucket is a value and capacityOf gives its size. Bucket Eight Five Three Action Fill Empty Pour defines class //THE GAME's state: the water in each bucket. Filling, emptying and pouring are methods, so //the rules for changing a level live in one place. Buckets levels as Dict of (Bucket, Integer): Dict() of (Bucket, Integer) Buckets() for bucket in Bucket levels += DictEntry(bucket, 0) levelOf() as pure -> bucket as Bucket <- rtn as Integer: levels.getOrDefault(bucket, 0) holds() as pure -> amount as Integer <- rtn as Boolean? amounts <- cat levels.values() | collect as List of Integer rtn: amounts contains amount fill() -> bucket as Bucket levels += DictEntry(bucket, capacityOf(bucket)) emptyOut() -> bucket as Bucket levels += DictEntry(bucket, 0) //Pour until the source is empty or the target is full. `<?` gives the lesser of the two, //the original's min(). pour() -> source as Bucket target as Bucket poured <- (capacityOf(target) - levelOf(target)) <? levelOf(source) levels += DictEntry(source, levelOf(source) - poured) levels += DictEntry(target, levelOf(target) + poured) 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 goal, then the buckets. The picture is drawn, not worded: computed from the levels, //from the top of the tallest bucket down, with a column per bucket the type declares. showBuckets() -> buckets as Buckets stdout.println("") stdout.println(say.goal(Goal)) for height in capacityOf(Bucket.Eight) ... 1 stdout.println(rowAt(buckets, height)) base <- () is Function of (Bucket, String) as pure function r: Base bases <- cat Bucket | map with base | join with sideBySide | collect as String stdout.println(bases) labels <- List() of String for bucket in Bucket label <- say.litres(capacityOf(bucket)) labels += label.center(CellWidth) labelRow <- cat labels | join with sideBySide | collect as String stdout.println(labelRow) showChoices() stdout.println(say.choices()) //An action. Unset when the player quits or the input ends: either way there is no action. nextAction() <- rtn as Action: Action() quitting <- false while not rtn? and not quitting and not input.ended() stdout.println(say.prompt()) if line <- input.next() entry <- line.upperCase() if entry == say.quit() or entry == say.quitLetter() quitting: true else rtn: actionOf(entry) if not rtn? stdout.println(say.notAnAction()) //The bucket to fill, empty or pour from. Unset when the player quits or the input ends. nextBucket() <- rtn as Bucket: bucketAsked(say.askBucket(), true) //The bucket to pour into: no QUIT here, as in the book. Unset only when the input ends. nextTarget() <- rtn as Bucket: bucketAsked(say.askTarget(), false) showSolved() -> steps as Integer stdout.println(say.solved(steps)) showGoodbye() stdout.println(say.goodbye()) ended() as pure <- rtn as Boolean: input.ended() private bucketAsked() -> question as String allowQuit as Boolean <- rtn as Bucket: Bucket() quitting <- false while not rtn? and not quitting and not input.ended() stdout.println(question) if line <- input.next() entry <- line.upperCase() if allowQuit and entry == say.quit() quitting: true else rtn: bucketOf(entry) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: its rules, with no input or output. capacityOf() as pure -> bucket as Bucket <- rtn as Integer? switch bucket case Bucket.Eight rtn: 8 case Bucket.Five rtn: 5 case Bucket.Three rtn: 3 default rtn: Integer() //What the player typed, as a Bucket or an Action: the bucket sizes and the action letters are //the game's own data. Anything else is UNSET, which the caller tests with `?`. A bucket is //found from the buckets the type declares, by the size the player typed. bucketOf() as pure -> entry as String <- rtn as Bucket: Bucket() for bucket in Bucket if $capacityOf(bucket) == entry rtn: bucket actionOf() as pure -> entry as String <- rtn as Action? switch entry case "F" rtn: Action.Fill case "E" rtn: Action.Empty case "P" rtn: Action.Pour default rtn: Action() defines function //THE FLOW, one step of the original's main(): an action, a bucket, and for a pour the bucket //to pour into. False when the player quits, or the input ends, before the step is complete. takeTurn() -> buckets as Buckets console as Console <- rtn as Boolean: false if action <- console.nextAction() if source <- console.nextBucket() switch action case Action.Fill buckets.fill(source) rtn: true case Action.Empty buckets.emptyOut(source) rtn: true case Action.Pour if target <- console.nextTarget() buckets.pour(source, target) rtn: true default rtn: false defines function //The Console's drawing, a level at a time: computed output, with no words in it. //One level of one bucket: "5|WWWWWW|" with water at that level, "5| |" without, and //nothing where the bucket is shorter than the level. cellAt() as pure -> buckets as Buckets bucket as Bucket height as Integer <- rtn as String: "" if height <= capacityOf(bucket) content <- buckets.levelOf(bucket) >= height <- Water : Air rtn: `${height}|${content}|` rowAt() as pure -> buckets as Buckets height as Integer <- rtn as String? //Every cell is padded to the column width, so a short bucket leaves a blank column. cellOf <- (buckets, height) is Function of (Bucket, String) as pure function r: cellAt(buckets, t, height).rightPadded(CellWidth) rtn: cat Bucket | map with cellOf | join with sideBySide | collect as String sideBySide() as pure -> left as String right as String <- rtn as String: `${left} ${right}` //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 buckets' picture is not here: the Console //draws it from the levels. The litre unit is a word: the game holds the number. defines text for "en" Say introduction() "Water Bucket Puzzle, by Al Sweigart al@inventwithpython.com" goal() -> amount as Integer `Try to get ${amount}L of water into one of these buckets:` litres() -> amount as Integer `${amount}L` choices() `You can: (F)ill the bucket (E)mpty the bucket (P)our one bucket into another (Q)uit` prompt() "> " quit() "QUIT" quitLetter() "Q" notAnAction() "Enter F, E, P, or Q" askBucket() "Select a bucket 8, 5, 3, or QUIT:" askTarget() "Select a bucket to pour into: 8, 5, or 3" solved() -> steps as Integer `Good job! You solved it in ${steps} steps!` goodbye() "Thanks for playing!" defines program //Water Bucket Puzzle (Big Book project 81). Fill, empty and pour three buckets (8L, 5L, 3L) until //one holds exactly 4L. The game decides; the Console asks and tells, in the player's words. WaterBucket() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) console.showIntroduction() buckets <- Buckets() steps <- 0 running <- true while running and not console.ended() console.showBuckets(buckets) if buckets.holds(Goal) console.showSolved(steps) running: false else console.showChoices() if takeTurn(buckets, console) steps += 1 else console.showGoodbye() running: false