DEV Community

Cover image for Creating a Minesweeper Game in SolidJS - The Board
Matti Bar-Zeev
Matti Bar-Zeev

Posted on • Updated on

Creating a Minesweeper Game in SolidJS - The Board

It has been some time since I last wrote anything. I thought that sharing something enjoyable with you could serve as a means to ease back into it. Thus, I patiently awaited something to rekindle my inspiration and interest, which eventually came in the form of the old yet addictive game, Minesweeper :)

My daughter recently discovered Minesweeper on her PC, and to my surprise, it has become a fun bonding activity for us. I sit beside her, offering my two cents every now and then, and I'm amazed at how quickly she's picked up the logic behind the game. She's been playing it faster and faster, breaking records by the day.

As I sat there, I couldn't help but wonder about the code behind the game. Specifically, I found myself pondering over the layout of the game board. Is it a 2-dimensional array or a flat array? How are the numbers being computed? I mean, what kind of wizardry did it take to build such a thing?

That’s enough to light a fire under my arse and get me going :)

So, Wanna jump in on the ride of creating a Minesweeper game using SolidJS?
Let’s go!


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 Game Board

The board is made out of an array with 16 cells. This will allow us to create a 4x4 board. Currently it is hard-coded, later on we can do some random arrangements there:

const boardArray: number[] = [0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0];
Enter fullscreen mode Exit fullscreen mode

The content of the array is made of 0’s and 1’s, where 1 indicates a tile with a mine in it, and 0 is an empty tile.
To display it I’m using Solid’s <For> component:

const App: Component = () => {
   return (
       <div class={styles.App}>
           <header class={styles.header}>
               <For each={boardArray}>
                   {(item: number, index: () => number) => <div>{item}</div>}
               </For>
           </header>
       </div>
   );
};
Enter fullscreen mode Exit fullscreen mode

Which results in this:

Image description

Wait… it will get better.

Obviously we need a way to arrange these in a 4x4 grid, and for that I will use a … css grid :)
I will put my <For …> tag inside a div which has a board css class to it, and here is the definition of that class:

.board {
 --tile-dimension: 30px;
 --row-length: 4;


 display: grid;
 grid-template-columns: repeat(auto-fit, minmax(var(--tile-dimension), 1fr));
 max-width: calc(var(--row-length) * var(--tile-dimension));
}
Enter fullscreen mode Exit fullscreen mode

Now it looks like this:

Image description

Yep, more like it.
We know that those empty cells need to indicate the number of mines that are adjacent to them in all 8 directions. How do we do that?

Minesweeper Simple Algorithm

I’m going with a “brute-force” approach here, though it might be proven to be the most efficient manner of achieving it. I’m inspecting each tile for mines next to it, but since this array is flat we need to be smart about it. Let me put the code here first and then explain what goes on in the “getMinesCount()” function.
Here is the rendering, where we pass the index of the current cell to the getMinesCount function:

<div class={styles.board}>
                   <For each={boardArray} fallback={<div>Loading...</div>}>
                       {(item: number, index: () => number) => (
                           <div style={{width: '30px', height: '30px'}}>{getMinesCount(index())}</div>
                       )}
                   </For>
               </div>
Enter fullscreen mode Exit fullscreen mode

And here is the implementation of the getMinesCount function:

function getMinesCount(index: number) {
   const cell = boardArray[index];
   if (cell === 1) {
       return 'x';
   } else {
       let minesCount = 0;
       const hasLeftCells = index % ROW_LENGTH > 0;
       const hasRightCells = (index + 1) % ROW_LENGTH !== 0;
       const bottomCellIndex = index + ROW_LENGTH;
       const topCellIndex = index - ROW_LENGTH;


       if (boardArray[bottomCellIndex] === 1) {
           minesCount++;
       }


       if (boardArray[topCellIndex] === 1) {
           minesCount++;
       }


       if (hasLeftCells) {
           boardArray[index - 1] === 1 && minesCount++;
           boardArray[topCellIndex - 1] === 1 && minesCount++;
           boardArray[bottomCellIndex - 1] === 1 && minesCount++;
       }


       if (hasRightCells) {
           boardArray[index + 1] === 1 && minesCount++;
           boardArray[topCellIndex + 1] && minesCount++;
           hasRightCells && boardArray[bottomCellIndex + 1] && minesCount++;
       }


       return minesCount;
   }
}
Enter fullscreen mode Exit fullscreen mode

The logic is quite simple - we calculate the top and bottom cells, figure out if the cell has left or right neighbors and increment the minesCount accordingly.

It’s actually not that bad, given that we have O(n) complexity.

Here how it looks now:

Image description

The “X” indicate the mines and the numbers are indicating how many mines are adjacent to the cell. Time to create the Tile component.

You might have noticed that I’m using a constant named “ROW_LENGTH” in order to compute the board dimensions, and I also have this value in the CSS as a variable, and this kinda annoys me that changing the grid dimensions requires changing both the CSS and JS instead of just a single place.
For that I chose to define the CSS variable within the rendering code, like so:

<div class={styles.App} style={{'--row-length': ROW_LENGTH}}>
           <header class={styles.header}>
               <div class={styles.board}>
                   <For each={boardArray}>
                       {(item: number, index: () => number) => <Tile {...getTileData(index())} />}
                   </For>
               </div>
           </header>
       </div>
Enter fullscreen mode Exit fullscreen mode

In this way I only need to change the “ROW_LENGTH” constant and everything aligned perfectly.

The Tile Component

import {Component, createSignal} from 'solid-js';
import styles from './Tile.module.css';


export type TileData = {
   isMine: boolean;
   value?: number;
};


const Tile: Component<TileData> = (data: TileData) => {
   const [isOpen, setIsOpen] = createSignal(false);
   const [isMarked, setIsMarked] = createSignal(false);


   const onTileClicked = (event: MouseEvent) => {
       !isMarked() && setIsOpen(true);
   };


   const onTileContextClick = (event: MouseEvent) => {
       event.preventDefault();
       !isOpen() && setIsMarked(!isMarked());
   };


   const value = data.isMine ? 'X' : data.value;


   return (
       <div class={styles.Tile} onclick={onTileClicked} onContextMenu={onTileContextClick}>
           <div class={styles.value} classList={{[styles.exposed]: isOpen() || isMarked()}}>
               {isMarked() ? '🚩' : value !== 0 ? value : ''}
           </div>
       </div>
   );
};


export default Tile;
Enter fullscreen mode Exit fullscreen mode

As you can see, right-clicking marks the tile, while a regular click opens it. You can toggle the marking but once opened it is done.

And we use it like this in the main App:

<For each={boardArray} fallback={<div>Loading...</div>}>
                       {(item: number, index: () => number) => <Tile {...getTileData(index())} />}
                   </For>
Enter fullscreen mode Exit fullscreen mode

And what we got is a board with “blank” tiles, and when we click on them they open - those with the numbers show the numbers, those with the mines show an “X” and the marked ones are flagged.

Image description

Random Board

Time to put some randomness into the pot. I would like to generate a flat array of 20x20 (400 cells) which has 40 mines scattered in it. Here is the code for it:

const totalMines = 40;
const ROW_LENGTH = 20;
const TOTAL_TILES = Math.pow(ROW_LENGTH, 2);


const boardArray = [...Array(TOTAL_TILES)].fill(0);


let count = 0;
while (count < totalMines) {
   const randomCellIndex = Math.floor(Math.random() * TOTAL_TILES);
   if (boardArray[randomCellIndex] !== 1) {
       boardArray[randomCellIndex] = 1;
       count++;
   }
}
Enter fullscreen mode Exit fullscreen mode

Here it how it looks without hiding the tiles value for the sake of demonstration:

Image description

That’s it for now.

We have a game board that allows us to customize its dimensions and the number of mines we want to include. Currently, we have the ability to expose or mark individual tiles.

The next step is to figure out the logic behind the "auto-expose" feature where multiple tiles are uncovered with a single click. We need to determine the appropriate logic to open adjacent tiles when a single tile is clicked. We also need to address the remaining mechanics of the game to ensure everything runs smoothly.

The code can be found in this GitHub repository:
https://github.com/mbarzeev/solid-minesweeper

That was fun :) stay tuned for more…

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 🍻

Latest comments (2)

Collapse
 
aquaductape profile image
Caleb Taylor

I didn't know this was a three part series, you should include the other two links in this post.

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

Done :)
Thanks!