Tic-tac-toe in Clojurescript
Tic-tac-toe is a great game to implement when learning a language. The rules are very simple, and it allows you to play with a couple of different data structures. In fact, the official React tutorial is exactly this.
I’ve implement Tic-tac-toe using reagent in clojurescript. The main code file is only 62 lines, with some styling in CSS.
Game state
The game state can be represented in a very simple atom. There are essentially only 3 things you need to know about the state at any time:
- Where are the pieces on the board?
- Whose turn is it?
- Has somebody won?
All of these can be put into a simple map:
(defonce app-state
(reagent/atom {:board [[0 0 0][0 0 0][0 0 0]]
:player false
:victory false}))
The game board is a simple 3x3 array. Each cell is represented by a number 0–2 (0 is empty, 1 is “X”, 2 is “O”). As there are only 2 players, using a simple boolean to determine whose turn it is makes sense. Victory is also just a boolean.
Rendering the board
Rendering the board is a simple set of nested loops. I’ll start from the outside and work inwards.
(defn page [ratom]
[:div {:class "board"}
(render-board (:board @ratom))
(if (victory-checker)
(str "Winner: " (player-piece (token (not (:player @app-state)))))
(str "Player: " (player-piece (token (:player @app-state)))))])
Here we define a new div
of class board
. The contents of the div are the output of render-board
with the current board state passed into it, as well as the player or winner message, depending on whether victory conditions have been met.
(defn render-board [board]
(map-indexed
(fn [i row]
[:div {:class "row"
:key (str "row" i)}
(render-row i row)])
board))
The render-board
function should output 3 ‘row’ divs. These divs are populated by the render-row
function. I’ve used map-indexed
in this case because for implementing the game logic layer it’s important that we can pass in the x and y co-ordinates of each cell to the click event handler. The index on this loop gives the y
position.
(defn render-row [y row]
(map-indexed
(fn [x piece]
[:div {:class "piece"
:on-click #(move x y)
:key (str "piece" y x)}
(player-piece piece)])
row))
The render-row
function is similar to the render-board
function. The elemsents of each row are looped over using map-indexed
again, this time to generate the x
co-ordinate. This, combined with the y
from the outer-loop, is passed into the click handler function `move.
The choice of piece to show is done by a simple lookup of the piece value in the map player-piece
:
(def player-piece {0 "-" 1 "X" 2 "O"})
For showing the next player piece we do a simple boolean lookup in a nother map token
and look up the output in player-piece
to turn true into X
and false into O
.
(def token {true 1 false 2})
Game logic
Tic-tac-toe game logic can be really simply defined as:
- Players take it in turn to place a piece.
- If a player places 3 of their pieces in a row, horizontally, vertically or diagonally, they win.
First let’s implement taking it in turns to play:
(defn move [x y]
(let [current (get-in @app-state [:board y x])]
(when
(and
(not (victory-checker (:board @app-state)))
(zero? current))
(swap! app-state assoc-in [:board y x] (token (:player @app-state)))
(swap! app-state assoc :player (not (:player @app-state))))))
The move function takes 2 arguments, x and y. These are the co-ordinates that the clicked on piece can be found at. The get-in
function allows us to use these co-ordinates to directly reference the piece position in the board state. From that point in the logic is simple—if it isn’t victory and as long as the position hasn’t been played before, then set the position to the current player’s token. Then swap the player by calling not
on the :player
key.
Victory checking can be pretty straightforward. In the original react tutorial they do this by defining all the possible combinations of indexes that you need to look at to check there are 3 in a row. This produces an 8x3 array, that just looks ugly. My approach is slightly more visually pleasing.
(defn three-in-a-row? [rows]
(some true?
(map #(apply = %)
(filter #(not-any? zero? %) rows))))
This function takes arbitrary 2d arrays. It filters out any rows that still have unplayed spaces. Any remaining rows are then checked to see if all elements are equal. If any are then the function returns true
. Even though the function if called three-in-a-row?
this is a bad name as it could be used on an arbitrarily sized game board.
We can pass the board state straight into that function and it will check for horizontal victories, but what about vertical and diagonals?
(defn victory-checker [b]
(let [tl [[(get-in b [0 0]) (get-in b [1 1]) (get-in b [2 2])]
[(get-in b [0 2]) (get-in b [1 1]) (get-in b [2 0])]]]
(some true? (map three-in-a-row? [b (apply map list b) tl]))))
One of clojure’s key strong points is data manipulation and transformation. The function (apply map list b)
takes the board and spins it 90 degrees. That means when we pass the output of that into the previous function we’re now checking verticals. Unfortunately, I didn’t come up with anything fancy for diaganols, but at least it’s only 2 referenced by index as opposed to 8 from the original react tutorial.
Conclusion
I’ve left out a tiny bit of scaffold code. The full code is available on my github. It’s taken a lot longer to write the blog post than it did to get the game to this state, thanks to the combination of figwheel and reagent. This is definitely a lot more straightforward and clearer than following the standard JavaScript React tutorial.