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 30 — Four in a Row

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

1
2
1
2
1
2
1

Run it with:

ek9 four-in-a-row.ek9 < input.txt

It prints:

Four in a Row, by Al Sweigart al@inventwithpython.com

Two players take turns dropping tiles into one of seven columns, trying
to make four in a row horizontally, vertically, or diagonally.

 1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|.......|
|.......|
+-------+
Player X, enter a column or QUIT:

 1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|.......|
|X......|
+-------+
Player O, enter a column or QUIT:

 1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|.......|
|XO.....|
+-------+
Player X, enter a column or QUIT:

 1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|X......|
|XO.....|
+-------+
Player O, enter a column or QUIT:

 1234567
+-------+
|.......|
|.......|
|.......|
|.......|
|XO.....|
|XO.....|
+-------+
Player X, enter a column or QUIT:

 1234567
+-------+
|.......|
|.......|
|.......|
|X......|
|XO.....|
|XO.....|
+-------+
Player O, enter a column or QUIT:

 1234567
+-------+
|.......|
|.......|
|.......|
|XO.....|
|XO.....|
|XO.....|
+-------+
Player X, enter a column or QUIT:

 1234567
+-------+
|.......|
|.......|
|X......|
|XO.....|
|XO.....|
|XO.....|
+-------+
Player X has won!

The program

#!ek9
defines module bigbook.fourinarow

  defines constant

    Width <- 7
    Height <- 6
    Connect <- 4
    //The board's own marks, data like the tiles: an empty cell, and the edges of the drawing.
    Empty <- "."
    Edge <- '|'
    Corner <- "+"
    Rule <- '-'

  defines type

    //The two players are a closed set, and '$' gives the mark their tiles show. The marks are
    //the game's data, not words: they are drawn on the board and do not translate.
    Player
      X
      O

    List of Coord
    Dict of (Coord, Player)
    DictEntry of (Coord, Player)

  defines record

    //A board position, compared by value, so it can be a Dict key. Copied from
    //lib/Coord.ek9, which explains its two details: the fields default UNSET, and the
    //constructor SNAPSHOTS its arguments.
    Coord
      x as Integer: Integer()
      y as Integer: Integer()

      default Coord() as pure

      Coord() as pure
        ->
          px as Integer
          py as Integer

        x :=? Integer(px)
        y :=? Integer(py)

      operator $ as pure
        <- rtn as String: `(${x},${y})`

      default operator ?
      default operator <=>
      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 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 board is drawn, not worded: computed from the game's grid, a row at a time, with the
      //column numbers above it and the borders sized to Width.
      showBoard()
        -> board as Dict of (Coord, Player)
        columnNumbers <- for column in 1 ... Width | map with asText | collect as String
        border <- `${Corner}${String("").rightPadded(Width, Rule)}${Corner}`
        stdout.println("")
        stdout.println(` ${columnNumbers}`)
        stdout.println(border)
        for row in 0 ... Height - 1
          line <- `${Edge}`
          for column in 0 ... Width - 1
            line += cellText(board.getOrDefault(Coord(column, row), Player()))
          stdout.println(line + `${Edge}`)
        stdout.println(border)

      //The cell the player's tile lands in; unset when they QUIT or the input ends. The guard
      //on the reply is what stops the loop asking again once the input has ended.
      nextMove()
        ->
          player as Player
          board as Dict of (Coord, Player)
        <- rtn as Coord: Coord()

        quitting <- false
        while not rtn? and not quitting and not input.ended()
          stdout.println(say.askColumn(player))
          if response <- input.next().upperCase().trim()
            column <- columnOf(response)
            if response == say.quit()
              quitting: true
            else if not column?
              stdout.println(say.notAColumn())
            else if row <- landingRow(board, column)
              rtn: Coord(column, row)
            else
              stdout.println(say.columnFull())

      showWinner()
        -> player as Player
        stdout.println(say.won(player))

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

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

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

      //The board holds only the tiles played, so an empty cell - or one off the board - reads
      //as an unset Player. The '.' exists only here, at the display edge.
      private cellText()
        -> occupant as Player
        <- rtn as String: occupant? <- $occupant : Empty

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

  defines function

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

    opponentOf() as pure
      -> player as Player
      <- rtn as Player: player == Player.X <- Player.O : Player.X

    isFull() as pure
      -> board as Dict of (Coord, Player)
      <- rtn as Boolean: length board == Width * Height

    //The column a reply names, counting from 0; unset unless the reply is 1 to Width.
    columnOf() as pure
      -> reply as String
      <- rtn as Integer: Integer()

      number <- Integer(reply)
      if number? and number >= 1 and number <= Width
        rtn: number - 1

    //Where a tile dropped into 'column' lands: the lowest empty row, or unset when the
    //column is full.
    landingRow() as pure
      ->
        board as Dict of (Coord, Player)
        column as Integer
      <- rtn as Integer: Integer()

      for row in Height - 1 ... 0
        if not rtn? and not (board contains Coord(column, row))
          rtn: row

    //The steps from one cell of a line to the next: right, down, and the two diagonals
    //going down. Every line on the board runs one of these four ways from its first cell.
    lineSteps() as pure
      <- rtn as List of Coord: [Coord(1, 0), Coord(0, 1), Coord(1, 1), Coord(-1, 1)]

    //Whether 'player' holds Connect cells in a row from 'start'. A cell that is empty or off
    //the board compares as unset, which the 'if' does not take - so no bounds check is needed.
    lineFrom()
      ->
        board as Dict of (Coord, Player)
        start as Coord
        step as Coord
        player as Player
      <- rtn as Boolean: false

      held <- 0
      for distance in 0 ... Connect - 1
        cell <- Coord(start.x + (step.x * distance), start.y + (step.y * distance))
        if board.getOrDefault(cell, Player()) == player
          held += 1
      rtn: held == Connect

    isWinner()
      ->
        board as Dict of (Coord, Player)
        player as Player
      <- rtn as Boolean: false

      for column in 0 ... Width - 1
        for row in 0 ... Height - 1
          for step in lineSteps()
            rtn: rtn or lineFrom(board: board, start: Coord(column, row), step: step, player: player)

  defines function

    asText() as pure
      -> number as Integer
      <- rtn as String: $number

    //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 board and its marks are not here: the
  //Console draws them from the game's grid.
  defines text for "en"

    Say

      introduction()
        `Four in a Row, by Al Sweigart al@inventwithpython.com

Two players take turns dropping tiles into one of seven columns, trying
to make four in a row horizontally, vertically, or diagonally.`

      askColumn()
        -> player as Player
        `Player ${player}, enter a column or QUIT:`

      quit()
        "QUIT"

      goodbye()
        "Thanks for playing!"

      notAColumn()
        `Enter a number from 1 to ${Width}.`

      columnFull()
        "That column is full, select another one."

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

      tie()
        "There is a tie!"

  defines program

    //Four in a Row (Big Book project 30). Two players drop X/O tiles into 7 columns; first to
    //four in a row (horizontal, vertical, or diagonal) wins. The game decides; the Console asks
    //and tells, in the player's words.
    FourInARow()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))

      console.showIntroduction()
      board <- Dict() of (Coord, Player)
      player <- Player.X
      running <- true
      while running and not console.ended()
        console.showBoard(board)
        if move <- console.nextMove(player, board)
          board += DictEntry(move, player)
          if isWinner(board, player)
            console.showBoard(board)
            console.showWinner(player)
            running: false
          else if isFull(board)
            console.showBoard(board)
            console.showTie()
            running: false
          else
            player: opponentOf(player)
        else
          //The player quit: the game says goodbye. At the end of the input it does not.
          console.showGoodbye()
          running: false

About this port