Welcome to the 3rd installment of the “Creating a Minesweeper Game in SolidJS” post series. In the first part, we constructed the game board using a flat array and mathematical operations. In the second part, we concentrated on the recursive iteration process required to implement the "zero-opening" functionality.
In this post, we aim to incorporate gamification elements into the game to make it more engaging and enjoyable for users.
We're going to spice things up by adding a mine counter to keep track of those pesky bombs and help you keep count of the ones you've marked. Plus, we'll introduce a game-ending logic and a handy new Timer component that will keep track of how long you've been playing until the bombs start detonating or you've marked all the mines.
We have plenty to do, so let’s get on to it!
Hey! for more content like the one you're about to read check out @mattibarzeev on Twitter 🍻
The code can be found in this GitHub repository:
https://github.com/mbarzeev/solid-minesweeper
The Mines Counter
The Minesweeper game has a game panel, which holds, among other things, the mines counter.
The counter starts from the initial number of total mines on the board, and each time the player marks a tile, the mine count is decreased accordingly, even if the mark is not actually on a Tile which has a mine in it.
We first render the game panel in the header:
<header class={styles.header}>
<div class={styles.gamePanel}></div>
<div class={styles.board}>
<For each={tilesArray()}>
{(tileData: TileData) => (
<Tile data={tileData} onTileContextMenu={toggleTileMark} onTileClicked={onTileClicked} />
)}
</For>
</div>
</header>
In order to show the remaining mines count we need to create a solid’s signal so it can be reactive:
const TOTAL_MINES = 50;
const [remainingMines, setRemainingMines] = createSignal<number>(TOTAL_MINES);
And the rendering will be:
<div class={styles.gamePanel}>
<span>{remainingMines()}</span>
</div>
Now, in order to set the number of total remaining mines correctly we need to update it each time the player toggle the marking and I would like to do that as a side effect of when the tiles array is being modified.
In order to achieve that we need to “listen” for changes in the tilesArray
and inspect the marked tiles in order to calculate the remaining mines. It may appear not optimized but it will make our game work in a deterministic manner with a game state - by that I mean that if we give the game a state it will render correctly.
For that we shall use Solid’s “createEffect()”:
createEffect(() => {
const markedTiles = tilesArray().filter((tile) => tile.isMarked);
setRemainingMines(TOTAL_MINES - markedTiles.length);
});
This effect will trigger each time the tilesArray signal is changed, filter out the tiles which are marked, and deduct them from the TOTAL_MINES to get the remaining mines number.
Detonating a Mine
When the player accidentally opens a tile which has a mine, the game is over.
Here is the code for the tile clicked handler:
const onTileClicked = (index: number) => {
const tile: TileData = tilesArray()[index];
// If the tile is marked, un-mark it
tile.isMarked = false;
// If the tile has a mine, it's game over
if (tile.value === MINE_VALUE) {
gameOver();
} else {
let indices = [index];
const tileValue = tile.value;
if (tileValue === 0) {
// get the indices that need to be opened...
indices = getTotalZeroTilesIndices(index);
}
openTiles(indices);
}
};
You can see that when we detect that the value is a “MINE_VALUE” we call the gameOver()
function.
What we want to do in the “gameOver” function is to detonate the clicked mine and then the rest of the mines on the board.
We start by adding another property to the TileData, which is isDetonated:boolean;
:
export type TileData = {
index: number;
value: TileValue;
isOpen: boolean;
isMarked: boolean;
isDetonated: boolean;
};
This will help us indicate that the tile is detonated. We obviously initiate it to “false”:
// Convert the boardArray to TilesArray
const [tilesArray, setTilesArray] = createSignal<TileData[]>(
boardArray.map((item, index) => ({
index,
value: getTileValue(index),
isOpen: false,
isMarked: false,
isDetonated: false,
}))
);
We also add a css class in case the Tile is detonated, in the Tile component:
classList={{[styles.exposed]: data.isOpen || data.isMarked, [styles.detonated]: data.isDetonated}}
.value.detonated {
display: block;
background-color: red;
}
Now, when the player detonates a mine, we mark all the tiles with mines in them as “detonated” and we open them all, so when the game is over the entire game board is exposed:
const gameOver = () => {
setTilesArray((prevArray) => {
const newArray = prevArray.map((tile) => {
if (tile.value === MINE_VALUE) {
return {...tile, isDetonated: true, isOpen: true};
}
return {...tile, isOpen: true};
});
return newArray;
});
};
And here is the result so far:
Finding all the mines
It’s time to handle the situation where the player finds all the mines. In other words, it means that each Tile that has a mine in it is marked with a flag, and that the number of marked Tiles is equal to the total number of mines in the board.
In the createEffect
we previously made we add the following code that checks if the game is won:
createEffect(() => {
if (isGameOver) return;
const markedTiles = tilesArray().filter((tile) => tile.isMarked);
setRemainingMines(TOTAL_MINES - markedTiles.length);
// If the marked tiles are actually mines and they equal to the total number
// of mines in the board, the game is won
if (markedTiles.length === TOTAL_MINES) {
try {
markedTiles.forEach((tile) => {
if (tile.value !== MINE_VALUE) {
throw new Error();
}
});
gameWon();
} catch (error) {
// Do nothing, the game is not won
}
}
});
In short, when the number of marked tiles is equal to the number of total mines we check if they are all mines. If they are not, we break the loop. If they are, we call the “gameWon()” method.
Notice that we check the “isGameOver” before we do anything. This is to prevent an infinite cycle when we want to do something to the Tiles array when the game is over, like in the gameWon
function, where we open the entire Tiles.
Here is the code for the gameWon
function:
const gameWon = () => {
isGameOver = true;
setTilesArray((prevArray) => {
const newArray = prevArray.map((tile) => {
return {...tile, isOpen: true};
});
return newArray;
});
};
The Timer
It’s time to add the timer.
For now it will be “dumb” - it will start at the refresh of the page and will stop when the game is over for some reason.
We start with a Signal and an interval for the timer:
let timerInterval: number;
const [timerSeconds, setTimerSeconds] = createSignal(0);
const startTimer = () => {
timerInterval = setInterval(() => {
setTimerSeconds(timerSeconds() + 1);
}, 1000);
};
And we call the startTimer()
on the main App file.
For the Timer we build a dedicated component which displays the time in a nice format. Notice that the “seconds” prop that is passed to the component is not a number but is actually a Solid’s Accessor type:
import {Accessor} from 'solid-js';
const Timer = ({seconds}: {seconds: Accessor<number>}) => {
return <div>{getDisplayTimeBySeconds(seconds())}</div>;
};
const getDisplayTimeBySeconds = (seconds: number) => {
const min = Math.floor(seconds / 60);
const sec = seconds % 60;
return `${getDisplayableTime(min)}:${getDisplayableTime(sec)}`;
};
function getDisplayableTime(timeValue: number): string {
return timeValue < 10 ? `0${timeValue}` : `${timeValue}`;
}
export default Timer;
We use this component like this in the main App file:
<div class={styles.scoring}>
<span>{remainingMines()}</span>
<Timer seconds={timerSeconds} />
</div>
And when the game is over for any reason, we clear the interval. It might be better to have a game state and have effects to it, but we will save that, and others, to the refactor phase.
Well, I think we made it and achieved our goals for this post!
So what do we have so far?
We have a mine counter, we have a timer, we have a way to indicate when the game is over or won, and even got a nice bomb emoji for the mines (couldn’t find a mine, sorry). Not too bad 🙂
Stay tuned for the next post where we will put some modals and attempt to complete and deploy the game!
The code can be found in this GitHub repository:
https://github.com/mbarzeev/solid-minesweeper
This article is one of a 4 parts post series:
- Creating a Minesweeper Game in SolidJS - The Board
- Creating a Minesweeper Game in SolidJS - The Zero-Opening
- Creating a Minesweeper Game in SolidJS - Score, Timer and Game State
- Creating a Minesweeper Game in SolidJS - Completing The Game
Hey! for more content like the one you've just read check out @mattibarzeev on Twitter 🍻
Top comments (0)