DEV Community

Cover image for Creating a Minesweeper Game in SolidJS - Score, Timer and Game State
Matti Bar-Zeev
Matti Bar-Zeev

Posted on • Updated on

Creating a Minesweeper Game in SolidJS - Score, Timer and Game State

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

And the rendering will be:

<div class={styles.gamePanel}>
                   <span>{remainingMines()}</span>
               </div>
Enter fullscreen mode Exit fullscreen mode

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);


 });
Enter fullscreen mode Exit fullscreen mode

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);
   }
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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,
   }))
);
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode
.value.detonated {
   display: block;
   background-color: red;
}
Enter fullscreen mode Exit fullscreen mode

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;
   });
};
Enter fullscreen mode Exit fullscreen mode

And here is the result so far:

Image description

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
       }
   }
});
Enter fullscreen mode Exit fullscreen mode

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;
   });
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

We use this component like this in the main App file:

<div class={styles.scoring}>
                   <span>{remainingMines()}</span>
                   <Timer seconds={timerSeconds} />
               </div>
Enter fullscreen mode Exit fullscreen mode

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?

Image description

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:


Hey! for more content like the one you've just read check out @mattibarzeev on Twitter 🍻

Top comments (0)