Turns and Deployment: Multiplayer React Tic Tac Toe Game

8 min readDec 2, 2022

This Tutorial Is Out of Date

While part two of this tutorial series contains useful information, this post was written using PubNub's React SDK V1. Check out our React SDK docs for an up-to-date reference. You can learn more about how PubNub powers thousands of customers worldwide in our PubNub for Developers resources.

Have suggestions or questions about the content of this post? Reach out to devrel@pubnub.com.

Note: This is part two of building a multiplayer tic-tac-toe game in React Native using PubNub. If you have not completed part one of the tutorial, which focuses on environment setup, lobby creation, and inviting other players to join, please go back to part one of the tutorial. Remember, you can view the completed application on GitHub.

Welcome to Part Two of the tutorial series on building a mobile multiplayer tic-tac-toe game with React Native and PubNub. In this section, you will implement the core functionality of the game and play the game.

If you haven’t already, please check out and go through Part One before working on this section, as the project was initialized and the lobby was set up in that section.

Implementing the Game Component

From the project root directory, go to src/components and create a new file named Game.js. All the game logic will be inside this file. Add the following code to the file.

import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View,
  TouchableHighlight,
  Alert
} from 'react-native';

import range from 'lodash';
const _ = range;

export default class Game extends Component {

  constructor(props) {
    super(props);	
    
    // Winning combinations
    this.possible_combinations = [
      [0, 3, 6],
      [1, 4, 7],
      [0, 1, 2],
      [3, 4, 5],
      [2, 5, 8],
      [6, 7, 8],
      [0, 4, 8],
      [2, 4, 6]
    ];

    // Corresponds to the square number on the table
    this.ids = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8]
    ];
                
    // For the 3x3 table
    this.rows = [
      _.range(3).fill(''),
      _.range(3).fill(''),
      _.range(3).fill(''),
    ];
  
    this.state = {
      moves: _.range(9).fill(''),
      x_score: 0,
      o_score: 0,
    }
    this.turn = 'X' // Changes every time a player makes a move
    this.game_over = false; // Set to true when the game is over
    this.count = 0; // used to check if the game ends in a draw
  }
}

There are several things occurring in the base constructor.

  1. An array of possible combinations to win the game is set up. If a player matches with any of the combinations, that player is the winner of the game.

  2. Another array named ids is initialized and labels every square of the table with a unique id to check if a square has been occupied or not.

  3. The variable rows contains three empty arrays with each size of three.

  4. For the state objects, the array moves is initialized with empty string values and the score for both players to 0. The array moves will be used to check if there is a winner, discussed later on.

  5. Finally, three variables are added, turn, game_over, and count that will be useful throughout the game.

Setting up the UI

The UI for the component includes the table and its styles, current score, and username for each player.

render() {
  return (
    <View style={styles.table_container}>
      <View style={styles.table}>
        {this.generateRows()}
      </View>

      <View style={styles.scores_container}>
        <View style={styles.score}>
          <Text style={styles.user_score}>{this.state.x_score}</Text>
          <Text style={styles.username}>{this.props.x_username} (X)</Text>
        </View>
        
        <View style={styles.score}>
          <Text style={styles.user_score}>{this.state.o_score}</Text>
          <Text style={styles.username}>{this.props.o_username} (O)</Text>
        </View>
      </View>				
    </View>
  );
 }
}

const styles = StyleSheet.create({
  table_container: {
    flex: 9
  },
  table: {
    flex: 7,
    flexDirection: 'column',
    color: 'black'
  },
  row: {
    flex: 1,
    flexDirection: 'row',
    borderBottomWidth: 1,
  },
  block: {
    flex: 1,
    borderRightWidth: 1,
    borderColor: '#000',
    alignItems: 'center',
    justifyContent: 'center'
  },
  block_text: {
    fontSize: 30,
    fontWeight: 'bold',
    color: 'black'
  },
  scores_container: {
    flex: 2,
    flexDirection: 'row',
    alignItems: 'center'
  },
  score: {
    flex: 1,
    alignItems: 'center',
  },
  user_score: {
    fontSize: 25,
    fontWeight: 'bold',
    color: 'black'
  },
  username: {
    fontSize: 20,
    color: 'black'
  }
});

For the username, this.props is used since the value is obtained from App.js. As a reminder from part one of the tutorial, here are the values that you’ll be using from App.js.

