I’ve been playing around with Rails 5 Beta and as great as the Rails::API integration is, I think by far my favourite new feature is Action Cable. A lot of the examples out there that showcase the power of Action Cable are either a chat or a live messaging system. Although they were great in helping me understand what I can do with Action Cable, to help further my understanding I decided to build a Tic Tac Toe game (Noughts and Crosses for us Brits).

I won’t dive into Action Cable in this blog post as I feel there are plenty of resources online already covering this, I’ll put the links below. Grab a friend (or another browser tab) and try out the finished demo.

Tic Tac Toe

This idea for the game is to allow two players to connect to the server and automatically get matched up and then play Tic Tac Toe. Action Cable WebSocket channel will be used to stream the moves of each player to their opponent.

If you are not familiar with the rules of Tic Tac Toe, it’s mainly played on a 3×3 grid, and the players take turn marking the grid. The player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row wins the game. Find out more.

Setup

We will begin with a single rails new command rails _5.0.0.rc1_ new tic-tac-toe. I’m specifying the version number because I have multiple copies of Rails. If you need any help with the setup of Action Cable check out the Readme or a more in-depth tutorial.

Then rails s command should work with no error. rails generate controller grid to have a landing page for the players.
To have the Action Cable part we need to do a bit of setting up in the following files

# config/routes.rb

Rails.application.routes.draw do
  root to: "grid#index"
  mount ActionCable.server => "/cable"
end

Make sure this is uncommented in cable.js

// app/assets/javascript/cable.js

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();

}).call(this);

Channels

When an app loads from a client-side one Action cable connection to the server is open, however, you can then further subscribe to many channels using this one connection. These channels are used to send and receive messages to the server, and to communicate a type of event or activity. We will be using one to send moves across to each player.

We can use a generator to create a channel class: rails generate channel game, a couple of files will be created.

Identifying Players

We need a way to be able to keep track of the players and their moves, So we will uniquely identify a connection object, which gives us a way to determine the players in the channel. Later on, we can then access this identifier through the instance variable uuid.

# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :uuid

    def connect
      self.uuid = SecureRandom.urlsafe_base64
    end
  end
end

Then in one of the generated channel class file, we will add to the game channel subscribe method

# app/channels/game_channel.rb

class GameChannel < ApplicationCable::Channel
  def subscribed
    stream_from "player_#{uuid}"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

Match Making

So far (with little effort) we have it so that when a new player lands on our page, they are given a unique identifier and are ready to be assigned to a game so they can begin playing.

The matchmaking of players will work as follows:

Let’s say we have two players (John and Lucy)

  • When John lands on the grid page he is given a unique identifier and is ready to play the game. We will then check if anyone is waiting and if not, we will then store somewhere that John is here and is looking for an opponent.
  • When Lucy lands on the same page she will be given a unique identity and then will be matched up with John, who is ready to play the game.

The two players will be matched up, and then the game will begin. We are going to create a model call Match which will handle the matchmaking.

We will need to set up redis quickly to use as a database for the match info; I uncommented the gem (gem 'redis', '~> 3.0') from the gemfile. Then I created a config to hold a REDIS constant.

# config/redis.rb

REDIS = Redis.new(Rails.application.config_for("cable"))

rails generate model match and rails generate model game

# app/models/match.rb

class Match < ApplicationRecord
  def self.create(uuid)
    if REDIS.get("matches").blank?
      REDIS.set("matches", uuid)
    else
    # Get the uuid of the player waiting
      opponent = REDIS.get("matches")

      Game.start(uuid, opponent)
      # Clear the waiting key as no one new is waiting
      REDIS.set("matches", nil)
    end
  end
end

The Game model houses the gameplay logic, like when a play has made a move, or a player withdrew.

class Game < ApplicationRecord
  def self.start(player1, player2)
    # Randomly choses who gets to be noughts or crosses
    cross, nought = [player1, player2].shuffle

    # Broadcast back to the players subscribed to the channel that the game has started
    ActionCable.server.broadcast "player_#{cross}", {action: "game_start", msg: "Cross"}
    ActionCable.server.broadcast "player_#{nought}", {action: "game_start", msg: "Nought"}

    # Store the details of each opponent
    REDIS.set("opponent_for:#{cross}", nought)
    REDIS.set("opponent_for:#{nought}", cross)
  end
end

then modify the Channel class to allow new Match.

# app/channels/game_channel.rb

class GameChannel < ApplicationCable::Channel
  def subscribed
     stream_from "player_#{uuid}"
     Match.create(uuid)
  end

We need to now set up the client side to be able to give an update on the players waiting.

App.game = App.cable.subscriptions.create "GameChannel",
  connected: ->
    # Called when the subscription is ready for use on the server
    $('#status').html("Waiting for an other payer")

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # Called when there's incoming data on the websocket for this channel

We will then create a view so we can to test it out.

<!-- app/views/grid/index.html.erb -->
<h1>Tic Tac Toe </h1>
<p id="status"></p>

The game

Above is the basic setup that is needed to have a connection between two players. for the actual Tic Tac Toe game I modified an already built game (thanks to Derek Anderson). Check out the repo for the game logic. I made the following modifications to the channels to be able to broadcast the moves between opponents.

# app/assets/javascript/channels/game.coffee

App.game = App.cable.subscriptions.create "GameChannel",
  connected: ->
    # Called when the subscription is ready for use on the server
    $('#status').html("Waiting for an other payer")

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # Called when there's incoming data on the websocket for this channel
    switch data.action
      when "game_start"
        $('#status').html("Player found")
        App.gamePlay = new Game('#game-container', data.msg)

      when "take_turn"
        App.gamePlay.move data.move
        App.gamePlay.getTurn()

      when "new_game"
        App.gamePlay.newGame()

      when "opponent_withdraw"
        $('#status').html("Opponent withdraw, You win!")
        $('#new-match').removeClass('hidden');

  take_turn: (move) ->
    @perform 'take_turn', data: move

  new_game: () ->
    @perform 'new_game'
# app/channels/game_channel.rb
# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class GameChannel < ApplicationCable::Channel
  def subscribed
     stream_from "player_#{uuid}"
     Match.create(uuid)
  end

  def unsubscribed
    Game.withdraw(uuid)
    # Remove yourself from the waiting list
    Match.remove(uuid)
  end

  def take_turn(data)
    Game.take_turn(uuid, data)
  end

  def new_game()
    Game.new_game(uuid)
  end
end

Pitfalls

Server restart

It took a while to get used to and it solved many of my problems, but when you edit any of the channel classes make sure you restart the server!!

Heroku Deploy

After you’ve finished your fantastic app and want to show it off to the world using Heroku, follow this guide. Additional to that, make sure you uncomment the following from production.rb

# config/environments/production.rb

# Action Cable endpoint configuration
config.action_cable.url = 'wss://cookieshq-tictactoe.herokuapp.com/cable'
config.action_cable.allowed_request_origins = [ 'https://cookieshq-tictactoe.herokuapp.com', /http:\/\/cookieshq-tictactoe.herokuapp.com.*/  ]

Source and Demo

Play The Demo

And grab a copy and play around, source on Github.

Further

For me, this was a great next step after following DHH’s chat app. I want to further improve my knowledge of Action Cable and Rails 5 in general by adding more to this app as time goes by:

  • A way to have a user system
  • To be able to save scores after the user system
  • Improve the matching and withdraw logic
  • Play against AI if an opponent isn’t in a reasonable amount of time

More Action Cable

Chess implementation in Action Cable

Quick intro to Action Cable by DHH

An in-Depth Tutorial