Creating Minesweeper with ReactJS and Mobx
Tags: javascript, react, mobx
Published: 2023-02-25 13:41:30

Hello,

I want to share with you my implementation of Minesweeper using React and Mobx. As you may know, Minesweeper is a popular Microsoft game where players have to open all cells without mines.

Let's recall what is a Mobx. [Mobx](https://mobx.js.org/README.html) is a simple, scalable state management library. It helps easily rerender react elements.

First of all, we need to set up our react environment. Here we will use the [create-react-app](https://create-react-app.dev/docs/getting-started) package, this package helps us to set up our environment with little configurations. This package has a lot of pros and cons. I think, that the main advantage is that you don't need to install or configure tools like `webpack` or `Babel`. Also, you can use the already predefined template in your app. You can find a list of available templates by searching for [cra-template-*](https://www.npmjs.com/search?q=cra-template-*) on npm. And the main disadvantage is that there is a wide range of [issues](https://github.com/facebook/create-react-app/issues). Nevertheless, you can simply use `eject script`, and then configure `webpack` as you want.

So, let's create our environment using `yarn` with a typescript template:

```
yarn create react-app minesweeper --template typescript
```

Let's create our mobx root store `src/app/store.ts` file. Firstly we need to add the GameType enum

```typescript
export enum GameType {
  easy,
  medium,
  hard,
  custom,
}
```

Then, we will add game options. The easiest game will have 9x9 cells and 10 bombs, medium - 16x16 and 40 bombs, and hard - 16x30 and 99 bombs.

```typescript
type GameOptions = {
  rows: number;
  columns: number;
  bombs: number;
};

const gameOptions: Record<GameType, GameOptions> = {
  [GameType.easy]: {
    rows: 9,
    columns: 9,
    bombs: 10,
  },
  [GameType.medium]: {
    rows: 16,
    columns: 16,
    bombs: 40,
  },
  [GameType.hard]: {
    rows: 16,
    columns: 30,
    bombs: 99,
  },
  [GameType.custom]: {
    rows: 16,
    columns: 30,
    bombs: 99,
  },
};
```

For the custom game, we will use hard properties as initial values. Setup our root store

```typescript
class RootStore {
  cells: Cell[] = [];
  gameType: GameType | null = null;
  timer: Timer | null = null;
  initalPress = true;
  inProgress = false;
  winner: boolean | null = null;
  rows: number = 0;
  columns: number = 0;
  bombs: number = 0;

  constructor() {
    makeObservable(this, {
      cells: observable,
      gameType: observable,
      inProgress: observable,
      bombsLeft: computed,
      timer: observable,
      initalPress: observable,
      startGame: action,
      allCellsWithoutBombPressed: computed,
      winner: observable,
      rows: observable,
      columns: observable,
      bombs: observable,
      informWinner: action,
      informLooser: action,
    });

    reaction(
      () => this.allCellsWithoutBombPressed,
      (value) => {
        if (value) {
          this.informWinner();
        }
      }
    );
  }

  get allCellsWithoutBombPressed() {
    return (
      this.cells.length > 0 &&
      this.cells.filter((cell) => !cell.hasBomb).every((cell) => cell.pressed)
    );
  }

  get bombsLeft() {
    ...
  }

  drawGrid() {
    this.cells = new Array(this.rows * this.columns)
      .fill(null)
      .map(() => new Cell());
  }

  assignBombs(ignoreCell: Cell) {
    const totalCells = this.rows * this.columns;

    const set = new Set();

    while (set.size < this.bombs) {
      const index = Math.floor(Math.random() * totalCells);
      if (ignoreCell !== this.cells[index]) {
        set.add(index);
      }
    }

    let prevRow: Cell[] | null = null;
    let row = this.cells.slice(0, this.columns);

    for (let i = 0; i < this.rows; i++) {
      let nextRow: Cell[] | null = null;

      if (i < this.rows - 1) {
        nextRow = this.cells.slice(
          (i + 1) * this.columns,
          (i + 2) * this.columns
        );
      }
      for (let j = 0; j < this.columns; j++) {
        const key = i * this.columns + j;
        const cell = this.cells[key];
        cell.hasBomb = set.has(key);

        if (prevRow) {
          if (prevRow[j - 1]) cell.siblings.push(prevRow[j - 1]);
          cell.siblings.push(prevRow[j]);
          if (prevRow[j + 1]) cell.siblings.push(prevRow[j + 1]);
        }

        if (row[j - 1]) {
          cell.siblings.push(row[j - 1]);
        }

        if (row[j + 1]) {
          cell.siblings.push(row[j + 1]);
        }

        if (nextRow) {
          if (nextRow[j - 1]) cell.siblings.push(nextRow[j - 1]);
          cell.siblings.push(nextRow[j]);
          if (nextRow[j + 1]) cell.siblings.push(nextRow[j + 1]);
        }
      }
      prevRow = row;
      if (nextRow) {
        row = nextRow;
      }
    }
  }

  initializeGame(ingoreCell: Cell) {
    this.initalPress = false;
    this.timer?.start();
    this.assignBombs(ingoreCell);
  }

  startGame(type: GameType, options?: GameOptions) {
    this.inProgress = true;
    this.gameType = type;
    this.winner = null;
    this.initalPress = true;
    this.timer?.stop();
    this.timer = new Timer();

    const { rows, columns, bombs } = options ?? gameOptions[type];
    this.rows = rows;
    this.columns = columns;
    this.bombs = bombs;

    this.drawGrid();
  }

  informWinner() {
    ...
  }

  informLooser() {
    ...
  }
}

export const store = new RootStore();
```
We will use mobx `reaction` to check if all cells without mine are revealed. Reactions are used to make side effects in your model. As you can see getter `allCellsWithoutBombPressed` returns a bool value.

> `reaction` takes two functions: the data function is tracked and returns the data that is used as input for the second, effect function. It is important to note that the side effect only reacts to the data accessed in the data function, which might be less than the data used in the effect function.

So, now we can inform the player when he wins the game.

```javascript
reaction(
      () => this.allCellsWithoutBombPressed,
      (value) => {
        if (value) {
          this.informWinner();
        }
      }
);
```

Next, we need to implement the `Cell` class and add functionality to our cell.

Create `src/app/cell.ts`

```typescript
export class Cell {
  pressed = false;
  hasBomb = false;
  marked = false;
  neighbours: Cell[] = [];
  constructor() {
    makeObservable(this, {
      pressed: observable,
      hasBomb: observable,
      press: action,
      bombsAmount: computed,
      neighbours: observable,
      marked: observable,
    });
  }

  press() {
    this.pressed = true;
    this.marked = false;

    if (this.bombsAmount === 0 && !this.hasBomb) {
      this.revealEmptyNeighbours()
    }
  }

  toggleMark() {
    this.marked = !this.marked;
  }

  get bombsAmount() {
    return this.neighbours.filter((cell) => cell.hasBomb).length;
  }

  revealEmptyNeighbours() {
    this.neighbours.forEach((cell) => {
      if (cell.pressed || cell.hasBomb) {
        return;
      }

      cell.press();
    });
  }
}
```
Each cell has a `neighbours` array to store cells next to the current cell. 

We need to keep track of how many neighbours have mine. For this, we will use the `bombsAmount` getter. This number we will show when revealing the cell.

```javascript
get bombsAmount() {
    return this.neighbours.filter((cell) => cell.hasBomb).length;
}
```

When we press on a cell we set `pressed` to `true` then we need to reveal empty neighbours if this cell doesn't have neighbours with mines. `revealEmptyNeighbours` method will run `press` for each neighbour

```javascript
press() {
    this.pressed = true;
    this.marked = false;

    if (this.bombsAmount === 0 && !this.hasBomb) {
      this.revealEmptyNeighbours()
    }
}
```

And our game needs a timer.
Create `src/app/timer.ts`

```typescript
export class Timer {
  secondsPassed = 0;
  interval: NodeJS.Timer | null = null;
  startedAt: number = 0;

  constructor() {
    makeAutoObservable(this);
  }

  increaseTimer() {
    this.secondsPassed = (performance.now() - this.startedAt) / 1000;
  }

  start() {
    this.startedAt = performance.now();
    this.interval = setInterval(() => {
      this.increaseTimer();
    }, 1000);
  }

  stop() {
    if (this.interval) {
      clearInterval(this.interval);
    }
    this.increaseTimer();
    return this.secondsPassed;
  }
}
```

The timer is a simple class that has `start`, and `stop` methods. When we start the timer then we initialise `startedAt` and interval fields. We use the `performance` application interface to determine how many seconds passed since the timer has started.

For our game, we need a header panel that shows how many mines we need to find a new game button and timer

![header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/52l376nxjwo44uygo50b.png)

So, let's create components for them. As you can see a mine counter and timer have the same styles, we can create a component that receives a number and shows only three characters when the number is more than `999` and fill `0` when characters are less than 3. Next, create our `Count` component, `padStart` method helps to handle this behaviour.

```typescript
const displayCount = (count: number) => {
  const strCount = count.toString();
  if (strCount.length > 3) {
    return "999";
  }

  return strCount.padStart(3, "0");
};

export interface Props extends PropsWithChildren {
  count: number;
}

export const Count = ({ count }: Props) => {
  return <div className={styles.Count}>{displayCount(count)}</div>;
};
```
We will use the helper function `displayCount` that returns a string representation of the number. Then reuse this component for our needs. 

Create `src/components/bombsCount/BombsCount.tsx` and connect our component to the store using `observer` from `mobx-react-lite`. It will subscribe for store changes

```typescript
import { observer } from "mobx-react-lite";
import { store } from "../../app/store";
import { Count } from "../count/Count";

export const BombsCount = observer(() => {
  return <Count count={store.bombsLeft} />;
});
```
We will use `bombsLeft` from our root store. We need to wrap our component with `observer` HoC and in simple words it will render component whenever `bombsLeft` getter is changed. More information you can read [here](https://mobx.js.org/react-integration.html#react-integration)

> The observer HoC automatically subscribes React components to any observables that are used during rendering. As a result, components will automatically re-render when relevant observables change. It also makes sure that components don't re-render when there are no relevant changes. So, observables that are accessible by the component, but not actually read, won't ever cause a re-render.

> You can use as many observable components as you want.

Next, create `src/components/Timer.tsx` component. It will show how many seconds passed after starting the game.

```tsx
import { observer } from "mobx-react-lite";
import { store } from "../../app/store";
import { Count } from "../count/Count";

export const Timer = observer(() => {
  return (
    <Count count={store.timer ? Math.floor(store.timer.secondsPassed) : 0} />
  );
});
```
Then, create `src/components/NewGameButton.tsx`. It will show win or lose smile and start new game on click with the same properties.

```tsx 
export const NewGameButton = observer(() => {
  return (
    <button
      className={clsx(styles.root, {
        [styles.winner]: store.winner,
        [styles.loser]: store.winner === false,
      })}
      onClick={() => {
        store.startGame(store.gameType!, {
          rows: store.rows,
          columns: store.columns,
          bombs: store.bombs,
        });
      }}
    ></button>
  );
});
```
Now, create `src/components/board/Board.tsx`. We can place our cells using css `grid` templates

```tsx
export const Board = observer(() => {
  return (
    <div
      className={styles.Board}
      style={{
        gridTemplateRows: `repeat(${store.rows}, 30px)`,
        gridTemplateColumns: `repeat(${store.columns}, 30px)`,
      }}
    >
      {store.cells.map((cell, i) => (
        <Cell key={i} cell={cell} />
      ))}
    </div>
  );
});
```

Now, we have header and cells, then we can create gaming layout

`src/layout/game/Game.tsx`

```tsx
export interface Props extends PropsWithChildren {
  type: GameType;
}

export const Game = ({ type }: Props) => {
  useEffect(() => {
    store.startGame(type);
  }, [type]);

  return (
    <div className="Game">
      <header className="Game__header">
        <div className="bombs_counter">
          <BombsCount />
        </div>
        <div className="new_game">
          <NewGameButton />
        </div>
        <div className="timer">
          <Timer />
        </div>
      </header>
      <div className="Game__content">
        <Board />
      </div>
    </div>
  );
};
```
Here, we will start the game when component property `type` is changed.

Then, create `src/layout/layout/Layout.tsx`

```tsx
export const Layout = () => {
  return (
    <div>
      <nav className="nav">
        <ul>
          <li>
            <CustomLink to="/easy">Easy</CustomLink>
          </li>
          <li>
            <CustomLink to="/medium">Medium</CustomLink>
          </li>
          <li>
            <CustomLink to="/hard">Hard</CustomLink>
          </li>
          <li>
            <CustomLink to="/custom">Custom</CustomLink>
          </li>
        </ul>
      </nav>

      <Outlet />
    </div>
  );
};
```

`Outlet` component helps us to render child routes.

`CustomLink` component is `Link` from  with custom style decoration for our links. We will underline active link.

Now, we can add routing to render specific game type on different routes. We will use react-router-dom v6. In `App.tsx` add 

```tsx
<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Navigate to="easy" />} />
    <Route path="easy" element={<Game type={GameType.easy} />} />
    <Route path="medium" element={<Game type={GameType.medium} />} />
    <Route path="hard" element={<Game type={GameType.hard} />} />
    <Route path="custom" element={<CustomGame />} />
  </Route>
  <Route path="*" element={<Navigate to="easy" />} />
</Routes>
```

We will have `/easy`, `/medium`, `/hard` and `/custom` routes. As element we render `Game` component with specific `type` property. For `/custom` route we need to have additional form to let player enter rows, columns, bombs for game.

Now create form for custom game `src/components/customGameForm/CustomGameForm.tsx`

```tsx
interface FormElements extends HTMLFormControlsCollection {
  rows: HTMLInputElement;
  columns: HTMLInputElement;
  bombs: HTMLInputElement;
}

export const CustomGameForm = () => {
  const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
    e.preventDefault();

    const formElements = e.currentTarget.elements as FormElements;

    const rows = parseInt(formElements.rows.value, 10);
    const columns = parseInt(formElements.columns.value, 10);
    const bombs = parseInt(formElements.bombs.value, 10);

    if (rows * columns <= bombs) {
      alert("Bombs must be less then square count");
      return;
    }

    store.startGame(GameType.custom, {
      rows,
      columns,
      bombs,
    });
  };

  return (
    <form onSubmit={handleSubmit} className={styles.root}>
      <input
        className={styles.input}
        type="number"
        name="rows"
        id="rows"
        placeholder="Rows"
        defaultValue={customGameOptions.rows}
        min={1}
      />
      <input
        className={styles.input}
        type="number"
        name="columns"
        id="columns"
        placeholder="Columns"
        defaultValue={customGameOptions.columns}
        min={1}
      />
      <input
        className={styles.input}
        type="number"
        name="bombs"
        id="bombs"
        placeholder="Bombs"
        defaultValue={customGameOptions.bombs}
        min={1}
        max={999}
      />
      <input className={styles.input} type="submit" />
    </form>
  );
};

```
Here we create a form component and assign `onSubmit` function. Fields we can get from the event argument `e.currentTarget.elements` property. We define interface `FormElements` and write our field names.

Then we can create `src/layout/customGame/CustomGame.tsx` and use form there

```tsx
export const CustomGame = () => {
  return (
    <div>
      <CustomGameForm />
      <Game type={GameType.custom} />
    </div>
  );
};
```

![custom game](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lsbe713j6r0mwq1nmpsd.png)

Finally we can run `yarn start` and play game. There is one issue in custom game. When we type rows more then 50 and columns more than 50 and start game we receive `Maximum call stack size exceeded` error. 


![max call stack error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n3d553mptpqynpq92gj8.png)

The error means that our call stack is full of functions. Let see what happend when we click a cell. We call `cell.press()` method, then call `revealEmptyNeighbours`, the method runs `press` for each sibling and repeat while all empty siblings will be revealed. Therefore, when we have a lot of cells stack looks like

```ts
press();
revealEmptyNeighbours();
press();
revealEmptyNeighbours();
press();
...
```

```ts
...
press() {
    this.pressed = true;
    this.marked = false;

    if (this.bombsAmount === 0 && !this.hasBomb) {
      this.revealEmptyNeighbours();
    }
}

revealEmptyNeighbours() {
    this.neighbours.forEach((cell) => {
      if (cell.pressed || cell.hasBomb) {
        return;
      }

      cell.press();
    });
}
...
```

We can fix this by using `setTimeout`. Let wrap `this.revealEmptyNeighbours();` into `setTimeout`. 

```ts
press() {
    this.pressed = true;
    this.marked = false;

    if (this.bombsAmount === 0 && !this.hasBomb) {
      setTimeout(() => this.revealEmptyNeighbours());
    }
}
```
Instead of recursively adding functions to stack, we will run `revealEmptyNeighbours()` when our synchronous code has been executed. Now, we can play custom game with `100x100` or even `200x200` without any errors.

Here is screenshot custom game `100x100` with `200` mines

![100x100 custom game](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/seik2a06bytz4g8lcvj0.png)

Mobx is really powerful state management library. I would recommend you to use it when you have a lot of custom events, side effects in web app. For more information you can read about mobx [understanding reactivity](https://mobx.js.org/understanding-reactivity.html).

If you are still here, thank you for reading. Hope you like it. It is my first article, so fill free to write some feedback or any comments. The code is available in my public [repo](https://github.com/MykhailoHoroshko/minesweeper).