Practical CoffeeScript: Making a Tic-Tac-Toe Game
CoffeeScript is a tiny little language that compiles to JavaScript. There is no interpretation at runtime since you write CoffeeScript, compile it to JavaScript and use the resulting JavaScript files for your app. You can use any JavaScript library (e.g. jQuery) from within CoffeeScript, just by using its features with the appropriate CoffeeScript syntax. CoffeeScript can be used both for writing JavaScript on the front-end and JavaScript on the back-end.
So Why CoffeeScript?
Less Code
According to the Little Book on CoffeeScript, CoffeeScript’s syntax reduces the amount of characters you need to type to get your JS working by around 33% to 50%. I will be presenting a simple Tic-Tac-Toe game created using CoffeeScript (you probably already guessed this from the title) which in its raw CoffeeScript format contains 4963 characters, whereas the compiled JavaScript code contains 7669 characters. That is a difference of 2706 characters or 36%!
Faster Development Time
Because you write shorter, less error-prone (e.g. variables are auto-scoped, meaning you can’t accidentally overwrite globals by omitting var
) you can finish your projects quicker. CoffeeScript’s terse syntax also makes for more readable code, and ultimately code which is easier to maintain.
Getting Started
In this article, we will be building a simple Tic-tac-toe game with CoffeeScript and jQuery. If you want to read up on the syntax before examining a practical case, I suggest my Accelerate Your JavaScript Development with CoffeeScript article here at SitePoint. This also details how to install CoffeeScript via npm (the Node Package manager).
As ever, all of the code from this tutorial is available on GitHub and a demo is available on CodePen or at the end of the tutorial.
The most common CoffeeScript commands you will be using are:
coffee -c fileName
will compile the CoffeeScript file to a file with the same name but with a .js
extension (CoffeeScript files typically have .coffee
extension).
coffee -cw fileName
will watch for changes in a file (whenever you save the file) and compile it.
coffee -cw folderName/
will watch for changes to all .coffee
files in the folder and compile them in the same directory when there are any changes.
Finally, it is handy to compile CoffeeScript from a folder with .coffee
files to a folder containing only .js
files.
coffee -o js/ -cw /coffee
will watch for changes in all .coffee
files located in the coffee
folder and place the output (JavaScript) in the js
folder.
If you are not into terminals, you can use a tool with a GUI to handle your CoffeeScript files. For instance, you can try Prepros on a free unlimited trial (although you have to buy it if you like it). The image below shows some of the options it provides:
You can see that Prepros does all the work for you—it sets up watchers so your .coffee
files will be compiled to JS, it allows you to use Uglify JS which will minify/compress your code, it can automatically mangle variables and it supports Iced CoffeeScript. Prepros can also be used for CSS preprocessors such as Less and Sass and template engines like Jade.
The Game
Let’s start with the markup:
<div class="wrapper">
<header class="text-center">
<h1>Tic Tac Toe</h1>
</header>
<div id="board"></div>
<div class="alerts welcome"></div>
<div class="notifications"></div>
<form action="" method="POST">
...
</form>
</div>
<script src="jquery.min.js"></script>
<script src="logic/app.js"></script>
The game’s interface consists of the following:
- A header which briefly describes the game
- A div element with the id of
board
which is where the 3×3 squares will be located - A div element with a class of
alerts
which is where the game status will be shown - A div element with a class of
notifications
which will show who is playing X and O, along with the general player statistics. - A form which will be displayed only when the game loads and will prompt the players to enter their names.
In accordance with best practice, both jQuery and the script that makes our app tick are loaded before the closing body tag.
The Styling
Using CSS, we can make the nine squares involved appear in a 3×3 grid by floating each square and clearing every 4th one.
.square:nth-of-type(3n + 1) {
clear: both;
}
We can also add a different color to the squares depending on whether they have the class x
or o
(which is added using JavaScript).
.square.x {
color: crimson;
}
.square.o {
color: #3997ff;
}
CoffeeScript in Action
For reference, you can find the main CoffeeScript file here.
You can see our Tic-Tac-Toe app starts with $ ->
, this is equivalent to the shorthand for jQuery’s function that executes code when the DOM is ready: $(function() { ... });
.
CoffeeScript does not rely on semicolons and braces but on indentation. ->
tells CoffeeScript that you are defining a function so you can start the body of the function on the next line and indent the body with two spaces.
Next, we create an object called Tic
which itself contains an object called data
. You can see that braces or commas are not obligatory when creating objects, as long as you indent the properties correctly.
$ ->
Tic =
data:
turns: 0
x: {}
o: {}
gameOver: false
The turns
property will hold the total number of turns taken in the game. We can check whether it holds an even or uneven number and in that way determine whether it is the turn of X or O.
The x
and o
properties are objects and will contain data relating to the number of X’s or O’s on the three axes that are important for the game: horizontal, vertical and diagonal. They will be updated on every move through the checkEnd
method to represent the distribution of X and O on the board. The checkEnd
method will then call checkWin
to determine if there is a winner.
After that we have a method inside the Tic
object that will get everything up and running:
initialize: ->
@data.gameOver = false
@.setPlayerNames()
@.retrieveStats()
@.assignRoles()
@.prepareBoard()
@.updateNotifications()
@.addListeners()
Notice the use of @
which compiles to the JavaScript keyword this
. As illustrated in the first property of initialize
, you can skip the dot after the @
keyword when setting or calling a property or method.
By giving the methods sensible names, we have a fair idea of what they are doing:
setPlayerNames
stores the values entered by users in the inputs into thedata
object.retrieveStats
retrieves the player’s statistics from localStorage and sets them up in thedata
object.assignRoles
determines who is playing X and who is playing O.prepareBoard
hides the form, removes any notifications, empties the board and fills it with nine empty squares.updateNotifications
updates the UI with information about who is playing X and who is playing O, as well as the player’s statistics.addListeners
attaches the event listeners, so that we can respond to players making a move.
Diving Deeper
Let’s look at a couple of these methods in more detail.
prepareBoard: ->
...
$("<div>", {class: "square"}).appendTo("#board") for square in [0..8]
Here we iterate nine times and add nine divs with a class of square
to the empty board in order to populate it. This demonstrates how CoffeeScript lets you write one-line loops and declare the body of the loop before writing the condition itself.
updateNotifications: ->
$(".notifications").empty().show()
@.addNotification "#{@data.player1} is playing #{@data.rolep1}"
...
CoffeeScript allows for string interpolation which increases readability and reduces complexity and code length. You can add a #{}
within any string and insert any variable or return value from a function call within the braces.
addNotification: (msg) ->
$(".notifications").append($("<p>", text: msg));
The addNotification
method exemplifies how you define parameters in CoffeeScript. You write them before the arrow (->
):
You can provide default values for parameters similar to PHP:
addNotification: (msg = "I am a message") ->
When a function with a default parameter is compiled, it is converted to:
if (msg == null) { msg = "I am a message"; }
Finally, let’s turn to the addListeners
method:
addListeners: ->
$(".square").click ->
if Tic.data.gameOver is no and not $(@).text().length
if Tic.data.turns % 2 is 0 then $(@).html("X").addClass("x moved")
else if Tic.data.turns % 2 isnt 0 then $(@).html("O").addClass("o moved")
...
Here we see that CoffeeScript offers additional keywords to represent truthy and falsy values such as no
, yes
, off
and on
. Additionally, !==
, ===
, &&
, !
can be represented using isnt
, is
, and
and not
accordingly.
You can make readable single line conditionals using if ... then ... else ...
syntax.
The Mechanics of the Game
The workhorse method checkEnd
checks if there is a winner every time a player makes a move. It does this by iterating over the board and counting the squares that belong to X and O. It first checks the diagonal axes, then the vertical, then the horizontal.
checkEnd : ->
@.data.x = {}
@.data.o = {}
#diagonal check
diagonals = [[0,4,8], [2,4,6]]
for diagonal in diagonals
for col in diagonal
@.checkField(col, 'diagonal')
@.checkWin()
@.emptyStorageVar('diagonal')
for row in [0..2]
start = row * 3
end = (row * 3) + 2
middle = (row * 3) + 1
#vertical check
@checkField(start, 'start')
@checkField(middle, 'middle')
@checkField(end, 'end')
@checkWin()
#horizontal check
for column in [start..end]
@checkField(column, 'horizontal')
@checkWin()
@emptyStorageVar('horizontal')
As you can see, this makes use of another handy CoffeeScript feature—ranges.
for row in [0..2]
This will loop three times, setting row equal to 0, 1 and 2 in this order. Alternatively, [0...2]
(an exclusive range) would result in just two iterations, setting row equal to 0 and 1.
In the horizontal check we see again how indentation is crucial in determining what is part of the loop and what is outside of the loop—only the checkField
call is inside the inner loop.
This is what checkField
looks like:
checkField: (field, storageVar) ->
if $(".square").eq(field).hasClass("x")
if @.data.x[storageVar]? then @.data.x[storageVar]++ else @.data.x[storageVar] = 1
else if $(".square").eq(field).hasClass("o")
if @.data.o[storageVar]? then @.data.o[storageVar]++ else @.data.o[storageVar] = 1
This method demonstrates the use of the ?
keyword, which when inserted next to a variable in a conditional, compiles to:
if (typeof someVariable !== "undefined" && someVariable !== null) {
Which is obviously quite handy.
What the checkField
method does is add one to the appropriate axis of the x
or o
property depending on the class name of the square which was clicked. The class name is added when a user clicks on an empty board square in the addListeners
method.
This brings us on to the checkWin
method, which is used to check if one of the players has won the game:
checkWin: ->
for key,value of @.data.x
if value >= 3
localStorage.x++
@showAlert "#{@.getPlayerName("X")} wins"
@data.gameOver = true
@addToScore("X")
for key,value of @.data.o
if value >= 3
localStorage.o++
@showAlert "#{@.getPlayerName("O")} wins"
@data.gameOver = true
@addToScore("O")
In CoffeeScript you can use for ... in array
to loop over array values and for key,value of object
to loop over the properties of an object. checkWin
utilizes this to check all the properties inside the x
and o
objects. If any of them holds a number greater than or equal to three, then we have a winner and the game should end. In such a case, we call the addToScore
method which persists the results of the players through localStorage
.
A Word about Local Storage
LocalStorage is part of the Web Storage specification and has a pretty good browser support. It allows you to store data (similar to cookies) on the user’s machine and access it whenever you want.
You can access the API in several ways, for example just as you would to the properties of a regular object:
//fetch item
localStorage.myProperty
// set item
localStorage.myProperty = 123
Local storage always saves strings so if you want to store an object or an array you would have to use JSON.stringify
when storing the array/object and JSON.parse
when retrieving it.
Our addToScore
method utilizes this fact:
addToScore: (winningParty) ->
...
if winningParty is "none"
@.showAlert "The game was a tie"
else
...
localStorage[@data.player1] = JSON.stringify @data.p1stats
It also demonstrates how you can omit parentheses in CoffeeScript (JSON.stringify
), although that is recommended for the outermost function calls only.
Next we have a couple of utility methods. We use emptyStorageVar
to clear the contents of a particular horizontal row or diagonal. This is necessary because there are two diagonals on the board and inside our chekEnd
method we use the same data property for both diagonals. Therefore, we have to clear the property before checking the second diagonal. The same goes for the horizontal rows.
emptyStorageVar: (storageVar) ->
@.data.x[storageVar] = null
@.data.o[storageVar] = null
Getting the Player Names
When the form with the names of the players is submitted at the beginning of a game, we can prevent its default action and handle the submission using JavaScript. We check if there is an empty name or if both names are the same and display a friendly alert if so. Otherwise, we start the game by calling Tic.initialize()
.
$("form").on "submit", (evt) ->
evt.preventDefault()
$inputs = $("input[type='text']")
namesNotEntered = $inputs.filter(->
return @.value.trim() isnt ""
).length isnt 2
namesIndentical = $inputs[0].value is $inputs[1].value
if namesNotEntered then Tic.showAlert("Player names cannot be empty")
else if namesIndentical then Tic.showAlert("Player names cannot be identical")
else Tic.initialize()
The final line uses event delegation to have any element with the class play-again
respond to a click. Event delegation is necessary, as this element is only added to a page once a game has finished. It is not present when the DOM is first rendered.
$("body").on("click", ".play-again", -> Tic.initialize())
Putting it all Together
And that’s it. In less than 150 lines of CoffeeScript we have a working game. Don’t forget, you can download the code from this tutorial from GitHub.
See the Pen Tic-Tac-Toe by SitePoint (@SitePoint) on CodePen.
Conclusion
I hope that this tutorial has solidified your knowledge of CoffeeScript and has shown you how jQuery and CoffeeScript can work together. There are many things that you can do to improve the game. For example you could add an option to make the board different than its standard 3×3 dimensions. You could implement some simple AI so that players can play against the machine, or you could implement bombs in the game, e.g. by adding a random X or O on a random game move while the players are battling for glory.