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 43 — Mancala

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

A
G
QUIT

Run it with:

ek9 mancala.ek9 < input.txt

It prints:

Mancala, by Al Sweigart al@inventwithpython.com
              Player 2
+------+------+------+------+------+------+
| G: 4 | H: 4 | I: 4 | J: 4 | K: 4 | L: 4 |
+------+------+------+------+------+------+
  Store 2: 0                       Store 1: 0
+------+------+------+------+------+------+
| A: 4 | B: 4 | C: 4 | D: 4 | E: 4 | F: 4 |
+------+------+------+------+------+------+
              Player 1
Player 1, choose move: A-F (or QUIT)
              Player 2
+------+------+------+------+------+------+
| G: 4 | H: 4 | I: 4 | J: 4 | K: 4 | L: 4 |
+------+------+------+------+------+------+
  Store 2: 0                       Store 1: 0
+------+------+------+------+------+------+
| A: 0 | B: 5 | C: 5 | D: 5 | E: 5 | F: 4 |
+------+------+------+------+------+------+
              Player 1
Player 2, choose move: G-L (or QUIT)
              Player 2
+------+------+------+------+------+------+
| G: 0 | H: 4 | I: 4 | J: 4 | K: 4 | L: 4 |
+------+------+------+------+------+------+
  Store 2: 1                       Store 1: 0
+------+------+------+------+------+------+
| A: 1 | B: 6 | C: 6 | D: 5 | E: 5 | F: 4 |
+------+------+------+------+------+------+
              Player 1
Player 1, choose move: A-F (or QUIT)
Thanks for playing!

The program

