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 #cell
s to represent each grid square. These #cell
s 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
[#session-connect]
// board constants
width = 3
height = 3
n-in-a-row = 3
starting-player = "x"
// generate the cells
i = range(0, width * height)
column = mod(i, width)
row = floor(i / width)
diag-left = column - row
diag-right = (width - column) - row
freeze
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:
- Ensure the cell hasn't already been played
- Ensure the game isn't won yet
- 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]]
not(cell.player)
board = [@board player: current not(winner)]
next_player = if current = "x" then "o"
else "x"
freeze
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"
freeze
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]
freeze
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 ""
maintain
[#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]
not(board.winner)
maintain
status.text += "{{board.player}}'s turn!"
Draw the winner
status = [#status board]
winner = board.winner
maintain
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.