Creating a Minesweeper Game in SolidJS - Completing The Game

Matti Bar-Zeev - Apr 11 '23 - - Dev Community

Welcome to the 4th and final installment of the “Creating a Minesweeper Game with 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 the third part we add the score, timers and the initial game state.

In this post, our focus will be on developing a GameOverModal component that will appear over a Backdrop in the event of a game over scenario. Our objective is to provide players with the option to either start a new game or restart the one they have just finished playing. Finally, we will deploy the finished product using Netlify.
Let's get started.


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

Game Over

There are 2 ways in which the game can be over - one is when the players detonate a mine by mistake and in this case they lose. The other is when the players find all the mines, which in this case - glory.

In our game app we currently have a single “state” which indicates whether the game is over or not:

let isGameOver = false;

Enter fullscreen mode Exit fullscreen mode

This boolean value is not enough to support the functionality we want. We need something that can indicate the game state and also why it ended - the player won or lost.
For that I will convert this boolean into a string that holds the state for the game-over. It can be either “won”, “lost” or “ongoing”. Let’s put this in an enum and set the game state signal:

export enum GAME_STATUS {
   WON = 'won',
   LOST = 'lost',
   ONGOING = 'ongoing',
}


export const [gameState, setGameState] = createSignal<GAME_STATUS>(GAME_STATUS.ONGOING);
Enter fullscreen mode Exit fullscreen mode

The game state initializes with “ongoing” value, but when the game is won we wish to change that to “won”.
I’m creating a gameWon() function that sets this state and exposes all the tiles:

const gameWon = () => {
   setGameState(GAME_STATUS.WON);
   clearInterval(timerInterval);
   setTilesArray((prevArray) => {
       const newArray = prevArray.map((tile) => {
           return {...tile, isOpen: true};
       });


       return newArray;
   });
};
Enter fullscreen mode Exit fullscreen mode

I will create a gameOver() function for when the game is lost, and in it I will set the game state to “lost”. In addition to that I will expose all the tiles which hold mines in them by setting the tiles data’s isDetonated to true:

