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