DEV Community

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

Posted on • Edited on

Creating a Minesweeper Game in SolidJS - The Zero-Opening

In the previous post I’ve created a Minesweeper game board using SolidJS and a flat array.
The tiles are all set, and we can mark or open them. Nice.
It is now time to put some of the game logic into place, and we start with what they call “zero-opening”

The “zero opening” is a situation where the player opens an empty tile (a tile with no value to it). In this case all the empty tiles which are adjacent to it are opened in a recursive manner, until they reach a tile which has a number to it.
SolidJS, TypeScript and some common logic under my belt, I’m ready to dive head first into 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

In order to be able to achieve the “zero-opening” we need to change the way we set a tile as “open”. Currently it is done on the Tile component, with an inner state, but this does not support what we are aiming for. We would like to keep the “open” as a state on the data of each tile, within the array of all tiles.

I start by enriching the tiles array. I want each tile to be of a TileData type, that will have the following properties:

export type TileValue = number | 'x';
export type TileData = {
   index: number;
   value: TileValue;
   isOpen: boolean;
   isMarked: boolean;
};
Enter fullscreen mode Exit fullscreen mode

Now we would like to convert the the boardArray to a tilesArray Solid’s signal, like this:

// Convert the boardArray to TilesArray
const [tilesArray, setTilesArray] = createSignal<TileData[]>(
   boardArray.map((item, index) => ({
       index,
       value: getTileValue(index),
       isOpen: false,
       isMarked: false,
   }))
);


Enter fullscreen mode Exit fullscreen mode

And we pass this array further down to be rendered:

<For each={tilesArray()}>
                       {(tileData: TileData) => (
                           <Tile data={tileData} />
                       )}
                   </For>
Enter fullscreen mode Exit fullscreen mode

But now when we want to open or mark a tile we need to update the tiles state and set that tile as opened or as marked. Let’s start with marking a tile

Marking a Tile

First let's set the TileProps:

export type TileProps = {
   data: TileData;
   onTileContextMenu: (index: number) => void;
};
Enter fullscreen mode Exit fullscreen mode

And when the Tile is marked we do this:

const onTileContextClick = (event: MouseEvent) => {
       event.preventDefault();
       onTileContextMenu(data.index);
   };
Enter fullscreen mode Exit fullscreen mode

And in the App we also have a function which marks the Tile by changing its “isMarked” boolean property to true:

const toggleTileMark = (index: number) => {
   setTilesArray((prevArray) => {
       const newArray = [...prevArray];
       newArray[index] = {...newArray[index], isMarked: !newArray[index].isMarked};
       return newArray;
   });
};


Enter fullscreen mode Exit fullscreen mode

We created a new clone of the tilesArray, with a tile that is marked.
Let’s do that same for the “open” action

Opening a Tile

We add the handler for clicking a tile to the TileProps:

export type TileProps = {
   ...
   onTileClicked?: (index: number) => void;
};
Enter fullscreen mode Exit fullscreen mode

And on App the handler is quite the same as the isMarked handler, but now it sets the isOpen to true with no toggle:

const openTile = (index: number) => {
setTilesArray((prevArray) => {
const newArray = [...prevArray];
newArray[index] = {...newArray[index], isOpen: true};
return newArray;
});
};

This one will change shortly… it is time to deal with the “zero-opening”.

The “Zero-Opening”

When we click on an empty tile we would like to open all its adjacent empty tiles, and for each its adjacent empty tiles and so forth until it reaches a tile which has a number value in it.

For that I first modify the “openTiles” to support receiving an array of indices to open, instead of a single one:

const onTileClicked = (index: number) => {
   openTiles([index]);
};


const openTiles = (indices: number[]) => {
   setTilesArray((prevArray) => {
       const newArray = [...prevArray];
       indices.forEach((index) => {
           newArray[index] = {...newArray[index], isOpen: true};
       });


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

Now let's do something else when the tile clicked has a 0 value. In that case we would like to calculate all the indices that need to be opened and pass them all to the “openTiles()” method.

We first extend our onTileClicked() method to indicate a “zero” tile was clicked and act accordingly:

const onTileClicked = (index: number) => {
   let indices = [index];
   const tileValue = tilesArray()[index].value;
   if (tileValue === 0) {
       // get the indices that need to be opened...
       indices = getTotalZeroTilesIndices(index);
   }
   openTiles(indices);
};
Enter fullscreen mode Exit fullscreen mode

The getTotalZeroTilesIndices() is a little bit complex. If I try to break it down, my idea (which is brute forcing it, and I’m sure there better and more optimized ways of getting there) was inspecting each tile in a recursive manner to find if it has any adjacent zero tiles and add them to the total indices we will send to the openTiles() method in the end.

Here is the function - it has 2 nested functions to it and a total of 38 LoC. If you have any questions or suggestions about it, be sure to leave it in the comments section below, I’d love to hear about them 🙂

function getTotalZeroTilesIndices(index: number): number[] {
   const indices: number[] = [];


   function inspectZeroTile(index: number) {
       const tile = tilesArray()[index];
       if (tile && !indices.includes(index)) {
           if (tile.value === 0) {
               getAdjacentZeroTilesIndices(index);
           } else {
               indices.push(index);
           }
       }
   }


   function getAdjacentZeroTilesIndices(index: number) {
       indices.push(index);


       inspectZeroTile(index);


       const leftTileIndex = index - 1;
       const rightTileIndex = index + 1;
       const topTileIndex = index - ROW_LENGTH;
       const bottomTileIndex = index + ROW_LENGTH;
       const hasLeftTiles = index % ROW_LENGTH > 0;
       const hasRightTiles = (index + 1) % ROW_LENGTH !== 0;


       hasRightTiles && inspectZeroTile(rightTileIndex);


       hasLeftTiles && inspectZeroTile(leftTileIndex);


       inspectZeroTile(topTileIndex);


       inspectZeroTile(bottomTileIndex);
   }


   getAdjacentZeroTilesIndices(index);


   return indices;
}
Enter fullscreen mode Exit fullscreen mode

And to check out the result, here is a board where I guessed the zero tile and clicked it, and you can see that it opened all the adjacent tiles:

Image description

Yay! 🙂

So what do we have so far?

We have our Minesweeper game board, fully generated with the option to open or mark tiles. When we open a “zero” tile, which is a tile that does not have any value to it, we trigger a multi-opening of other adjacent zero tiles - this is the “zero-opening”.

Stay tuned for the next post where we will put some scoring logic to 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)