TicTacToe Design with React Native

Greetings to all TicTacToe fans! Some time ago I designed classes for the game in this post using C++ and it was quite interesting. However, to play it, there is one more step to go. In this post I will show you the front-end design that you can play on your browser, on your Android device or even iOS since React Native enables all. I keep the same game logic and object oriented classes and finish it with the user interface 🎮

Creating the Development Environment

This is the simplest part, thanks to the nature of React Native. Follow a few steps here.

When you are done, you can use the expo-cli to launch the game on your device. You can open it in a web browser by simply clicking on the corresponding link or use Expo Client on your smart device.

The Game in JavaScript

Game Logic

Here is my javascript port of the game.

export const GameState = { NONE: 0, WON: 1, LOST: -1, INVALID: -2, DRAW: 2 };
Object.freeze(GameState);

export class Strategy {
    FindMove(game, player) {}
}

export class RandomPlay {
    FindMove(game, player) {
        var N = game.GetBoardSize();
        while (game.GetMovesLeft()) {
            var r = Math.floor(Math.random() * N);
            var c = Math.floor(Math.random() * N);
            var dest = game.GetCell(r, c);
            if (dest != player && dest != -player) return [r, c];
        }
        return [-1, -1];
    }
}
Object.setPrototypeOf(RandomPlay.prototype, Strategy);

export class TicTacToe {
    strategy;
    N;
    board;
    rows;
    cols;
    diag;
    movesLeft;
    gameState;
 
    constructor(strategy, boardSize = 3) {
        this.strategy = strategy;
        this.N = boardSize;
        this.board = []
        for(var r = 0; r < this.N; ++r) this.board[r] = Array(this.N).fill(0);
        this.rows = Array(this.N).fill(0);
        this.cols = Array(this.N).fill(0);
        this.diag = Array(2).fill(0);
        this.movesLeft = this.N * this.N;
        this.gameState = GameState.NONE;
    }
 
    GetCell(r, c) {
        if (!this.validateMove(r, c)) return 0;
        return this.board[r][c];
    }
 
    GetBoardSize() {
        return this.N;
    }
 
    GetGameState() {
        return this.gameState;
    }
 
    GetMovesLeft() {
        return this.movesLeft;
    }
 
    Play(r, c) {
        if (this.GetGameState() != GameState.NONE) return this.GetGameState();
 
        var selfResult = this.playHelper(r, c, 1);
        if (selfResult == GameState.WON) return this.gameState = GameState.WON;
        if (selfResult == GameState.INVALID) return GameState.INVALID;
 
        if (this.GetMovesLeft()) {
            var [r, c] = this.strategy.FindMove(this, -1);
            var otherResult = this.playHelper(r, c, -1);
            if (otherResult == GameState.WON) return this.gameState = GameState.LOST;
        }
 
        if (this.GetMovesLeft()) return this.gameState = GameState.NONE;
        else return this.gameState = GameState.DRAW;
    }
 
    playHelper(r, c, player) {
        if (this.gameState != GameState.NONE) return GameState.INVALID;
        if (!this.validateMove(r, c)) return GameState.INVALID;
        if (!this.validatePlayer(player)) return GameState.INVALID;
        if (this.board[r][c] != 0) return GameState.INVALID;
        if (!this.movesLeft) return GameState.INVALID;
        
        --this.movesLeft;
        this.board[r][c] = player;
 
        this.rows[r] += player;
        this.cols[c] += player;
        if (r == c) this.diag[0] += player;
        if (r + c + 1 == this.N) this.diag[1] += player;
 
        if (this.rows[r] == this.N * player) return GameState.WON;
        if (this.cols[c] == this.N * player) return GameState.WON;
        if (this.diag[0] == this.N * player) return GameState.WON;
        if (this.diag[1] == this.N * player) return GameState.WON;
 
        return GameState.NONE;
    }
 
    validateMove(r, c) {
        if (r < 0 || r >= this.N) return false;
        if (c < 0 || c >= this.N) return false;
        return true;
    }
 
    validatePlayer(player) {
        return player * player == 1;
    }
}

Conversion is very straightforward, so please check the previous post for implementation details.

User Interface Design

Here is the full code and the details will follow.

import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
import { RandomPlay, GameState, TicTacToe } from './TicTacToe.js'
import React, { Component, useState, useEffect } from "react";

var game = new TicTacToe(new RandomPlay);

const GetCellSymbol = function(prop) {
  var num = game.GetCell(prop.row, prop.col);
  return num == 1 ? 'x' : num == '-1' ? 'o' : ' ';
};

const OnPlay = (prop) => {
  var result = game.Play(parseInt(prop.row), parseInt(prop.col));
  if(result == GameState.INVALID || result == GameState.NONE) return; 
  if(result == GameState.WON) alert('You won the game :)');
  if(result == GameState.LOST) alert('You lost the game :(');
  if(result == GameState.DRAW) alert('It is a draw!');
  game = new TicTacToe(new RandomPlay);
};

const Cell = (prop) => {
  const [state, setState] = useState(true);
  const refresh = useEffect(() => {
    const toggle = setInterval(() => {
      setState(!state);
    }, 100);

    return () => clearInterval(toggle);
  })

  return (
    <TouchableOpacity onPress={() => OnPlay(prop)}>
      <Text style={styles.cell}>{GetCellSymbol(prop)}</Text>
    </TouchableOpacity>
  )
};

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.title}>TicTacToe</Text>
        <View style={styles.row}>
          <Cell row='0' col='0'/>
          <Cell row='0' col='1'/>
          <Cell row='0' col='2'/>
        </View>
        <View style={styles.row}>
          <Cell row='1' col='0'/>
          <Cell row='1' col='1'/>
          <Cell row='1' col='2'/>
        </View>
        <View style={styles.row}>
          <Cell row='2' col='0'/>
          <Cell row='2' col='1'/>
          <Cell row='2' col='2'/>
        </View>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1, 
    alignItems:'center', 
    justifyContent:'center', 
    alignSelf: 'center'
  },
  title: {
    fontSize: 40, 
    padding: 40
  },
  row: {
    flexDirection: 'row'
  },
  cell:{
    width: 50, 
    height: 50,
    backgroundColor:'#fff',
    borderWidth: 2,
    borderColor: '#000',
    fontSize: 30,
    textAlign: 'center',
  },
});

Cell Object

I represented each slot with Cell  object (see line 21). It has one simple task of showing the current state of a slot. It is touchable, based on TouchableOpacity  component and invokes OnPlay method with its row and column numbers. Finally, a cell gets its value by pulling from the game  object. This is refreshed with 100ms intervals via useState and useEffect methods (see line 23).

OnPlay Method

This is where a move is executed on the TicTacToe class (see line 12). It shows an alert with respect to the game state returned from Play(…)  method.

Screen Layout

The game consists of 3-by-3 grid of Cell objects other than its title (see line 41). Each of 3 rows has 3 cells. Rest of the code is the styles only.

The Game on Action

On browser you can see the game similar to below. And after you play it for a while, you can see the game result on screen. After the game is over, it automatically starts again with an empty board.

Final Words

React Native provides a plenty of features on multiple platforms yet it is simple to start with. This short post is all you need to play it on your phone as I did many times before telling you 😆