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
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