Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
March 31, 2023 10:25 am GMT

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 lets 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 solids 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 Solids 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 TilesArrayconst [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:

Image description

Finding all the mines

Its 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

Its 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 Solids 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?

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 (couldnt 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

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


Original Link: https://dev.to/mbarzeev/creating-a-minesweeper-game-in-solidjs-score-timer-and-game-state-56n0

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To