TL;DR: In this article, you will learn how to build a real-time, collaborative Tic Tac Toe game application that enables two users to play the game simultaneously. To create this app, you will use React & Yjs.
We were trying to build realtime multiplayer editing capabilities into ToolJet (https://github.com/ToolJet/ToolJet/). Those who are not familiar with ToolJet, it is an open-source low-code application builder. We had tried different approaches for this but found Yjs to be the best approach. Hope this tutorial will help you get started with Yjs and CRDTs. If you want to see how capable is CRDTs, check out our PR for realtime multiplayer editing of applications in ToolJet.
Yjs is a CRDT implementation that exposes its internal data structure as shared types which we will be using to build a real time collaborative game tic tac toe.
Want to jump straight to the code? here it is: https://github.com/ToolJet/yjs-crdt-game
Building the UI
Let's first start by building the 3-by-3 grid user interface for the game and then add reactivity to it
Let's start by creating our parent component <Game />,
<div className="game">
<div className="game-board">
...
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
The game component does the following:
- Shows the status of the game i.e. displays the winner or displays who has to play the next turn
- Allows a user to reset the game from the beginning
- Show a list of moves performed
- Renders the game board (explained in the next section)
<Board /> component will look like this:
<div className="game-board">
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
</div>
The board component renders squares that we need to display for user input:
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
<Square /> component is a simple functional component that renders a square:
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
After adding all these components our UI should look like the following:
Adding the game logic
Let's start by adding the initial state of the application inside the <Game /> component:
this.state = {
history: [
{
squares: Array(9).fill(null),
},
],
stepNumber: 0,
xIsNext: true,
};
Initially, all nine square are neither filled with "X" or "O" hence we are storing the array with nine null values, initialising the step with count 0 and allowing "X" to be the first one to make the move.
In the game of tic tac toe, a winner is decided whenever the player succeeds in placing three of their marks in a horizontal, vertical, or diagonal row is the winner.
Let's convert this into code:
function calculateWinner(squares) {
const lines = [
[0, 1, 2], // horizontal
[3, 4, 5], // horizontal
[6, 7, 8], // horizontal
[0, 3, 6], // vertical
[1, 4, 7], // vertical
[2, 5, 8], // vertical
[0, 4, 8], // diagonal
[2, 4, 6], // diagonal
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
In the above function we pass a variable called squares
that we had declared in our <Game /> component. Each square[i] contains either 'X' or 'O'. In the above for loop we check if the three consecutive values in either horizontal, vertical or diagonal contain the same value i.e. either X or O. If its true then 'X' or 'O' is returned as the winner.
Making the game collaborative
Let's add yjs to the code for allowing two users to be able to play the game collaboratively.
For this purpose we are going to use two packages yjs and y-webrtc.
const ydoc = new Y.Doc();
const provider = new WebrtcProvider(`tutorial-tic-tac-toe`, ydoc);
To start of with we create a ydoc which represents a yjs document. This document is passed to a webrtc provider that helps us utilize public signalling servers for creating a peer-to-peer connection.
componentDidMount() {
const ymap = ydoc.getMap('state');
ymap.observe(() => {
this.setState({
...ymap.get('state')
})
});
}
In the componentDidMount function we are going to declare a variable ymap, which gets a shared data type Y.Map called 'state'.
Then on the shared data type we add an observable to observe the changes on the state.
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState(
{
history: history.concat([
{
squares: squares,
},
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
},
() => {
const ymap = ydoc.getMap('state');
ymap.set('state', this.state);
}
);
}
Whenever a use clicks on any square, we use ymap.set(..)
function to set a key value on the Y.map shared data type.
Now whenever we have a change on the ydoc the observable is called and all peers connected to the room through webrtc signalling server will receive the update.
The final outcome looks like below:
You can find the code at the GitHub repo https://github.com/ToolJet/yjs-crdt-game
Top comments (0)