<Game 
  pubnub={this.pubnub}
  channel={this.channel} 
  username={this.state.username} 
  piece={this.state.piece}
  x_username={this.state.x_username}
  o_username={this.state.o_username}
  is_room_creator={this.state.is_room_creator}
  endGame={this.endGame}
/>

To build the tic-tac-toe game board, generateRows is called, which calls generateBlocks. These methods are responsible for creating the table.

generateRows = () => {
  return this.rows.map((row, col_index) => {
    return (
      <View style={styles.row} key={col_index}>
        {this.generateBlocks(row, col_index)}
      </View>
    );
  });
}

generateBlocks = (row, row_index) => {
  return row.map((block, col_index) => {
    let id = this.ids[row_index][col_index];
    return (
      <TouchableHighlight 
        key={col_index} 
        onPress={
          this.onMakeMove.bind(this, row_index, col_index)
        } 
        underlayColor={"#CCC"} 
        style={styles.block}>

        <Text style={styles.block_text}>
          {
            this.state.moves[id]
          }
        </Text>
      
      </TouchableHighlight>	
    );
  });
}

TouchableHightlight is called so players can touch any square on the table. In order for their corresponding piece to be placed on that square, set the block text to be the value of this.state.moves[id], which will contain the correct piece for the player that made the move. This will be discussed in detail later on in the tutorial.

Adding the Logic

Create the function onMakeMove with row_index, the row that the piece was placed on, and col_index, the column the piece was placed on, as the method arguments.

onMakeMove(row_index, col_index) {
  let moves = this.state.moves;
  let id = this.ids[row_index][col_index];

  // Check that the square is empty
  if(!moves[id] && (this.turn === this.props.piece)){ 
    moves[id] = this.props.piece;
    
    this.setState({
      moves
    });

    // Change the turn so the next player can make a move
    this.turn = (this.turn === 'X') ? 'O' : 'X';
    
    // Publish the data to the game channel
    this.props.pubnub.publish({
      message: {
        row: row_index,
        col: col_index,
        piece: this.props.piece,
        is_room_creator: this.props.is_room_creator,
        turn: this.turn
      },
      channel: this.props.channel
    });
    this.updateScores.call(this, moves);
  }
}

The integer ID of the square pressed is obtained by getting the value of this.ids[row_index][col_index]. The if statement checks if the square the player touched is empty and if it is the current player's turn to play. The touch is ignored if these two conditions are not met. If the conditions are met, the piece is added to the array moves with id as the index.

For example, if the room creator makes a move on row 0 column 2 on the table, then this.ids[0][2] returns the integer 2.

this.ids = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8]
];

The piece X is added to the array moves on the second index: [“”, “”, “X”, “”, “”, “”, “”, “”, “”]. Later in updateScoresyou will see how moves is used to check for a winner.

Back in onMakeMove, after the state for moves is changed, turns is updated so the next player can make their move. Data is published to the game channel, such as the piece that moved and its position. The other player’s table updates with the current data. Finally, make a call to updateScores to check if there is a winner or if the game ended in a draw.

Implement the channel listener in componentDidMount to listen to incoming messages from the channel.

componentDidMount() {
  // Listen for messages in the channel
  this.props.pubnub.getMessage(this.props.channel, (msg) => {
    // Add the other player's move to the table
    if(msg.message.turn === this.props.piece){
      let moves = this.state.moves;
      let id = this.ids[msg.message.row][msg.message.col];

      moves[id] = msg.message.piece;
      
      this.setState({
        moves
      });

      this.turn = msg.message.turn;
      this.updateScores.call(this, moves);
    }
  });
}

Both players will receive this message since both are subscribed to the same channel. When the message is received, several of the same steps performed in onMakeMove occur: update the table and check if there is a winner. However, the player that made the move should not repeat the steps again. Perform a if statement to ensure that only the opposite player performs the steps. Do this with the conditional if turn (which is either X or O) matches the player’s piece.

Next, implement the method updateScores.

updateScores = (moves) => {
 // Iterate the double array possible_combinations to check if there is a winner
 for (let i = 0; i < this.possible_combinations.length; i++) {
    const [a, b, c] = this.possible_combinations[i];
    if (moves[a] && moves[a] === moves[b] && moves[a] === moves[c]) {
      this.determineWinner(moves[a]);	
      break;
    }
  }
}

