Minesweeper game built with React, little-saga and @little-saga/use-saga
Follow the links and start playing the game:
As you can see, you could configure the game via URL parameter. Enjoy playing~
Minesweeper is a single-player puzzle video game. The objective of the game is to clear a rectangular board containing hidden "mines" or bombs without detonating any of them, with help from clues about the number of neighboring mines in each field.
The user interface of this game is quite straightforward: there are 480 (30 * 16, advanced level default) tiles and each tile is either covered or revealed. Every covered tile may or may not have a mine under it. Every revealed tile shows the number of neighboring mines. Our task is to clear/open all the tiles while avoiding mines.
Data structure of the game state is not hard to design and implement. But when we play the game, the game state changes a lot and the UI changes a lot. Manipulating data is easy while synchronizing the UI according to the state by hand is really hard. In such cases, React turns out be a great view library which can synchronize the UI automatically for us.
This project uses React to draw the UI, Immutable.js as the data structure utilities, little-saga and @little-saga/use-saga to implement the state management and game logic. Other used packages can be viewed in package.json. If you are familiar with these packages, then it is easy to figure out how the game runs.
By default, the game contains 480(30*16) tiles. We use array mines
of length 480 to store whether there is a mine under tile. Tile at row row
and col col
corresponds to array index row * 30 + col
, and...
-1
in the array means there is a mine.- A non-negative number
x
means there is no mine under the tile and there arex
neighboring mines.
We use another array modes
to store the mode of each tile. Each tile has one of the following modes:
COVERED
means it is covered now. (Initial mode).REVEALED
means it is revealed now. (After player clicks the tile or open propagation).FLAG
means that player right-clicks the tile and marks there's a mine under the tile.QUESTIONED
means that player right-clicks the tile twice and marks there may be a mine under the tile.CROSS
means that player mark the wrong flags when game fails.EXPLODED
means that this tile with a mine leads to game fail.
And status
keeps the game state/status. Game status is one of the following 4 choices: IDLE, ON, WIN, LOSE
In IDLE status, all tiles are covered and the timer is stopped, and when player click one tile timer will start and status will become ON. In ON status, there are some tiles revealed and timer is counting which means the game is running. Once player clears all tiles or detonates a mine, then status becomes WIN or LOSE.
timer
keeps the number of seconds since game start.
indicators
is used for AI auto playing the game. See the code for detail.
status
, mines
, modes
and timer
, the four variables together are sufficient to describe the total game state.
The render loginc of the 480 tiles are defined in App#renderElements
in app/App.js. For each tile at row
-th row col
-th column that corresponds to array index i = row * 30 + col
, we examine modes[i]
and mines[i]
to determine what should be rendered as follows:
- If
modes[i]
isREVEALED
, then a<Mine />
or<Number number={x} />
should be rendered according tomines[i]
; - If
modes[i]
isFLAG
orQUESTIONED
, then a<Flag />
or<QuestionMark />
is renderd; - If
modes[i]
isCROSS
OREXPLODED
, then a<Mine />
with properties corss/exploded set should be renderd; - Else
modes[i]
isCOVERED
, render nothing.
status
indicates global game state and influences the player interactions. Only in IDLE
/ON
status should game handle click actions. On WIN
/LOSE
status it is supposed that player must click the face to restart the game.
Besides left-click, we needs to handle middle-click (reveal neighboring tiles) and right-click (toggle among COVERD
, FLAG
, QUESTIONED
). The game supports mouse movement with left-button or middle-button pressed, so wee need to track the mouse state. Because mouse state is not related with global game state, we puts the mouse state in App#state
.
In App#state
, field btn1
/btn2
records whether left-button/middle-button is pressed now. Field point
records the array index of the tile under the mouse. We add event listeners at svg element to manipulate these fields. The event listeners are App#onContextMenu
, App#onMouseDown
, App#onMouseMove
and App#onMouseUp
.
In onMouseDown
, if the game is not running, then the mouse down event is ignored, which is equivalent to disabling interactions. We do not add onClick
event listener. The click actions are dispatched in onMouseUp
after checking the mouse state. Is this way we implements mouse interactions that allow click behavior and allow mouse movement when the button is pressed.
The mouse interactions will be translated to three kinds of redux actions: LEFT_CLICK
, MIDDLE_CLICK
and RIGHT_CLICK
. The action describes which button is clicked and the corresponding tile array index. There three kinds of actions are processed by saga in app/sagas.js which will translate them into more detailed actions like REVEAL
or GAME_ON
for reducers.
Detailed logic could be viewed in app/App.js and app/sagas.js.
The game main loop is managed by sagas and reducers. The root saga in app/sagas.js starts several other sagas.
Saga handleXxxClick
handles click actions and translate them into more detailed actions which will be processed by reducers.
Saga timerHandler
watches for GAME_OVER_WIN
/ GAME_OVER_LOSE
/ RESTART
actions and then reset timer. This saga also forks a tickEmitter
to emit a tick action every second when the game status is ON
.
Saga watchReveal
watches for reveal actions. If player clears all tiles then this saga dispatch a GAME_OVER_WIN
action; If player detonates a mine then this saga dispatch GAME_OVER_LOSE
.
Saga handleLeftClick
handles LEFT_CLICK
actions. A LEFT_CLICK
action describes that player click one tile and wants to reveal it. If we generates mines before player's first click , then the first click may encounter a mine and it is frustrating. So we generates mines after the first click and ensure first click can make a open propagation. When handling click actions in IDLE
status, the left click must be the first left click; In ON
status, it is not the first click.
Since in IDLE
status mines have not been generated, defaultMines
is used (in app/common.js). Function generateMines
(in app/common) uses reservoir sampling algothrim and could generate random mines.
Saga handleMiddleClick
handles MIDDLE_CLICK
actions. It first checks the number at the tile (the tile is revealed, so it has a number) is equal to the number of neighboring flags. If the two numbers are equals, it means that neighboring covered tiles are supported to be safe, so it will reveal all the safe tiles at once.
Saga handleRightClick
handles RIGHT_CLICK
actions and dispatch CHANGE_MODE
actions.
Most logic is implemented in sagas. Reducers are rather straightforward so it is not covered here.
Components are listed in the above image. The first line contains an <LED />
, four <Face />
with different facial expressions. The <LED />
contains three <SevenSegmentDisplay />
and a border. The second line contains several <Number />
. The third line contains <Mine />
, <Flag />
, <QuestionMark />
and <Cover />
.