Today, while scrolling through Pinterest, I came across an adorable tic-tac-toe board (a Chocolate board) that I instantly wanted to create. I thought, why not build it using CSS and turn it into a game as well?
First, I created a React app using the create-react-app command and removed all the default code from App.js.
Create a Game Board Component
I started with a game board designed to look like chocolate, with Marshmallow and Strawberry as the players. (I know, it sounds delicious! 😋).
Gameboard
The game board displays a grid of Cell
components, each representing a cell in the tic-tac-toe game. The Cell
components are responsible for rendering individual cells and handling user interactions.
Here is an overview of the code below.
Message and Restart Button
- Shows the current game message or the winning message if there is one.
- Includes an image of a restart button. Clicking this image triggers
handleRestartGame
resetting the game.
Functions
1. handleRestartGame
Resets the game by clearing the cells
array, setting the turn back to "marshmallow", and clearing the winning message.
2. checkScore
Check the current state of the cells
to see if there’s a winning combination based on predefined winning combos (rows, columns, diagonals).
Updates winningMsg
if a player has won or if the game is a draw.
3. makeRain
Positions the raindrop randomly across the screen and sets an animation duration.
Sets the content of the raindrop based on the winningMsg
. For example, if "Strawberry Wins!" is displayed, the raindrop will show a strawberry emoji.
Adds the raindrop to the document body and remove it after 5 seconds.
4. startRain
Starts a rain animation by setting up an interval that repeatedly calls makeRain
every 300 milliseconds.
Stops the animation after 10 seconds by clearing the interval.
State Variables
cells
An array with 9 empty strings representing the tic-tac-toe grid. Each element corresponds to a cell in the grid.
go
A string that indicates whose turn it is (“marshmallow” or “strawberry”).
winningMsg
A string that holds the message when there’s a winner or if the game is a draw (“Marshmallow Wins!”, “Strawberry Wins!”, or “It’s a Draw!”).
rainIntervalId
Stores the ID of the interval used for creating the rain animation. This allows you to clear the interval when needed.
import React, { useEffect, useState } from "react";
import restart from "./asset/refresh.png";
import Cell from "./components/Cell";
const App = () => {
const [cells, setCells] = useState(["", "", "", "", "", "", "", "", ""]);
const [go, setGo] = useState("marshmallow");
const [winningMsg, setWinningMsg] = useState(null);
const [rainIntervalId, setRainIntervalId] = useState(null);
const message = "it is now " + go + "'s go.";
useEffect(() => {
checkScore();
}, [cells]);
useEffect(() => {
if(winningMsg) {
startRain();
}
}, [winningMsg])
const handleRestartGame = () => {
setCells(["", "", "", "", "", "", "", "", ""]);
setGo("marshmallow");
setWinningMsg(null);
if (rainIntervalId) {
clearInterval(rainIntervalId);
setRainIntervalId(null);
}
};
const checkScore = () => {
const winningCombos = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
let winnerFound = false;
winningCombos.forEach((array) => {
let marshmallowWins = array.every(
(cell) => cells[cell] === "marshmallow"
);
if (marshmallowWins) {
setWinningMsg("Marshmallow Wins!");
winnerFound = true;
return;
}
});
winningCombos.forEach((array) => {
let strawberrywWins = array.every((cell) => cells[cell] === "strawberry");
if (strawberrywWins) {
setWinningMsg("Strawberry Wins!");
winnerFound = true;
return;
}
});
if (!winnerFound && cells.every((cell) => cell !== "")) {
setWinningMsg("It's a Draw!");
}
};
const makeRain = () => {
const rain = document.createElement("div");
rain.classList.add("makeRain");
rain.style.left = Math.random() * 100 + "vw";
rain.style.animationDuration = Math.random() * 2 + 3 + "s";
if (winningMsg === "Strawberry Wins!") {
rain.innerText = '🍓'
} else if (winningMsg === "Marshmallow Wins!") {
rain.innerText = "🍡"
} else {
rain.innerText = "😶"; // Default or draw condition
}
document.body.appendChild(rain);
setTimeout(() => {
rain.remove();
}, 5000);
};
const startRain = () => {
const intervalId = setInterval(makeRain, 300);
setRainIntervalId(intervalId);
setTimeout(() => {
clearInterval(intervalId);
}, 10000);
};
return (
<div className="app">
<div className="gameboard">
{cells.map((cell, index) => (
<Cell
key={index}
id={index}
cell={cell}
go={go}
setGo={setGo}
cells={cells}
setCells={setCells}
winningMsg={winningMsg}
/>
))}
</div>
<p className="message">
{winningMsg || message}
<img
src={restart}
className="restartIcon"
alt="restart"
onClick={handleRestartGame}
/>
</p>
</div>
);
};
export default App;
Create a Cell Component
The Cell
component represents an individual cell in the tic-tac-toe grid. It handles user interactions, such as clicks, to update the cell's state and manage player turns.
It updates the cells
array with the current player's symbol and switches turns between "marshmallow" and "strawberry".
import React from "react";
const Cell = ({ cell, id, go, setGo, cells, setCells, winningMsg }) => {
const handleClick = (e) => {
if (!winningMsg) {
const firstChild = e.target.firstChild;
const taken =
firstChild?.classList.contains("marshmallow") ||
firstChild?.classList.contains("strawberry");
if (!taken) {
if (go === "marshmallow") {
firstChild.classList.add("marshmallow");
handleCellChange("marshmallow");
setGo("strawberry");
} else if (go === "strawberry") {
firstChild.classList.add("strawberry");
handleCellChange("strawberry");
setGo("marshmallow");
}
}
}
};
const handleCellChange = (className) => {
const nextCells = cells.map((cell, index) => {
if (index === id) {
return className;
} else {
return cell;
}
});
setCells(nextCells);
};
return (
<div className="square" id={id} onClick={handleClick}>
<div className={cell}></div>
</div>
);
};
export default Cell;
Create Styles
I give the below styles to display the tic-tac-toe app.
.app {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.gameboard {
width: 300px;
height: 300px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
border: 1px solid black;
background-color: rgb(92, 51, 22);
border-radius: 3px;
}
.square {
width: 100px;
height: 100px;
border: 3px solid rgb(37, 16, 1);
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 3px;
box-shadow: 3px 3px 0px 0px wheat inset;
}
.message {
font-family: 'Gill Sans';
display: flex;
align-items: center;
justify-content: space-between;
}
.marshmallow {
height: 70px;
width: 70px;
background-color: inherit;
border: 20px;
background-image: url("./asset/marshmallow.png");
background-size: cover;
}
.strawberry {
height: 70px;
width: 70px;
background-color: inherit;
border: 20px;
background-image: url("./asset/strawberry.png");
background-size: cover;
}
.restartIcon {
height: 20px;
width: 20px;
cursor: pointer;
margin: 10px;
}
.makeRain {
position: fixed;
top: -1vh;
font-size: 2rem;
transform: translateY(0);
animation: fall 2s linear forwards;
}
@keyframes fall {
to {
transform: translateY(105vh);
}
}
Screenshots of Game
Use the links below to view the code and demo of the app.
Thank you for reading! Feel free to connect with me on LinkedIn or GitHub.