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 68 — Sliding Tile Puzzle

This is project 68 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 program is random, so the run fixes it by setting BIGBOOK_SEED=1. The player's answers are piped in:

W
QUIT

Run it with:

BIGBOOK_SEED=1 ek9 slidingtilepuzzle.ek9 < input.txt

It prints:

Sliding Tile Puzzle, by Al Sweigart al@inventwithpython.com

Use the WASD keys to move the tiles
back into their original order:
        1 2 3 4
        5 6 7 8
        9 10 11 12
      13 14 15

+----+----+----+----+
|    |    |    |    |
|    | 2  | 3  | 4  |
|    |    |    |    |
+----+----+----+----+
|    |    |    |    |
| 11 | 1  | 5  | 7  |
|    |    |    |    |
+----+----+----+----+
|    |    |    |    |
| 13 | 6  | 15 | 9  |
|    |    |    |    |
+----+----+----+----+
|    |    |    |    |
| 10 | 12 | 14 | 8  |
|    |    |    |    |
+----+----+----+----+
                          (W)
Enter WASD (or QUIT): (A) ( ) ( )

+----+----+----+----+
|    |    |    |    |
| 11 | 2  | 3  | 4  |
|    |    |    |    |
+----+----+----+----+
|    |    |    |    |
|    | 1  | 5  | 7  |
|    |    |    |    |
+----+----+----+----+
|    |    |    |    |
| 13 | 6  | 15 | 9  |
|    |    |    |    |
+----+----+----+----+
|    |    |    |    |
| 10 | 12 | 14 | 8  |
|    |    |    |    |
+----+----+----+----+
                          (W)
Enter WASD (or QUIT): (A) (S) ( )

The program