To determine if there is a winner, you'll need to iterate possible_combinations to check if any of the combinations are present in the table. This is where the array moves comes in handy. You'll use [a,b,c] to get each array of possible_combinations. Then check if that matches any pattern in moves.

For example, the room creator makes a winning move on row 2 column 2, with an id of 8, on the table. The table will look as follows:

winning move example

The winning moves are in positions 2, 5, and 8, according to the IDs. The array moves is now:  [“O”, “”, “X”, “”, “O”, “X”, “”, “”, “X”]. In updateScores, iterate through every possible winning combination.

For this example, the winning combination is [2,5,8]. So when [a,b,c] has the values of [2,5,8], the if statement in the for loop will be true since [2,5,8] all have the same value of X. A call is made to determineWinner to update the score of the winning player, which in this case, is the room creator. Before implementing that method, finish the rest of updateScores.

If no winner is found and the game ends in a draw, then neither player gets a point. To check if there is a draw, add the following code below the above for loop.

this.count++;
// Check if the game ends in a draw
if(this.count === 9){
  this.game_over = true;
  this.newGame();	
}

Every time a square is pressed on the table updateScores is called. If no winner is found, then count increments by one. If the count is equal to 9, the game ends in a draw. This occurs when a player makes a final move on the last square of the table that is not a winning move, so count is incremented from 8 to 9. The method newGame is then called.

Next, implement determineWinner.

determineWinner = (winner) => {
  var pieces = {
    'X': this.state.x_score,
    'O': this.state.o_score
  }

  // Update score for the winner
  if(winner === 'X'){
    pieces['X'] += 1;
    this.setState({
      x_score: pieces['X']
    });
  }
  else{
    pieces['O'] += 1;
    this.setState({
      o_score: pieces['O']
    });
  }
  // End the game once there is a winner
  this.game_over = true;
  this.newGame();	
}

You check who has won the game and increment the winner’s current score by one point. Once the score has been updated, game_over is set to true and calls the method newGame.

Game Over

Two different alerts are shown to the room creator and the opponent. The alert for the room creator has a message asking them if they want to play another round or exit the game.

new_game = () => {
  // Show this alert if the player is not the room creator
  if((this.props.is_room_creator === false) && this.game_over){
    Alert.alert('Game Over','Waiting for rematch...');
    this.turn = 'X'; // Set turn to X so opponent can't make a move
  }

  // Show this alert to the room creator
  else if(this.props.is_room_creator && this.game_over){
    Alert.alert(
      "Game Over!", 
      "Do you want to play another round?",
      [
        {
          text: "Nah", 
          onPress: () => {
            this.props.pubnub.publish({
              message: {
                gameOver: true
              },
              channel: this.props.channel
            });				
          },
          style: 'cancel'
        },
        {
          text: 'Yea', 
          onPress: () => {
            this.props.pubnub.publish({
              message: {
                reset: true
              },
              channel: this.props.channel
            });
          }  
        },
      ],
      { cancelable: false } 
    );
  }
}
game over alert room creator

For the opponent, an alert is shown with a message telling them to wait for a new round, which will be decided by the room creator.

game over opponent

If the room creator decides to end the game, then the message gameOver is published to the channel. Otherwise, the message restart is published to the channel. These messages are handled inside of getMessage  in componentDidMount.

componentDidMount() {
  // Listen for messages in the channel
  this.props.pubnub.getMessage(this.props.channel, (msg) => {
    ...

    if(msg.message.reset){
      this.setState({
        moves: _.range(9).fill('')
      });
      this.turn = 'X';
      this.game_over = false;
    }

    if(msg.message.gameOver){
      this.props.pubnub.unsubscribe({
        channels : [this.props.channel]
      });
      this.props.endGame();
    }
  }); 
}

If the room creator wants to play another round, then the table is reset. This occurs by setting the array moves to its original state: turn is set to X and game_over to false.

However, if the room creator decides to end the game, then both players unsubscribe from the current channel and a call to the method endGame is made. This method will end the game and take the players back to the lobby since is_playing is reset to false. The method endGame is a prop from App.js, so you need to go back to App.js to implement this functionality.

// In App.js
endGame = () => {
  // Reset the state values
  this.setState({
    username: '',
    rival_username: '',
    is_playing: false,
    is_waiting: false,
    isDisabled: false
  });

  // Subscribe to gameLobby again on a new game
  this.channel = null;
  this.pubnub.subscribe({
    channels: ['gameLobby'],
    withPresence: true
  });
}

