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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
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 😆