#!ek9
defines module bigbook.slidingtile

  defines constant

    //The board is Size squares a side, numbered 0 to Last in each direction.
    Size <- 4
    Last <- 3
    Scramble <- 200

    //The board's own marks, data like the tiles: the corners, rules and edges of the grid, the
    //inside of a square, the blank square, and how wide a tile's number is drawn.
    Corner <- "+"
    CellRule <- "----"
    Edge <- "|"
    CellSpace <- "    "
    Blank <- "  "
    LabelWidth <- 2
    //A key the prompt does not offer, because that slide is not possible.
    NoKey <- " "

  defines type

    //A numbered tile is 1 to 15 and nothing else. The blank square is an UNSET Tile, so
    //there is no "  " sentinel in the board: the blank text appears only when it is drawn.
    Tile as Integer constrain as
      >= 1 and <= 15

    //The four slides, named for the way a TILE moves into the blank (W, A, S, D). An
    //enumeration iterates in declaration order, so possibleSlides lists them in the book's order.
    Slide
      Up
      Left
      Down
      Right

    Dict of (Coord, Tile)
    DictEntry of (Coord, Tile)
    List of Slide

  defines record

    //A board position used as a Dict key, compared by value (the shape of lib/Coord.ek9).
    Coord
      x as Integer: Integer()
      y as Integer: Integer()

      default Coord() as pure

      //Integer(px) snapshots the argument: a key holding the caller's object would change
      //when the caller's variable changed, and the Dict would no longer find it.
      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 GAME's state: the tiles, and where the blank square is.
    Board
      tiles as Dict of (Coord, Tile): Dict() of (Coord, Tile)
      blank as Coord: Coord(Last, Last)

      //A new board is the solved puzzle.
      Board()
        for y in 0 ... Last
          for x in 0 ... Last
            at <- Coord(x, y)
            tiles += DictEntry(at, homeTile(at))

      //The tile at a position; unset for the blank square.
      tileAt() as pure
        -> at as Coord
        <- rtn as Tile: tiles.getOrDefault(at, Tile())

      //A slide is possible when there is a tile on the far side of the blank; an off-board
      //source is simply not in the Dict, so there is no edge arithmetic to get wrong.
      canSlide() as pure
        -> slide as Slide
        <- rtn as Boolean: tiles contains sourceOf(blank, slide)

      possibleSlides()
        <- rtn as List of Slide: List() of Slide
        for slide in Slide
          if canSlide(slide)
            rtn += slide

      //The caller checks canSlide first, as the book's makeMove assumes a valid move.
      slideTile()
        -> direction as Slide
        source <- sourceOf(blank, direction)
        tiles += DictEntry(blank, tiles.getOrDefault(source, Tile()))
        tiles += DictEntry(source, Tile())
        blank: source

      isSolved()
        <- rtn as Boolean: blank == Coord(Last, Last)
        for y in 0 ... Last
          for x in 0 ... Last
            at <- Coord(x, y)
            if at <> blank and tiles.getOrDefault(at, Tile()) <> homeTile(at)
              rtn: false

      default operator ?

    //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 tiles a row at a time, with the
      //grid sized to Size.
      showBoard()
        -> board as Board
        border <- gridLine(Corner, CellRule)
        spacer <- gridLine(Edge, CellSpace)
        stdout.println("")
        stdout.println(border)
        for y in 0 ... Last
          stdout.println(spacer)
          line <- Edge
          for x in 0 ... Last
            line += ` ${tileText(board.tileAt(Coord(x, y)))} ${Edge}`
          stdout.println(line)
          stdout.println(spacer)
          stdout.println(border)

      //Asks until the player picks a possible slide. QUIT, or the end of the input, gives back
      //an UNSET Slide, which the caller reads as "stop playing".
      nextSlide()
        -> board as Board
        <- rtn as Slide: Slide()

        quitting <- false
        while not rtn? and not quitting and not input.ended()
          showPrompt(board)
          if line <- input.next()
            response <- line.upperCase().trim()
            if response == say.quit()
              quitting: true
            else if chosen <- slideOf(response) then board.canSlide(chosen)
              rtn: chosen

      showWon()
        stdout.println(say.won())

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

      //The prompt's words, then a key for each slide the board allows, laid out as the keys sit
      //on a keyboard: the Up key above the Down key, wherever the words end.
      private showPrompt()
        -> board as Board
        label <- say.askSlide()
        left <- keyBox(board, Slide.Left)
        labelWidth <- length label
        leftWidth <- length left
        upIndent <- String("").rightPadded(labelWidth + leftWidth + 2, ' ')
        stdout.println(`${upIndent}${keyBox(board, Slide.Up)}`)
        stdout.println(`${label} ${left} ${keyBox(board, Slide.Down)} ${keyBox(board, Slide.Right)}`)

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

  defines function

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

    //Where the tile that slides in a direction comes from: the neighbour of the blank on
    //the opposite side. It may be off the board, and then the board simply has no tile there.
    sourceOf() as pure
      ->
        blank as Coord
        slide as Slide
      <- rtn as Coord: Coord()

      switch slide
        case Slide.Up
          rtn: Coord(blank.x, blank.y + 1)
        case Slide.Left
          rtn: Coord(blank.x + 1, blank.y)
        case Slide.Down
          rtn: Coord(blank.x, blank.y - 1)
        case Slide.Right
          rtn: Coord(blank.x - 1, blank.y)
        default
          rtn: Coord()

    //The key for each slide: the game's own data, as the player types it.
    keyOf() as pure
      -> slide as Slide
      <- rtn as String: String()

      switch slide
        case Slide.Up
          rtn: "W"
        case Slide.Left
          rtn: "A"
        case Slide.Down
          rtn: "S"
        case Slide.Right
          rtn: "D"
        default
          rtn: String()

    //Anything that is not one of the four keys parses to an UNSET Slide.
    slideOf() as pure
      -> key as String
      <- rtn as Slide: (key == "W" <- Slide.Up
        : key == "A" <- Slide.Left
        : key == "S" <- Slide.Down
        : key == "D" <- Slide.Right
        : Slide())

    //The tile that belongs at a position when the puzzle is solved: 1 to 15 reading across,
    //and no tile (the blank) in the last square.
    homeTile() as pure
      -> at as Coord
      <- rtn as Tile: Tile()

      number <- (at.y * Size) + at.x + 1
      if at.x <> Last or at.y <> Last
        rtn: Tile(number)

    //The book's getNewPuzzle: make random possible slides from the solved state, so the
    //scrambled puzzle is always solvable.
    scrambled()
      -> rng as Random
      <- rtn as Board: Board()
      for move in 1 ... Scramble
        slides <- rtn.possibleSlides()
        rtn.slideTile(slides.getOrDefault(rng.next(0, length slides - 1), Slide()))

  defines function

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

    //A line across the grid: the mark between the squares, and what fills each square. The
    //line starts as a COPY of the mark: '+=' changes the String it is applied to, and that must
    //not be the caller's mark.
    gridLine()
      ->
        mark as String
        fill as String
      <- rtn as String: String(mark)

      for x in 0 ... Last
        rtn += `${fill}${mark}`

    //Two characters wide, as the book's labels are, so every column of the grid lines up.
    tileText() as pure
      -> tile as Tile
      <- rtn as String: Blank

      if tile?
        number <- $tile
        rtn: number.rightPadded(LabelWidth)

    //A slide's key in brackets, or an empty pair when the board does not allow that slide.
    keyBox() as pure
      ->
        board as Board
        slide as Slide
      <- rtn as String: String()

      key <- board.canSlide(slide) <- keyOf(slide) : NoKey
      rtn: `(${key})`

    //THE source of randomness, copied unchanged from lib/RandomSource.ek9: SystemRandom(), or
    //SeededRandom(seed) when BIGBOOK_SEED holds a number (run-and-diff.sh sets it from seed.txt).
    randomSource()
      <- rtn as Random: SeededRandom(Integer(EnvVars().get("BIGBOOK_SEED"))) ?: SystemRandom()

    //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 the prompt's keys are not here:
  //the Console draws them from the game's tiles and slides. The solved order in the
  //introduction is the book's fixed picture, so it is part of the words.
  defines text for "en"

    Say

      introduction()
        `Sliding Tile Puzzle, by Al Sweigart al@inventwithpython.com

Use the WASD keys to move the tiles
back into their original order:
        1 2 3 4
        5 6 7 8
        9 10 11 12
      13 14 15`

      askSlide()
        "Enter WASD (or QUIT):"

      quit()
        "QUIT"

      won()
        "You won!"

  defines program

    //Sliding Tile Puzzle (Big Book project 68). The classic 4x4 15-puzzle: slide the tiles back
    //into order. The game decides; the Console asks and tells, in the player's words. The golden
    //replays a seeded game (seed.txt).
    SlidingTile()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      console.showIntroduction()
      board <- scrambled(rng)
      running <- true
      while running and not console.ended()
        console.showBoard(board)
        if board.isSolved()
          console.showWon()
          running: false
        else if slide <- console.nextSlide(board)
          board.slideTile(slide)
        else
          running: false

About this port