Skip to content

Instantly share code, notes, and snippets.

What would you like to do?

Breaking Down Tic-tac-toe

Last week, @RubenSandwich posted an interactive demo on the mailing list capable of playing and scoring tic-tac-toe matches. He provided some great feedback about the issues he ran into along the way. Now that the language is becoming more stable, our first priority is seeing it used and addressing the problems which surface. To that end, his troubles became our guide to making Eve a little friendlier for writing interactive applications in general and tic-tac-toe in specific.

This analysis (and future breakdowns) will be written inline in Eve to make the discussion flow more naturally. Since our blog is capable of rendering Markdown, we can provide a pleasant reading experience directly from the source code. At the moment, Eve's syntax only lends itself to a subset of Markdown, but we plan to make some small changes in the near future to become fully compatible with GFM.

To begin with, we initialize the board. We freeze an object named @board to hold our global state and create a set of #cells to represent each grid square. These #cells will keep track of the moves players have made. Common connect-N games like tic-tac-toe are scored along 4 axes (horizontal, vertical, and the two diagonals). We group cells together along each axis up front to make scoring easier later. We've also pulled width, height, and n-in-a-row out because, with a few minor changes, our program is generic enough to play any connect-N game. This process is made much cleaner by the addition of new math expressions, including range(from, to), floor(value), and mod(value, by). This is a small part of our effort to expand the standard library based on usage. If you're interested in helping shape this, stop by our RFCs repository or jump right in on our discussion of standard string expressions.

Initialize the board


    // board constants
    width = 3
    height = 3
    n-in-a-row = 3
    starting-player = "x"

    // generate the cells
    i = range[from: 0, to: width * height]
    column = mod[value: i, by: width]
    row = floor[value: i / width]
    diag-left = column - row
    diag-right = (width - column) - row

    board = [@board width height n-in-a-row player: starting-player]
    [#cell board row column diag-left diag-right]

Next, we handle user input. Any time a cell is directly clicked, we:

  1. Ensure the cell hasn't already been played
  2. Ensure the game isn't won yet
  3. Determine whose turn is next (currently by flip-flopping, but you could easily implement a round robin scheme, if you wanted to play a game of N-person tic-tac-toe!)

Then update the cell the reflect its new owner and the board to show the current player.

Click on a cell to make your move

    [#click #direct-target element: [#div cell]]
    board = [@board player: current not(winner)]
    next_player = if current = "x" then "o"
                  else "x"
    board.player := next_player
    cell.player := current

Next, we score the board. When writing Eve, we don't have to worry about triggers and subscriptions to decide when to reevaluate. We simply ask for the data we need and when it changes our block will be reevaluated for us. This is where our extra computation during initialization comes in handy. Since we've already separated our cells into winnable groupings, we only need to check if the number of cells owned by a player in the group is equal to the number required to win (called n-in-a-row). Astute readers will immediately note that while this works for tic-tac-toe, and more generally for connect-N games where width = height = n-in-a-row, this will incorrectly mark a victor on sparse rows when width or height is greater than n-in-a-row. There's a cute change to the grouping logic using convolutions which remedies this problem, but for clarity we've omitted that from the example.

One other gotcha of connect-N games (and particularly tic-tac-toe) is that there won't always be a winner. We catch this case by determining if every cell on the board (of which there are width * height cells) have been filled when no winner has been found. When this happens, we mark the mysterious third player "nobody" as our winner and allow the reset handling logic to do the rest.

Get N in a row to win the game!

    board = [@board n-in-a-row width height not(winner)]
    winner = if cell = [#cell row player]
                n-in-a-row = count[given: cell, per: (row, player)] then player
            else if cell = [#cell column player]
                n-in-a-row = count[given: cell, per: (column, player)] then player
            else if cell = [#cell diag-left player]
                n-in-a-row = count[given: cell, per: (diag-left, player)] then player
            else if cell = [#cell diag-right player]
                n-in-a-row = count[given: cell, per: (diag-right, player)] then player
            else if cell = [#cell player]
                    width * height = count[given: cell] then "nobody"
    board.winner := winner

Since games of tic-tac-toe are often very short and extremely competitive, it's imperative that it be quick and easy to begin a new match. In this case, when the game is over (the board has a winner), a click anywhere will reset the state for another round of play. This means clearing every cell.player and removing the board.winner.

Reset the board state after a win

    [#click #direct-target]
    board = [@board winner]
    cell = [#cell player]
    board.winner -= winner
    cell.player -= player

With the logic in place, we need to actually draw the board so players have something to see and interact with. In the case that a #cell isn't owned yet, we substitute its content with a blank string ”” (the cell won’t render without this). We also add a #status div, which we'll write game state into later. Our cells have the CSS inlined, but you could just as easily link to an external file.

Draw the board

    board = [@board]
    cell = [#cell board row column]
    contents = if cell.player then cell.player
              else ""
    [#div board @container children:
      [#div #status board class: "status"]
      [#div class: "board" children:
        [#div class: "row" sort: row children:
          [#div class: "cell" cell text: contents sort: column style:
            [display: "inline-block" width: "50px" height: "50px" border: "1px solid rgb(47, 47, 49)" color: "black" background: "white" font-size: "2em" line-height: "50px" text-align: "center"]]]]]

Finally, we fill the previously mentioned #status div with our current game state. If no winner has been declared, we remind the competitors of whose turn it is, and once a winner is found we announce her newly-acquired bragging rights.

Draw the current player

    status = [#status board]
    status.text += "{{board.player}}'s turn!"

Draw the winner

    status = [#status board]
    winner = board.winner
    status.text += "{{winner}} wins! Click anywhere to restart!"

Along the way to making this demo, many new standard library expressions were added, the execution strategy for aggregates was overhauled, parser bugs were fixed, and dependency ordering glitches resolved. Even so, the human-compiled version of tic-tac-toe was completed in only about half an hour, and required very few changes to get working once the platform caught up. The latest iteration of Eve is still very much in its infancy, but even now its showing a lot of promise for teasing out simple and general solutions to complicated problems. As one of its creators, I'm obviously a biased party when discussing Eve, so feedback such as @RubenSandwich's is invaluable in helping us make the language more robust. If you find the time to try Eve yourself, please don't hesitate to share your projects experiences with us on the mailing list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment