Practical CoffeeScript: Making a Tic-Tac-Toe Game

Ivan Dimov
Share

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:

Screen shot of Prepros

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 the data object.
  • retrieveStats retrieves the player’s statistics from localStorage and sets them up in the data 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.