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