Skip to main content

build simple tic tac toe with rails action cable

ยท 10 min read

Tictactoe, it's a classical games that everybody knows. Before deep down into the implementation, I just wanna share the simple runnable code to build a tictactoe on single web page using WebSocket connection. In here, I used Rails 7 to simplify the build process, a simple database to maintain the state and a tailwind css to simplify the css ๐Ÿ‘

Initializationโ€‹

The initialization is quite simple, just follow the classical rails way to create a new project. Just simply as

rails new tictactoe

If you not have rails installed, you can follow this link

After initialize the project, we need to define the architecture for tictactoe game.

Architectureโ€‹

The architecture is quite simple, just follow this diagram

Yeah, we just need one class to implement the tictactoe game. ๐Ÿ‘

The flowโ€‹

The current flow just simple, two players on one page, and played together. Every turn, player must be click on end button to confirm the move. The diagram just like this

And for the evaluation process the diagram just like this:

Winning strategiesโ€‹

TicTacToe is very simple game, but have many winning strategies. The winning strategies is consist of 3 rules. These rules are

  1. If one player can pot the symbol in one horizontal line
  2. If one player can pot the symbol in one vertical line
  3. The rest is if one player can pot the symbol in one crossed line

The codeโ€‹

Let's deep down into the code, first of all, we need to create a rails migration. By entering this command you can build the Game class.

rails generate model game state:json symbol:string first_player:string second_player:string finished_at:timestamp
rails db:migrate

After creating the game class, we need to create all the function that required in the specifications. We should add validation on first player and second player to be present. So we need add

app/models/game.rb
validates :first_player, presence: true
validates :second_player, presence: true

After that, we should add validation before creating the game instance. The validation is quite simple, just need to create default state of the board. The board is consist of 3 columns and 3 rows. We need initialize all the matrix using nil values. And initialize the first player is using x sign.

app/models/game.rb
before_validation(on: :create) do
self.state = {
0 => { 0 => nil, 1 => nil, 2 => nil },
1 => { 0 => nil, 1 => nil, 2 => nil },
2 => { 0 => nil, 1 => nil, 2 => nil },
}
self.symbol = :x
end

Okay, the code initialization is done, and let's move to the functions. Let's create the first function, [] is the function to assign the instance to an array-like, but we manipulated it to our state to make it easier to work with. And we need to create the move function, the purpose of the function just to assign nil matrix with a symbol, and after the user move, the symbol should switch.

app/models/game.rb
def [](row, col)
state[row.to_s][col.to_s]
end

def move!(row, col)
state[row.to_s][col.to_s] = symbol

return if finished?

self.symbol = symbol == "x" ? "o" : "x"
self.save!
end

After that we should create a function to evaluate our game. The function should follow our winning strategies that we decided before. And all of the function is private function. The code just like this

app/models/game.rb
def check_the_winner
values = state.values.map(&:values)

if values.flatten.all?
self.symbol = 't'
self.finished_at = Time.zone.now()
self.save! && return
end

winner_symbol = check_vertical(values.flatten, 0) || check_horizontal(values, 0) || check_crossed(values.flatten)

if winner_symbol == 'x' || winner_symbol == 'o'
self.symbol = winner_symbol
self.finished_at = Time.zone.now()
self.save!
end
end

def check_vertical(values, index)
return false if index == 3

point = [0, 3, 6]
data = point.map(&index.method(:+)).map{|p| values[p]}

return 'x' if check_values(data, 'x')
return 'o' if check_values(data, 'o')
check_vertical(values, index + 1)
end

def check_horizontal(values, index)
return false if index == 3
return 'x' if check_values(values[index], 'x')
return 'o' if check_values(values[index], 'o')
check_horizontal(values, index + 1)
end

def check_crossed(data)
crossed = [[0, 4, 8], [2, 4, 6]].map do |check|
value = check.map{|p| data[p]}
return 'x' if check_values(value, 'x')
return 'o' if check_values(value, 'o')
false
end

return 'x' if crossed.any?(&'x'.method(:==))
return 'o' if crossed.any?(&'o'.method(:==))
false
end

def check_values(data, value)
data.all?(&value.method(:==))
end

Okay, after all of the winning strategies writen down to code, just set for our evaluation to our game class.

after_save :check_the_winner, if: Proc.new { |g| !g.finished? }

Yeah, all of the code is assembled and ready to integrate with front-end. But before that, here is the full version of game.rb

app/models/game.rb
class Game < ApplicationRecord
validates :first_player, presence: true
validates :second_player, presence: true

before_validation(on: :create) do
self.state = {
0 => { 0 => nil, 1 => nil, 2 => nil },
1 => { 0 => nil, 1 => nil, 2 => nil },
2 => { 0 => nil, 1 => nil, 2 => nil },
}
self.symbol = :x
end

after_save :check_the_winner, if: Proc.new { |g| !g.finished? }

def [](row, col)
state[row.to_s][col.to_s]
end

def move!(row, col)
state[row.to_s][col.to_s] = symbol

return if finished?

self.symbol = symbol == "x" ? "o" : "x"
self.save!
end

def finished?
finished_at.present?
end

private

def check_the_winner
values = state.values.map(&:values)

if values.flatten.all?
self.symbol = 't'
self.finished_at = Time.zone.now()
self.save! && return
end

winner_symbol = check_vertical(values.flatten, 0) || check_horizontal(values, 0) || check_crossed(values.flatten)