The state values are reset back to the original state and channel to null since there will be a new room_id when the create button is pressed again. Finally, subscribe again to the gameLobby channel so the players can continue playing the game if they choose to do so.

The React Native multiplayer tic-tac-toe game is now ready to test and play!

Testing the App

Before running the app, you'll need to enable the Presence feature to detect the number of people in the channel. To turn it on, go to the PubNub Admin Dashboard and click on your application. Click on your Keyset and toggle the Presence function switch to enable it. A dialog will appear, asking you to confirm by typing in "ENABLE" in all caps. Do so and keep the default values the same.

enable presence

To run the game, make sure you are in the project folder and enter the following command in the terminal: react-native run-ios.

This command will open the simulator and run the game. You can also run the game on the Android emulator by replacing run-ios with run-android (Note: Make sure you open an emulator first before running the command). Once the simulator opens, the lobby UI will be displayed.

lobby

You can test the game by running the React app version of the tic-tac-toe game used to test the simulator applications. The test React app is already connected to the React Native app used in this tutorial and the necessary logic is taken care of. All you need to do is insert the same Pub/Sub keys from the tutorial's React Native app.

Playing the Game

You will use the simulator to create a channel and the React app to join that channel (Note: The React app is currently set up to only join channels and not create them). You can clone the test React app from this repo. Once you open the project, go to the file Game.js and in the constructor, add the Pub/Sub keys you used for the React Native app. That’s all you have to edit for the test React project.

replace keys

Run the npm install command in the terminal to install the dependencies. To run the app, enter the npm start command in the terminal.

The app will open at http://localhost:3000 with an empty table and two input fields.

empty board react app

Navigate to the simulator and in the lobby, type Player X for the username and press the create button to create a new room ID. Go back to the browser and for the username field, type Player O. In the room name field, type the room ID that was created in the simulator.

join room react app

Once both fields are filled in, press the Submit button and the game will start for both players. Since the simulator is the room creator, press any square on the table to place an X. You should see the in both the simulator and in the browser. Try pressing another square in the simulator’s table and you will notice that nothing happens as it’s the opponent’s turn to make a move. In the browser, press any square to place an O.

react app gameplay

Keep playing until someone wins or the game ends in a draw. If there is a winner, the winner will be announced in the browser and the winning square's background color will turn green. In the simulator, the score for the winning player will update and an alert will ask the room creator if they want to play another round or exit the game.

game over react app

If the room creator decides to play another round, both the simulator and the test React app tables will reset for the new round. If the room creator decides to exit the game, the room creator will be taken to the lobby while the test React app browser resets the table. You can create a new channel in the simulator and in the React test app browser you can join that new channel.

What’s Next

You've successfully created a React Native multiplayer tic-tac-toe game using PubNub for real-time updates and user online detection. In Part one of the tutorial, you set up your application environment, created the lobby for players to join, and displayed dialogs for players to join other lobbies. In Part two of the tutorial, you completed the logic of players actually playing the game, winning conditions, and how players can choose to play once more or leave the lobby.

You can check out the completed version of the application on GitHub, as well as the React version of the application used to test the game created in this tutorial. If you would like to learn more about how to power your React applications with PubNub, be sure to check out PubNub's other React developer resources:

If you have any other questions or concerns, please feel free to reach out to devrel@pubnub.com.

More from PubNub

How to Create a Dating App: 7 Steps to Fit Any Design
Insights6 minMar 15, 2023

How to Create a Dating App: 7 Steps to Fit Any Design

There are common underlying technologies for a dating app, and in this post, we’ll talk about the major technologies and designs...

Michael Carroll

Michael Carroll

How to Create a Real-time Public Transportation Schedule App
Build6 minMar 14, 2023

How to Create a Real-time Public Transportation Schedule App

How to use geohashing, JavaScript, Google Maps API, and BART API to build a real-time public transit schedule app.

Michael Carroll

Michael Carroll

How to Create Real-Time Vehicle Location Tracking App
Build2 minMar 9, 2023

How to Create Real-Time Vehicle Location Tracking App

How to track and stream real-time vehicle location on a live-updating map using EON, JavaScript, and the Mapbox API.

Michael Carroll

Michael Carroll

Talk to an expert