⭕Tic Tac Toe game built on mobile app using react native and redux
Welcome to Tic Tac Toe mobile application game, built with React Native.
This is a Tic Tac Toe mobile application game where 2 players can play at the same time on a 3x3 grid. The app is built in react native framework and uses redux for the state management of the game.
Victory | Game Tied |
---|---|
I have publised the application on Expo but unfortunately, Expo does not allow clients to try the app publicly anymore. Therefore, I have provided information below to run the app locally.
git clone https://github.com/mitulsavani/X_O.git
yarn install
yarn run ios
yarn run android
This project was bootstrapped with Expo CLI Quickstart .
Note: I could have initiated this project using React Native CLI for more flexibility on native side. However, I decided to use Expo CLI because I knew before hand that this application is a comparatively small demo application which would not require much third party libraries.
X_O
|__ App.js
|__ package.json
|__ README.md
|__ .eslintrc.json
|__ .gitignore
|__ assests
| |__ p1.png
| |__ p2.png
|__ src
|__ components
| |__ Board.js
| |__ Button.js
| |__ Cell.js
| |__ Player.js
|__ navigation
| |__ RootNavigator.js
|__ redux
| |__ actions.js
| |__ gameReducer.js
| |__ InitialState.js
| |__ root.reducer.js
| |__ store.js
|__ screens
| |__ GameScreen.js
|__ styles
|__ colors.js
|__ GameScreenStyles.js
|__ components
|__ BoardStyles.js
|__ ButtonStyles.js
|__ CellStyles.js
|__ PlayerStyles.js
The following components were built to complete the GameScreen:
Board
which renders the grid backgroundCell
is called by Board to render 9 squares which finishes our 3x3 gridButton
is called by Cell to render the value( X / O / null )
of that cellPlayer
to indicate players turnAll custom components have a set of props which I am validating using Prop-Types.
The game have one screen called as GameScreen
. I was intitally thinking to make multiple screens for this game, but I did not see a point in doing it because that would only increase player clicks to reach up to the main GameScreen
and the navigation was acquired using react navigation.
export const InitialState = () => ({
board: [
[null, null, null],
[null, null, null],
[null, null, null]
],
winner: null,
currentPlayer: 'X',
gameOver: false,
});
NEW_GAME
will be dispatched in 2 scenarios; when users open up an application which returns the Initial_State, and when player clicks on the resets game button
of the alert. This alert is rendered when game declares the draw/winner of the game.UPDATE_CELL
will be dispatched when player clicks on one of the 9 cells of the board. This updates the value of that cell.TOGGLE_PLAYER
will be dispatched to alter players turn.CHECK_GAME_OVER
will be dispatched to check whether player have won the game.export const NEW_GAME = 'NEW_GAME';
export const UPDATE_CELL = 'UPDATE_CELL';
export const TOGGLE_PLAYER = 'TOGGLE_PLAYER';
export const CHECK_GAME_OVER = 'CHECK_GAME_OVER';
The application has one reducer named as gameReducer.js
which is responsible to return the updated state.
From GameScreen.js
:
I am fetching the rowIndex
and colIndex
of the cell when player clicks on the cell from the component props and then dispatching UPDATE_CELL
action with the payload(rowIndex and colIndex)
.
From gameReducer.js
:
I am directly updating the value of that cell on the board with the currentPlayer
.
case UPDATE_CELL:
const { row, col } = action.payload;
state.board[row][col] = state.currentPlayer;
return {
...state,
}
From gameReducer.js
:
There are in total of 8 ways a player can win this game on 3x3 board. let's look at those patterns:
const winningPatterns = [
// Horizontals
[{ r: 0, c: 0 }, { r: 0, c: 1 }, { r: 0, c: 2 }],
[{ r: 1, c: 0 }, { r: 1, c: 1 }, { r: 1, c: 2 }],
[{ r: 2, c: 0 }, { r: 2, c: 1 }, { r: 2, c: 2 }],
// Verticals
[{ r: 0, c: 0 }, { r: 1, c: 0 }, { r: 2, c: 0 }],
[{ r: 0, c: 1 }, { r: 1, c: 1 }, { r: 2, c: 1 }],
[{ r: 0, c: 2 }, { r: 1, c: 2 }, { r: 2, c: 2 }],
// Diagonals
[{ r: 0, c: 0 }, { r: 1, c: 1 }, { r: 2, c: 2 }],
[{ r: 0, c: 2 }, { r: 1, c: 1 }, { r: 2, c: 0 }],
];
The function checks if the currentPlayer
value matches with all the three positions of a single combination of the winningPatterns.
const checkWinner = (board) => {
return winningPatterns.some(pattern => pattern.every(cell => {
const { r, c } = cell;
return board[r][c] === state.currentPlayer;
}));
}
The function checks if our current board is full or not.
const isBoardFull = (board) => {
const notFull = board.some(row => row.some(col => col === null));
return !notFull;
}
The function checks whether a player has won or if the game is draw? By the end of the game, if all cell values are filled up and no player value have matched with one of the winningPatters then the game will be considered as draw.
Note: I am aware that the function returns three different kind of values (string, null, booleans)
. I believe that this is considered as bad practice, therefore I would definitely optimize this later.
const checkGameOver = (board) => {
if(checkWinner(board)) {
return state.currentPlayer;
}
if(isBoardFull(board)) {
return null;
}
return false;
}
From gameReducer.js
:
I am doing 2 things here; Firstly, checking if winner is already declared, if so then there is no point in altering player because game is over! Ortherwise, I would alter the player.
case TOGGLE_PLAYER:
if(state.winner !== null)
return state;
const nextPlayer = state.currentPlayer === 'X' ? 'O' : 'X';
return {
...state,
currentPlayer: nextPlayer,
}
From GameScreen.js
:
I am rendering alert based on winner
and gameOver
entity from the state. If gameReducer.js have found out a winner then pop Victory alert, otherwise render Game-Tied alert.
displayGameStatusAlert = () => {
const { game } = this.props;
const { winner, gameOver } = game;
if(gameOver === true) {
if(winner !== null) {
return(
Alert.alert(
'Victory',
`${winner} - Wins the game`,
[{ text: 'Reset Game', onPress: () => this.resetGame()}],
{ cancelable: false }
)
)
} else {
return(
Alert.alert(
'Game Tied',
'Play Again',
[{ text: 'Reset Game', onPress: () => this.resetGame()}],
{ cancelable: false }
)
)
}
}
}
When player presses the alert button, I am calling a resetGame()
function which re-renders the game with the Initial_State.
resetGame() {
this.props.newGame();
}
I am thinking to implement a algorithm by which a Player can play the game with CPU bot. What this means from technical side is after players' input, I will
dispatch an action which will randomly select one null
postition from the board and assign a value( X / O) to it. I found an interesting article which might possibly help me achieve this feature.
As of now my app is dispatching three actions asynchronously in one function handleClick()
, this works completely fine, but this is also considered as a bad practice in my case. At some point of time I want to avoid this and dispatch the actions synchronously using middleware
.
handleClick(rowIndex, colIndex) {
this.props.updateCell(rowIndex, colIndex);
this.props.checkGameOver();
this.props.togglePlayer();
}
In case if you have any questions or feedback about this application, feel free to reach out to me @mitulsavani
created by Mitul Savani, updated on 01/14/2020