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;
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);
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;
});
};
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;
});
};
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;
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;
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>
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 />}
And now we have this when the game is over:
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:
<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;
When the game ends for some reason, we render the GameOver Modal like so:
{gameState() !== GAME_STATUS.ONGOING && (
<GameOverModal onPlayAgain={restartGame} onNewGame={startNewGame} />
)}
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:
(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);
};
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);
};
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>
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:
- 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)