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 76 — Tic-Tac-Toe

This is project 76 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
4
2
5
3

Run it with:

ek9 tic-tac-toe.ek9 < input.txt

It prints:

Welcome to Tic-Tac-Toe!

   | |  1 2 3
  -+-+-
   | |  4 5 6
  -+-+-
   | |  7 8 9
What is X's move? (1-9)

  X| |  1 2 3
  -+-+-
   | |  4 5 6
  -+-+-
   | |  7 8 9
What is O's move? (1-9)

  X| |  1 2 3
  -+-+-
  O| |  4 5 6
  -+-+-
   | |  7 8 9
What is X's move? (1-9)

  X|X|  1 2 3
  -+-+-
  O| |  4 5 6
  -+-+-
   | |  7 8 9
What is O's move? (1-9)

  X|X|  1 2 3
  -+-+-
  O|O|  4 5 6
  -+-+-
   | |  7 8 9
What is X's move? (1-9)

  X|X|X 1 2 3
  -+-+-
  O|O|  4 5 6
  -+-+-
   | |  7 8 9
X has won the game!
Thanks for playing!

The program

#!ek9
defines module bigbook.tictactoe

  defines constant

    //The board is Width spaces a side, numbered 1 to Spaces reading across.
    Width <- 3
    Spaces <- 9

    //The board's own marks, data like the X and O tiles: an empty space, the lines between the
    //spaces, and the drawing's indent (this port's own layout).
    Empty <- " "
    Edge <- "|"
    Crossing <- "+"
    Rule <- "-"
    Indent <- "  "

  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

    Dict of (Integer, Player)
    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 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, not worded: computed from the game's spaces a row at a time, with the
      //space numbers beside each row as a guide.
      showBoard()
        -> board as Dict of (Integer, Player)
        rules <- List() of String
        for column in 1 ... Width
          rules += Rule
        stdout.println("")
        for row in 0 ... Width - 1
          if row > 0
            stdout.println(`${Indent}${joined(rules, Crossing)}`)
          marks <- List() of String
          numbers <- List() of String
          for column in 1 ... Width
            space <- (row * Width) + column
            marks += cellText(board.getOrDefault(space, Player()))
            numbers += $space
          stdout.println(`${Indent}${joined(marks, Edge)} ${joined(numbers, " ")}`)

      //The space the player claims; unset only if the input ends first.
      nextMove()
        ->
          player as Player
          board as Dict of (Integer, Player)
        <- rtn as Integer: Integer()

        while not rtn? and not input.ended()
          stdout.println(say.askMove(player))
          if reply <- input.next()
            rtn: spaceFrom(board, reply.trim())

      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 spaces claimed, so an empty space reads as an unset Player. The
      //blank mark 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.

    //The eight lines of three spaces that win: three rows, three columns, two diagonals.
    winningLines() as pure
      <- rtn as List of List of Integer: [
        [1, 2, 3], [4, 5, 6], [7, 8, 9],
        [1, 4, 7], [2, 5, 8], [3, 6, 9],
        [1, 5, 9], [3, 5, 7]
      ]

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

    //The space a reply names, or unset when it is not a number from 1 to 9 or the space is taken.
    spaceFrom() as pure
      ->
        board as Dict of (Integer, Player)
        reply as String
      <- rtn as Integer: Integer()

      space <- Integer(reply)
      if space? and space >= 1 and space <= Spaces and not (board contains space)
        rtn: space

    ownsLine() as pure
      ->
        board as Dict of (Integer, Player)
        line as List of Integer
        player as Player
      <- rtn as Boolean: true

      for space in line
        if not (board contains space) or board.getOrDefault(space, Player()) <> player
          rtn: false

    hasWon() as pure
      ->
        board as Dict of (Integer, Player)
        player as Player
      <- rtn as Boolean: false

      for line in winningLines()
        if ownsLine(board, line, player)
          rtn: true

    isFull() as pure
      -> board as Dict of (Integer, Player)
      <- rtn as Boolean: length board == Spaces

  defines function

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

    //The parts with the separator between each: "X|O| ", "1 2 3". Whether a part is the first
    //is a flag: it says what is meant, where rtn.empty() would ask about the text built so far.
    joined()
      ->
        parts as List of String
        separator as String
      <- rtn as String: ""

      first <- true
      for part in parts
        rtn += first <- part : `${separator}${part}`
        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 players see. The board and its marks are not here: the Console draws
  //them from the game's spaces.
  defines text for "en"

    Say

      introduction()
        "Welcome to Tic-Tac-Toe!"

      askMove()
        -> player as Player
        `What is ${player}'s move? (1-${Spaces})`

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

      tie()
        "The game is a tie!"

      goodbye()
        "Thanks for playing!"

  defines program

    //Tic-Tac-Toe (Big Book project 76). Two players take turns to claim a space; three in a line
    //wins. The game decides; the Console asks and tells, in the players' words.
    TicTacToe()
      //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 (Integer, 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 hasWon(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)
      console.showGoodbye()

About this port