const gameOver = () => {
   setGameState(GAME_STATUS.LOST);
   clearInterval(timerInterval);
   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

Notice that in both methods I’m stopping the time by clearing the timerInterval interval.

It is time to see how the game will render according to the game state. I would like to have a modal popup showing when the game is over - let’s do that!

The Modal and Backdrop

We would like to popup a Modal when the game is over. For that we’re going to use Portals.
The first step is to create a Modal component which uses a Portal. This component appends the Modal unto an existing element with “modal” id to it. We will create this element later on.
As you can see, we can append any content to the Modal component, so this component is agnostic to the content it has:

import {ComponentProps} from 'solid-js';
import {Portal} from 'solid-js/web';


interface ModalProps extends ComponentProps<'div'> {}


const Modal = ({children}: ModalProps) => {
   return <Portal mount={document.getElementById('modal') as Node}>{children}</Portal>;
};


export default Modal;
Enter fullscreen mode Exit fullscreen mode

I’m also creating a “Backdrop” component to block any interaction behind the opened modal. This component will also blur out the background, focusing the player on the Modal. The Backdrop is also a Portal which appends itself unto and existing element with “backdrop” id to to:

import {ComponentProps} from 'solid-js';
import {Portal} from 'solid-js/web';
import styles from './Backdrop.module.css';


interface BackdropProp extends ComponentProps<'div'> {}


const Backdrop = (props: BackdropProp) => {
   return (
       <Portal mount={document.getElementById('backdrop') as Node}>
           <div class={styles.backdrop} />
       </Portal>
   );
};


export default Backdrop;
Enter fullscreen mode Exit fullscreen mode

Next, in the main HTML file, I’m adding a div for the “modal” and a div for the “backdrop”:

<body>
       . . . 
 <div id="root"></div>
       <div id="backdrop"></div>
       <div id="modal"></div>
     . . .
</body>
Enter fullscreen mode Exit fullscreen mode

Now, when the gameState is not “ongoing”, let’s bring the Backdrop up. We do that in the main App:

{gameState() !== GAME_STATUS.ONGOING && <Backdrop />}
Enter fullscreen mode Exit fullscreen mode

And now we have this when the game is over:

Image description

Not bad… it’s time to create a GameOverModal

The GameOverModal

For the time being I think that we can settle with a single GameOverModal component which supports both “won” and “lost” states, but obviously if you feel that you need something more complex you can divide it into 2 different components with some common “ground”.

The GameOverModal component is simple - it displays a title according to the game state, displays the elapsed time since the game has started and offers 2 actions, restarting the game or starting a new game.
Restarting the game is playing the same mines map, while a new game generates a new mines map.

Here is the code for the GameOverModal. Notice that it uses the Modal component and injects its content into it, and I’m using the same Timer component talked about in previous posts. I’m also importing the gameState and the timerSeconds so I can render the content accordingly, but if you can also pass these as props if you feel the need:

import {ComponentProps} from 'solid-js';
import {gameState, GAME_STATUS, timerSeconds} from '../App';
import Modal from '../Modal/Modal';
import Timer from '../Timer';
import styles from './GameOverModal.module.css';


interface GameOverModalProps extends ComponentProps<'div'> {
   onPlayAgain?: () => void;
   onNewGame?: () => void;
}


const GameOverModal = ({onPlayAgain, onNewGame}: GameOverModalProps) => {
   return (
       <Modal>
           <div class={styles.GameOverModal}>
               <h1>{gameState() === GAME_STATUS.WON ? `You've made it!` : 'Ka-Boom! :('}</h1>
               <div class={styles.time}>
                   Elapsed time:&nbsp;
                   <Timer seconds={timerSeconds} />
               </div>


               <div class={styles.actionPanel}>
                   <button class={styles.actionBtn} onclick={onPlayAgain}>
                       Play again
                   </button>
                   <button class={styles.actionBtn} onclick={onNewGame}>
                       New game
                   </button>
               </div>
           </div>
       </Modal>
   );
};


export default GameOverModal;
Enter fullscreen mode Exit fullscreen mode

When the game ends for some reason, we render the GameOver Modal like so:

{gameState() !== GAME_STATUS.ONGOING && (
               <GameOverModal onPlayAgain={restartGame} onNewGame={startNewGame} />
           )}
Enter fullscreen mode Exit fullscreen mode

Notice that we’re passing 2 callbacks to the GameOverModal which we will discuss soon.
Here is the result - you see the blurry backdrop and the modal saying that game was lost:

Image description

(what is that frowning emoji up there you might ask… I’ll tell you later ;)

It is time to deal with the actions offered.

Play again and New game actions

You might have noticed that our GameOverModal can accept 2 callbacks for onPlayAgain and onNewGame. Let’s see what they do -

The onPlayAgain refers to a restartGame function which looks like this:

const restartGame = () => {
   // Hide all the tiles
   setTilesArray((prevArray) => {
       const newArray = prevArray.map((tile) => {
           return {...tile, isOpen: false, isDetonated: false, isMarked: false};
       });


       return newArray;
   });
   startTimer();
   setGameState(GAME_STATUS.ONGOING);
};
Enter fullscreen mode Exit fullscreen mode

What this does is to reset the tiles, just by hiding them all again and removing any marks, and then resetting the timer and the game state. That’s it.

Let’s have a look at the onNewGame which refers to the startNewGame function. Here is its code:

const startNewGame = () => {
   let count = 0;
   const boardArray = [...Array(TOTAL_TILES)].fill(0);
   while (count < TOTAL_MINES) {
       const randomCellIndex = Math.floor(Math.random() * TOTAL_TILES);
       if (boardArray[randomCellIndex] !== 1) {
           boardArray[randomCellIndex] = 1;
           count++;
       }
   }


   setTilesArray(
       boardArray.map((item, index, array) => ({
           index,
           value: getTileValue(index, array),
           isOpen: false,
           isMarked: false,
           isDetonated: false,
       }))
   );
   startTimer();
   setGameState(GAME_STATUS.ONGOING);
};
Enter fullscreen mode Exit fullscreen mode

This function creates a new board array and from it, it generates the tiles array which we render. In other words, this function creates a new board.
Now that we have this function, we can reuse it when the game first starts as well.

When we have these 2 function, we can add that “smiley/frowning” emoji at the top that can start a new game each time it is pressed:

<button class={styles.resetBtn} onclick={startNewGame}>
                       {gameState() === GAME_STATUS.LOST ? '🙁' : '🙂'}
                   </button>
Enter fullscreen mode Exit fullscreen mode

And… Yes! I think we can say that our game is pretty much done.
Of course there’s a lot more that can be done with it, but the general flow and logic are in place. Be sure to check the code on GitHub for parts I did not mention here (like CSS etc.)
The code can be found in this GitHub repository:
https://github.com/mbarzeev/solid-minesweeper

Let’s deploy it ;)

Deploying the Game

I’m using Netlify in order to publish the game. This is rather simple - after you create a n account, you define the GitHub repo (in my case) you’d like Netlify to "listen to" and build upon change, define the output directory in which the result artifacts can be found and Boom! - you have it.

Here it is: https://solid-minesweeper.netlify.app/

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 🍻

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .