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 70 — Soroban Japanese Abacus

This is project 70 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:

42
quit

Run it with:

ek9 soroban.ek9 < input.txt

It prints:

Soroban - The Japanese Abacus
+=====================+
I O O O O O O O O O O I
I | | | | | | | | | | I
I | | | | | | | | | | I
+=====================+
I | | | | | | | | | | I
I | | | | | | | | | | I
I O O O O O O O O O O I
I O O O O O O O O O O I
I O O O O O O O O O O I
I O O O O O O O O O O I
+=====================+
  0 0 0 0 0 0 0 0 0 0
 +q w e r t y u i o p
 -a s d f g h j k l ;
(Enter a number, "quit", or a stream of up/down letters.)
+=====================+
I O O O O O O O O O O I
I | | | | | | | | | | I
I | | | | | | | | | | I
+=====================+
I | | | | | | | | O O I
I | | | | | | | | O O I
I O O O O O O O O O | I
I O O O O O O O O O | I
I O O O O O O O O | O I
I O O O O O O O O | O I
+=====================+
  0 0 0 0 0 0 0 0 4 2
 +q w e r t y u i o p
 -a s d f g h j k l ;
(Enter a number, "quit", or a stream of up/down letters.)

The program

#!ek9
defines module bigbook.soroban

  defines constant

    Columns <- 10
    Largest <- 9999999999
    RaiseKeys <- "qwertyuiop"
    LowerKeys <- "asdfghjkl;"

    //The abacus's own marks, data like the beads: the frame, a bead, a bare rod, and how far the
    //digits under the rods are indented (this port's own layout).
    Corner <- "+"
    Rail <- '='
    Frame <- "I"
    Bead <- " O"
    Rod <- " |"
    DigitIndent <- "  "

  defines type

    List of String

  defines class

    //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 abacus is drawn, not worded: computed from the number, a row of rods at a time, with
      //the frame sized to Columns.
      showAbacus()
        -> number as Integer
        shown <- $number
        digits <- shown.zeroPad(Columns)
        border <- frameEdge()
        stdout.println(border)
        for members in heavenRows()
          stdout.println(beadRow(digits, members))
        stdout.println(border)
        for members in earthRows()
          stdout.println(beadRow(digits, members))
        stdout.println(border)
        stdout.println(`${DigitIndent}${spacedOut(digits)}`)

      //The keys are the game's; the Console spaces them out under the rods they move.
      showControls()
        stdout.println(say.controls(spacedOut(RaiseKeys), spacedOut(LowerKeys)))

      //A number or a run of keys; unset when the player quits or the input ends.
      nextCommands()
        <- rtn as String: String()
        if line <- input.next()
          commands <- line.trim()
          if commands <> say.quit()
            rtn: commands

      ended() as pure
        <- rtn as Boolean: input.ended()

      override operator ? as pure
        <- rtn as Boolean: true

  defines function

    //THE GAME: the abacus's rules, with no input or output.

    //The value of one bead step in each column, left to right. Key n of RaiseKeys and of
    //LowerKeys works on column n, so indexOf on the key string finds the column.
    placeValue() as pure
      -> column as Integer
      <- rtn as Integer: 0
      values <- [1000000000, 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1]
      rtn: values.getOrDefault(column, 0)

    //What one control key adds to the abacus; 0 for any other character.
    keyAmount() as pure
      -> key as Character
      <- rtn as Integer: 0
      if raised <- RaiseKeys.indexOf(key)
        rtn: placeValue(raised)
      else if lowered <- LowerKeys.indexOf(key)
        rtn: -placeValue(lowered)

    //A text of digits that does not fit in an Integer is larger than the abacus anyway.
    clamped() as pure
      -> amount as Integer
      <- rtn as Integer: Largest
      if amount? and amount < 0
        rtn: 0
      else if amount? and amount <= Largest
        rtn: amount

    //The number on the abacus once the player's commands are applied: a number sets it, a run
    //of keys moves its beads up and down.
    abacusAfter()
      ->
        abacus as Integer
        commands as String
      <- rtn as Integer: Integer()

      if commands matches /\d+/
        rtn: clamped(Integer(commands))
      else
        //A copy, so the keys do not change abacus itself before it is clamped.
        total <- Integer(abacus)
        for key in commands
          total += keyAmount(key)
        rtn: clamped(total)

    //The book's rules for which digits put a bead on each row: two heaven rows either side of
    //a bare rod row (no digit is in ""), then six earth rows.
    heavenRows() as pure
      <- rtn as List of String: ["01234", "", "56789"]

    earthRows() as pure
      <- rtn as List of String: ["12346789", "234789", "034589", "014569", "012567", "01235678"]

  defines function

    //The Console's drawing: computed output, with no words in it.

    //The top, middle and bottom of the frame: a rail as wide as the rods and the frame's sides.
    frameEdge()
      <- rtn as String: String()
      rail <- String("").rightPadded((Columns * 2) + 1, Rail)
      rtn: `${Corner}${rail}${Corner}`

    //One row of rods: a bead in each column whose digit is in members, else a bare rod.
    beadRow()
      ->
        digits as String
        members as String
      <- rtn as String: Frame

      for digit in digits
        rtn += members contains $digit <- Bead : Rod
      rtn += ` ${Frame}`

    //The characters of a text with a space between each: "0 0 4 2", "q w e".
    spacedOut()
      -> text as String
      <- rtn as String: ""
      first <- true
      for character in text
        rtn += first <- $character : ` ${character}`
        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 abacus and its marks are not here: the
  //Console draws them from the number.
  defines text for "en"

    Say

      introduction()
        "Soroban - The Japanese Abacus"

      controls()
        ->
          raiseKeys as String
          lowerKeys as String
        ` +${raiseKeys}
 -${lowerKeys}
(Enter a number, "quit", or a stream of up/down letters.)`

      quit()
        "quit"

  defines program

    //Soroban Japanese Abacus (Big Book project 70). Shows a number as abacus beads; type a number
    //to set it, or a run of keys to move the beads. The game decides; the Console asks and tells,
    //in the player's words.
    Soroban()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))

      console.showIntroduction()
      abacus <- 0
      running <- true
      while running and not console.ended()
        console.showAbacus(abacus)
        console.showControls()
        if commands <- console.nextCommands()
          abacus: abacusAfter(abacus, commands)
        else
          running: false

About this port