meain/blog

Sep 28, 2017 . 6 min

Creating a tic-tac-toe game in Clojurescript using Reagent

Whoosh, I am tired of JS all day long, let me try out something else. And yeah, I ended up here. Actually I was kinda playing around with Haskell before I reached here. It feels pretty good to have a change from the usual stuff. To be frank I think everyone should try out functional programming at some point of time. Well, enough bullshit let us get to building it.

screenshot

Fully working code: tictactoe-clojurescript-reagent

To start with Clojrescript is a functional programming language. It derives from Clojure.

Random fact: Clojure runs on top of Java.

Installation and setup #

I want to add the setup instructions, but it is different for different systems and I am really lazy right now so go check out the webpage.

Just some pointers, you will need Java first. Then install Clojurescript and then leiningen.

Leiningen is kinda like gulp.

Btw in mac it is:

brew install clojurescript
brew install leiningen

Getting started #

Setting up template #

First you blah blah....

Run this command:

lein new figwheel ttt -- --reagent

It is kinda like npm init command but with livereload and react installed.

OK, what that does is create a new project with figwheel and using reagent

Figwheel is like gulp-livereload plugin. Reagent is the Clojurescript wrapper arround Facebook's React JS framework.

Now you start the lien server ( which has hot reload ) by using the command:

lein figwheel

You will get a file structure like this

ttt
├── README.md
├── dev
│   └── user.clj
├── project.clj
├── resources
│   └── public
│       ├── css
│       │   └── style.css
│       └── index.html
└── src
    └── ttt
        └── core.cljs

6 directories, 6 files

The main file you will have to work on here is src/ttt/core.cljs. It is the file that we will add all the core logic into.

project.clj is a configuration file, kinda like package.json.

Well, let us see some basics of Clojrescript #

In the core.cljs file you will have:

(ns ttt.core
(:require [reagent.core :as reagent :refer [atom]]))

(enable-console-print!)

(println "This text is printed from src/ttt/core.cljs. Go ahead and edit it and see reloading in action.")

;; define your app data so that it doesn't get over-written on reload

(defonce app-state (atom {:text "Hello world!"}))


(defn hello-world []
[:div
[:h1 (:text @app-state)]
[:h3 "Edit this and watch it change!"]])

(reagent/render-component [hello-world]
(. js/document (getElementById "app")))

(defn on-js-reload []
;; optionally touch your app-state to force rerendering depending on
;; your application
;; (swap! app-state update-in [:__figwheel_counter] inc)
)

Let us see what all of these lines do.

(ns ....) #

The top two lines is kinda your import statements. Mostly importing Reagent here.

console.log() #

Well we have println instead of console.log. But since this is a functional language we use it like:

(println "Stuff you wanna print")

This will get printed in the JS console in your browser.

Commenting #

You can use ; to start a comment. Anything after this, used on start of a line or anywhere in the line is considered as a comment.

Example:

; I heard you like comments

app-state #

In line 10, what you see is a variable ( atom ) declaration. The difference between this variable and others is that it is an immutable variable which means you cannot modify it in place.

(reagent/render-component) #

Renders component, duh. It renders the hello-world component on to the div with id app.

(defn hello-world [] ) #

Well hello-world is a function that is return a html string kinda thing or more like return a JSX object ( if that is a thing ). In Clojurescript world it is called hiccup like syntax. It has pretty much redid the html in a Clojure ish syntax.

Actually from Reagent.

The defenition here:

[:div
[:h1 (:text @app-state)]
[:h3 "Edit this and watch it change!"]]

gives you something like:

<div>
<h1>${app-state.text}</h1>
<h3>Edit this and watch it change!</h3>
</div>

(:text @app-state) #

This is how you get a value from the immutable variable.

You use @ when using an atom

Now if you have some value multiple levels deep you can use get-in

For example:

user=> (def m {:username "sally"
:profile {:name "Sally Clojurian"
:address {:city "Austin" :state "TX"}}})
#'user/m

user=> (get-in m [:profile :name])
"Sally Clojurian"
user=> (get-in m [:profile :address :city])
"Austin"
user=> (get-in m [:profile :address :zip-code])
nil
user=> (get-in m [:profile :address :zip-code] "no zip code!")
"no zip code!"

Check out ClojureScript Cheatsheet in case you get stuck.

Building the game #

Cool, now with all that basics out of the way let us get to building the actual game.

I will be using the code from here (tictactoe-clojurescript-reagent)

So as I said the main file you will be checking out will be src/ttt/core.cljs. We will go line by line ( mostly ) from the above cited project's core.cljs file.

Lines 9 - 10 #

(defn make-board "Creates a new board. n denotes the size." [n]
(vec (repeat n (vec (repeat n 0)))))

In here we define a function which will, on call return a [n x n] matrix. (repeat n 0) creates a list of n 0's. We turn that into a vector. Now we get one n dimensional array. We create multiple copies of this to create an [n x n] matrix.

Lines 12 - 17 #

(def board-size 3)
(defonce app-state
(atom {:text ":game"
:board (make-board board-size)
;; none win lose draw
:win "none"}))

In here we are mainly defining variables. In line 12 we define the variable board-size. In lines 13 to 17 we are creating an atom ( an immutable variable ) which contains a text, the current board and win state.

Lines 19 - 30 #

(defn check-win "Check for win and lose conditions" [user computer]
(if (or (some #(= board-size %) (for [freq (frequencies (for [el user] (first el)))] (second freq)))
(some #(= board-size %) (for [freq (frequencies (for [el user] (second el)))] (second freq)))
(= board-size (get-in (frequencies (for [el user] (= (first el) (second el)))) [true]))
(= board-size (get-in (frequencies (for [el user] (= (first el) (- (- board-size (second el)) 1)))) [true])))
(swap! app-state assoc :win "win"))
(if (or (some #(= board-size %) (for [freq (frequencies (for [el computer] (first el)))] (second freq)))
(some #(= board-size %) (for [freq (frequencies (for [el computer] (second el)))] (second freq)))
(= board-size (get-in (frequencies (for [el computer] (= (first el) (second el)))) [true]))
(= board-size (get-in (frequencies (for [el computer] (= (first el) (- (- board-size (second el)) 1)))) [true])))
(swap! app-state assoc :win "lose"))
)

Here we define the checks to see if the player or bot has won. Draw conditions are checked in another function. The variables user and computer are values that are passed from the function check-state defined at line 32. The contents of these variables are list of [i j] values for which the respective players have marked.

Once we have found out if there is a win or lose condition we use swap! and assoc to change the value of win inside of app-state.

Lines 32 - 49 #

(defn check-state []
(let [board (:board @app-state)
remaining (for [i (range board-size)
j (range board-size)
:when (= (get-in board [i j]) 0)]
[i j])
user (for [i (range board-size)
j (range board-size)
:when (= (get-in board [i j]) 1)]
[i j])
computer (for [i (range board-size)
j (range board-size)
:when (= (get-in board [i j]) 2)]
[i j])]
(if (= (count remaining) 0)
(swap! app-state assoc :win "draw"))
(check-win user computer)
))

This function check-state along with the above one is used to determine the win, lose, draw or none setting of the :win setting. This variable :win is what is then used in order to set the message on the screen.

What we use in our matrix to denote played position is 1 and 2 for user and computer respectively. In this function we get the positions which are remaining ie not marked, positions played by the user and positions played by the computer into the variables remaining, user and computer.

If no elements are present in the remaining variable we set the condition as draw. Win lose conditions are checked in the check-win function which is called later on.

Lines 51 - 63 #

(defn computer-move []
;; choose a random unplayed block
(let [board (:board @app-state)
remaining (for [i (range board-size)
j (range board-size)
:when (= (get-in board [i j]) 0)]
[i j])
move (rand-nth remaining)
path (into [:board] move)]
(swap! app-state assoc-in path 2)
)
(check-state)
)

This function is used to compute a move for the computer to play. What it does it it picks out the remaining positions in the game-matrix and store it to remaining variable. Then we chose a random value from the remaining variable and make the change to the board.

Lines 65 - 121 #

(defn block [color i j]
[:div {:style {:background-color color
:width "100px"
:height "100px"
:border "5px solid #fff"}
:on-click (fn [e]
(if (and (= 0 (get-in @app-state [:board i j])) (= (:win @app-state) "none"))
((swap! app-state assoc-in [:board i j] 1)
(check-state)
(if (= (:win @app-state) "none")
(computer-move)
))))}])

(defn blank [i j]
(block "#f5f5f5" i j))
(defn cross [i j]
(block "#FF7043" i j))
(defn circle [i j]
(block "#FFEE58" i j))

(defn render-board []
[:div {:style {:display "flex" :flex-wrap "wrap"}}
(doall(for [i (range board-size)
j (range board-size)]
(case (get-in @app-state [:board i j])
0 ^{:key (str i "-" j)} [blank i j]
1 ^{:key (str i "-" j)} [cross i j]
2 ^{:key (str i "-" j)} [circle i j]
)))
])

(defn app []
[:div {:style {:text-align "center"}}
[:div
[:h1 {:style {:display "block" :float "left"}} (:text @app-state)]
[:h1 {:style{:background-color "#f5f5f5"
:display "block"
:float "right"}} "tic-tac-toe"]]
[:div.clearfix {:style {:clear "both"}}]
[:h3 {:style {:width "100%" :text-align "center"}} "Let us play some "
[:code {:style {:font-family "cursive"}} "tic-tac-toe"] " now"]
[:center [:div {:style {:font-size "20px"}} (:win @app-state)]]
[:div.play-area {:style {:width (str (* 110 board-size) "px")
:height (str (* 110 board-size) "px")
:background-color "#ded"
:cursor "pointer"
:display "inline-block"}}
[render-board]
]
[:center
[:button {:on-click (fn [e]
(swap! app-state assoc :board (make-board board-size))
(swap! app-state assoc :win "none")
)
:style {:font-size "30px"
:font-family "monaco, monospace"
:margin-top "20px"}} "New Game"]]])

This is the UI definition of the game. The function block gives a hiccup like object of a single block. The function blank cross and circle are just simple wrappers around block to give different color blocks.

We use colors instead of symbols just because they are easy.

The render-board function is used to render the whole board using the board value in app-state atom. The definition is mostly the hiccup like syntax and I hope that makes sense to you. Towards the end we also add a button to reset the game which has an on-click lister added to it which changes the value of board into a new one and reset the win value to none.

And that basically wraps up the whole code. :)

← Home