if winner_symbol == 'x' || winner_symbol == 'o'
self.symbol = winner_symbol
self.finished_at = Time.zone.now()
self.save!
end
end

def check_vertical(values, index)
return false if index == 3

point = [0, 3, 6]
data = point.map(&index.method(:+)).map{|p| values[p]}

return 'x' if check_values(data, 'x')
return 'o' if check_values(data, 'o')
return check_vertical(values, index + 1)
end

def check_horizontal(values, index)
return false if index == 3
return 'x' if check_values(values[index], 'x')
return 'o' if check_values(values[index], 'o')
check_horizontal(values, index + 1)
end

def check_crossed(data)
crossed = [[0, 4, 8], [2, 4, 6]].map do |check|
value = check.map{|p| data[p]}
return 'x' if check_values(value, 'x')
return 'o' if check_values(value, 'o')
false
end

return 'x' if crossed.any?(&'x'.method(:==))
return 'o' if crossed.any?(&'o'.method(:==))
false
end

def check_values(data, value)
data.all?(&value.method(:==))
end
end

The Front end applicationโ€‹

Just built a simple webpage that retrives data from controller and render it to views. We need to create websocket connection between them. First of all, you can add the routes just like this

resources :games, except: [:delete] do
resources :moves, only: [:create]
end

And all you need to do is create the controller for all you routes by hitting this command

rails generate controller game index show create
rails generate controller move create

And for the controller we need to add this

app/controllers/games_controller.rb
class GamesController < ApplicationController
def index
@games = Game.all
end

def show
@game = Game.find(params[:id])
end

def create
redirect_to Game.create(permitted_params)
end

private

def permitted_params
params.permit(:first_player, :second_player)
end
end
app/controllers/games_controller.rb
class MovesController < ApplicationController
def create
@game = Game.find(params[:game_id])
if params[:button] == 'reset'
@game.destroy!
redirect_to root_path
else
matrix = params[:choose].split(',')
@game.move!(matrix.first, matrix.last)
respond_to do |format|
format.turbo_stream
format.html {
redirect_to @game
}
end
end
end
end

To ensure that the page no need to refresh, we should add the callback after we save the game instance. So we need to add broadcast_update into our game model

app/models/game.rb
after_update_commit { broadcast_update }

And the rest is just a view. So, we need create a simple view to render all the data from the controller and turbo stream as well. The view just like this

app/views/games/index.html.erb
<h1 class="text-2xl font-semibold">Tic Tac Toe Games</h1>

<%= form_with url: games_path, method: :post, id: "new_game" do |f| %>
<%= f.label :first_player, class: "block" do %>
<span class="text-gray-700">Player 1 Name</span>
<%= f.text_field :first_player, class: "mt-0 block w-full px-0.5 border-0 border-b-2 border-gray-200 focus:ring-0 focus:border-black" %>
<% end %>

<%= f.label :second_player, class: "block" do %>
<span class="text-gray-700">Player 2 Name</span>
<%= f.text_field :second_player, class: "mt-0 block w-full px-0.5 border-0 border-b-2 border-gray-200 focus:ring-0 focus:border-black" %>
<% end %>

<div class="mt-4">
<%= f.submit "New game", class: "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800" %>
</div>
<% end %>
app/views/games/show.html.erb
<h1 class="text-2xl font-semibold">Tic Tac Toe Games</h1>

<%= turbo_stream_from @game %>

<div id="<%= dom_id(@game) %>" class="flex mt-2">
<%= render @game %>
</div>
app/views/games/_game.html.erb
<div class="flex flex-col">
<% if !game.finished? %>
<div>
Turn for <%= game.symbol == 'x' ? game.first_player : game.second_player %>
</div>
<% end %>

<% if game.finished? %>
<% if game.winner == "tied" %>
<div>The game is tied</div>
<% else %>
<div>The winner is <%= game.winner %></div>
<% end %>
<% end %>

<%= form_with url: game_moves_path(game), method: :post, class: "mt-4", id: "game-container" do |form| %>
<div class="flex flex-col">
<% 3.times do |row| %>
<div class="flex border-b border-black w-36 first:border-t">
<% 3.times do |col| %>
<span class="flex border-l border-black last:border-r items-center justify-center h-12 w-12">
<% if game[row, col].present? %>
<%= game[row, col] %>
<% elsif game.finished? %>
-
<% else %>
<%= form.label :choose do %>
<%= form.radio_button :choose, "#{row},#{col}", required: true %>
<% end %>
<% end %>
</span>
<% end %>
</div>
<% end %>
</div>

<div class="flex mt-4">
<%= form.submit "End", disabled: game.finished?, class: "disabled:cursor-not-allowed text-white bg-blue-700 cursor-pointer hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800" %>
<%= form.button "Reset", value: "reset", class: "disabled:cursor-not-allowed text-white bg-blue-700 cursor-pointer hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800" %>
<div>
<% end %>
</div>

And for our final view just render the turbo stream from moves controller, the code just like this

app/views/moves/create.turbo_stream.erb
<%= turbo_stream.update dom_id(@game) do %>
<%= render @game %>
<% end %>

Conclusionโ€‹

The tictactoe game is very simple and easy to work with. All we need just pay attention to detail and simplify the process. By leveraging the turbo stream feature we shouldn't need to refresh our page for next move. The callbacks from models just insane, because we doesn't care about updating the data to our views. All you need just stream directly from the model via broadcast_update. But here is all the code I hosted on my github