#!ek9
defines module bigbook.mancala

  defines constant

    SeedsPerPit <- 4

    //The board's own marks, data like the pit letters: the edges of the drawing, and where its
    //labels sit (this port's own layout).
    PitEdge <- "+------"
    Corner <- "+"
    Divider <- "|"
    PlayerLabelIndent <- 14
    StoreLabelIndent <- 2
    RightStoreColumn <- 35

  defines type

    Player
      First
      Second

    //The twelve pits, lettered as the players type them, and the two stores.
    Pit
      A
      B
      C
      D
      E
      F
      G
      H
      I
      J
      K
      L
      StoreOne
      StoreTwo

    //Why a pit cannot be sown, in the order the book checks.
    PitProblem
      NotYourPit
      EmptyPit

    List of Pit
    Dict of (Pit, Pit)
    Dict of (Pit, Integer)

  defines record

    //What belongs to one player: the number shown on screen, their six pits in display order,
    //and their store.
    Side
      number as String: String()
      pits as List of Pit: List() of Pit
      store as Pit: Pit()

      Side()
        ->
          number as String
          pits as List of Pit
          store as Pit

        this.number: number
        this.pits: pits
        this.store: store

      default operator ?

  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 players see or type 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 board is drawn from the game's pits, with the edges sized to a side's pits. The labels
      //on it are words; where they sit is the drawing's.
      showBoard()
        -> board as Dict of (Pit, Integer)
        top <- sideOf(Player.Second)
        bottom <- sideOf(Player.First)
        edge <- edgeOf(top)
        playerIndent <- String("").rightPadded(PlayerLabelIndent, ' ')
        stdout.println(`${playerIndent}${say.player(top.number)}`)
        stdout.println(edge)
        stdout.println(pitRow(board, top))
        stdout.println(edge)
        stdout.println(storeLine(board, top, bottom))
        stdout.println(edge)
        stdout.println(pitRow(board, bottom))
        stdout.println(edge)
        stdout.println(`${playerIndent}${say.player(bottom.number)}`)

      //The pit the player chooses, or unset when they QUIT or the input ends.
      nextMove()
        ->
          side as Side
          board as Dict of (Pit, Integer)
        <- rtn as Pit: Pit()

        quitting <- false
        while not rtn? and not quitting and not input.ended()
          stdout.println(say.askMove(side.number, pitRange(side)))
          if line <- input.next()
            reply <- line.trim().upperCase()
            //Constructing an enumeration from text gives an unset value for anything that is not a pit.
            pit <- Pit(reply)
            if reply == say.quit()
              quitting: true
            else if problem <- problemWith(side, board, pit)
              showProblem(problem)
            else
              rtn: pit

      showWinner()
        -> winner as Player
        stdout.println(say.won(sideOf(winner).number))

      showTie()
        stdout.println(say.tie())

      showGoodbye()
        stdout.println(say.goodbye())

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

      private showProblem()
        -> problem as PitProblem
        switch problem
          case PitProblem.NotYourPit
            stdout.println(say.notYourPit())
          case PitProblem.EmptyPit
            stdout.println(say.emptyPit())
          default
            //problemWith returns a set problem or none; the compiler requires this branch (E07320).
            stdout.println(say.notYourPit())

      //Each store's label and its seeds, the second player's store on the left.
      private storeLine()
        ->
          board as Dict of (Pit, Integer)
          left as Side
          right as Side
        <- rtn as String: String()

        indent <- String("").rightPadded(StoreLabelIndent, ' ')
        leftStore <- `${indent}${say.store(left.number)}${cell(board, left.store)}`
        rtn: `${leftStore.rightPadded(RightStoreColumn)}${say.store(right.number)}${cell(board, right.store)}`

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

  defines function

    //THE GAME: its rules, with no input or output.

    sideOf()
      -> player as Player
      <- rtn as Side: Side()
      switch player
        case Player.First
          rtn: Side("1", [Pit.A, Pit.B, Pit.C, Pit.D, Pit.E, Pit.F], Pit.StoreOne)
        case Player.Second
          rtn: Side("2", [Pit.G, Pit.H, Pit.I, Pit.J, Pit.K, Pit.L], Pit.StoreTwo)
        default
          rtn: Side()

    opponentOf() as pure
      -> player as Player
      <- rtn as Player: Player()
      switch player
        case Player.First
          rtn: Player.Second
        case Player.Second
          rtn: Player.First
        default
          rtn: Player()

    //Seeds travel counterclockwise: along Player 1's pits to their store, back along Player 2's.
    nextPits() as pure
      <- rtn as Dict of (Pit, Pit): {
        Pit.A: Pit.B, Pit.B: Pit.C, Pit.C: Pit.D, Pit.D: Pit.E, Pit.E: Pit.F, Pit.F: Pit.StoreOne,
        Pit.StoreOne: Pit.L,
        Pit.L: Pit.K, Pit.K: Pit.J, Pit.J: Pit.I, Pit.I: Pit.H, Pit.H: Pit.G, Pit.G: Pit.StoreTwo,
        Pit.StoreTwo: Pit.A
      }

    oppositePits() as pure
      <- rtn as Dict of (Pit, Pit): {
        Pit.A: Pit.G, Pit.B: Pit.H, Pit.C: Pit.I, Pit.D: Pit.J, Pit.E: Pit.K, Pit.F: Pit.L,
        Pit.G: Pit.A, Pit.H: Pit.B, Pit.I: Pit.C, Pit.J: Pit.D, Pit.K: Pit.E, Pit.L: Pit.F
      }

    seedsIn() as pure
      ->
        board as Dict of (Pit, Integer)
        pit as Pit
      <- rtn as Integer: board.getOrDefault(pit, 0)

    seedsOnSide()
      ->
        board as Dict of (Pit, Integer)
        side as Side
      <- rtn as Integer: 0

      for pit in side.pits
        rtn += seedsIn(board, pit)

    //The letters a player may choose from, such as "A-F".
    pitRange() as pure
      -> side as Side
      <- rtn as String: `${side.pits.getOrDefault(0, Pit())}-${side.pits.getOrDefault(length side.pits - 1, Pit())}`

    //Unset when the player may sow from the pit; otherwise what is wrong with it.
    problemWith()
      ->
        side as Side
        board as Dict of (Pit, Integer)
        pit as Pit
      <- rtn as PitProblem: PitProblem()

      if not pit? or not (side.pits contains pit)
        rtn: PitProblem.NotYourPit
      else if seedsIn(board, pit) == 0
        rtn: PitProblem.EmptyPit

    newBoard()
      <- rtn as Dict of (Pit, Integer): Dict() of (Pit, Integer)
      for player in Player
        side <- sideOf(player)
        rtn += DictEntry(side.store, 0)
        for pit in side.pits
          rtn += DictEntry(pit, SeedsPerPit)

    //Sows the seeds from `start` and returns whose turn is next: the same player when the last
    //seed lands in their own store. The board is updated in place.
    sow()
      ->
        board as Dict of (Pit, Integer)
        player as Player
        start as Pit
      <- rtn as Player: opponentOf(player)

      side <- sideOf(player)
      opponentsStore <- sideOf(rtn).store
      next <- nextPits()

      //A copy, because `-=` changes the Integer it is applied to and the board still refers to it.
      remaining <- Integer(seedsIn(board, start))
      board += DictEntry(start, 0)
      pit <- start
      while remaining > 0
        pit: next.getOrDefault(pit, Pit())
        if pit <> opponentsStore
          board += DictEntry(pit, seedsIn(board, pit) + 1)
          remaining -= 1

      if pit == side.store
        rtn: player
      else if side.pits contains pit and seedsIn(board, pit) == 1
        opposite <- oppositePits().getOrDefault(pit, Pit())
        board += DictEntry(side.store, seedsIn(board, side.store) + seedsIn(board, opposite))
        board += DictEntry(opposite, 0)

    //When either side has no seeds left the game is over, and the seeds still on the other
    //side go to that side's store.
    gameOver()
      -> board as Dict of (Pit, Integer)
      <- rtn as Boolean: false
      for player in Player
        if seedsOnSide(board, sideOf(player)) == 0
          other <- sideOf(opponentOf(player))
          board += DictEntry(other.store, seedsIn(board, other.store) + seedsOnSide(board, other))
          for pit in other.pits
            board += DictEntry(pit, 0)
          rtn: true

    //Unset when the stores hold the same number of seeds.
    winnerOf() as pure
      -> board as Dict of (Pit, Integer)
      <- rtn as Player: Player()
      first <- seedsIn(board, Pit.StoreOne)
      second <- seedsIn(board, Pit.StoreTwo)
      if first > second
        rtn: Player.First
      else if second > first
        rtn: Player.Second

  defines function

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

    cell() as pure
      ->
        board as Dict of (Pit, Integer)
        pit as Pit
      <- rtn as String: String()

      seeds <- $seedsIn(board, pit)
      rtn: seeds.leftPadded(2)

    pitRow()
      ->
        board as Dict of (Pit, Integer)
        side as Side
      <- rtn as String: ""

      for pit in side.pits
        rtn += `${Divider} ${pit}:${cell(board, pit)} `
      rtn += Divider

    //A border with one segment per pit.
    edgeOf()
      -> side as Side
      <- rtn as String: ""
      for pit in side.pits
        rtn += PitEdge
      rtn += Corner

    //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 players see or type. The board is not here: the Console draws it
  //from the game's pits, and only its labels are words.
  defines text for "en"

    Say

      introduction()
        "Mancala, by Al Sweigart al@inventwithpython.com"

      player()
        -> number as String
        `Player ${number}`

      store()
        -> number as String
        `Store ${number}:`

      askMove()
        ->
          number as String
          letters as String
        `Player ${number}, choose move: ${letters} (or QUIT)`

      quit()
        "QUIT"

      notYourPit()
        "Please pick a letter on your side of the board."

      emptyPit()
        "Please pick a non-empty pit."

      won()
        -> number as String
        `Player ${number} has won!`

      tie()
        "There is a tie!"

      goodbye()
        "Thanks for playing!"

  defines program

    //Mancala (Big Book project 43). The two-player seed-sowing game: sow the seeds of a pit round
    //the board, and the most seeds in your store wins. The game decides; the Console asks and
    //tells, in the players' words.
    Mancala()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))

      console.showIntroduction()
      board <- newBoard()
      player <- Player.First
      running <- true
      while running and not console.ended()
        console.showBoard(board)
        if move <- console.nextMove(sideOf(player), board)
          player: sow(board, player, move)
          if gameOver(board)
            console.showBoard(board)
            if winner <- winnerOf(board)
              console.showWinner(winner)
            else
              console.showTie()
            running: false
        else
          //The player quit, or the input ended: as in the book, either way the game says goodbye.
          console.showGoodbye()
          running: false

About this port