A few days ago, I saw an article about how to implement a Chinese chess program using TypeScript type operations. After I learned TypeScript, which is a collection of TypeScript gymnastic postures that I don’t use much in future projects, I took the opportunity to refresh myself.

demand

The main functions of minesweeper are clicking squares, marking squares, and checking game results:

  • I'm gonna go to "Game, abscissa, ordinate"
  • Tag < game, abscissa, ordinate >
  • Check the game < game >

The data structure of the grid

In minesweeper, each cell has only three types: mines, blanks, numbers (indicating the number of bombs in the surrounding 8 cells), and each cell can be flagged or clicked to show its original contents, so we define it like this:

typeBlank ='blank'
typeDigital ='digital'
typeThe bomb ='bomb'

typeBlank grid type = | | digital bombstypeCell content = {type: cell type, number of bombs around:number, clicked:booleanTo be marked:boolean
}

typeConstruct the lattice content < typeextendsCell type, number of bombsextends number = 0By clickingextends boolean = false, markedextends boolean = false> = {type: type, clicked: clicked, bombs around: number of bombs, marked: marked}Copy the code

The game structure

Since minesweeper interfaces are made up of standard N*M tiles, it is natural to think of a two-dimensional array to represent the structure:

typeAbscissa =0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
typeOrdinate =0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8

typeA row of cells = [cells content, cells content, cells content, cells content, cells content, cells content, cells content, cells content]typeGame = [row, row, row, row, row, row, row, row, row, row, row]Copy the code

Here we define a 9 by 9 minesweeper game. A game has nine rows, each row has nine squares.

We can define a minesweeper game like this:

typeA game = [[construct grid content < blank >, construct grid content < blank >, construct grid content < number,1>,... ] . ]Copy the code

At the same time, two-dimensional arrays are also convenient for us to directly use subscripts to obtain a corresponding grid:

typeA grid = a game [0] [4]
Copy the code

Grid operation

Next is the core part, we need to click on the grid, marking grid, namely change the grid by clicking in the content and marked the two properties, but we can’t go directly to modify a defined type value, so we can change the way of thinking, to create a new grid directly replace the old grid line (see previous article mentioned method).

Update the grid

Our game structure is game-line-lattice-lattice-so if we want to update a cell, we need to find the row corresponding to the cell to be updated, update the corresponding cell, and then update the row.

So how do you replace an item in an array? We can do this recursively, if we’re replacing the 0th item, we just merge the new item with the rest of the array, and if we’re replacing the third item, we just merge the first two items with the new item, and everything after the third item.

The concrete implementation is as follows:

typeReplace some cell in a row that is less than some row, new cell, replace the x-coordinateextendsThe abscissa temporarily stores an array of all the cells in front of the replacement cellextends any[] = [] > = a lineextends[infer head,...infer rest]? Substitution abscissaextends 0? [... temporary array of all squares in front of the replacement grid, new grid,... remaining] : Replaces a grid on a row < remaining, new grid, minus 1 < replace abscis >, [... temporary array of all squares in front of the replacement grid, head]> : []Copy the code

Each recursion is to take the first cell in a row and put it into the temporary array. When the replacement coordinate is reduced to 0, it means that the first cell in the row array is the one to be replaced. At this time, the previous temporary and new cells and the remaining array are merged, which is the result after the replacement.

A subtractive type is used here. As a quick note, Typescript doesn’t add or subtract, but it does use array subscripting to look up values.

typeAdd table = [1.2.3.4.5.6.7.8.9.never
]
typeSubtraction table = [never.0.1.2.3.4.5.6.7.8.9
]

typeLook up table < table, number, value =never> = numberextendsKeyof form? Table [number] : valuetypeAdd one < number > = query table < add table, number >typeSubtract one < number > = look up table < subtraction table, number >typeFive = minus one <6>
Copy the code

Substituting a line might be similar:

typeReplace some cell in a row that is less than some row, new cell, replace the x-coordinateextendsOn the x-coordinate, store the previous array temporarilyextends any[] = [] > = a lineextends[infer head,...infer rest]? Substitution abscissaextends 0? [... staging in front of the array, the new grid,... the rest of the] : replace a row of a grid < remaining, the new grid, minus one < replace abscissa >, [... temporary array, in front of head] > : []Copy the code

Combining the above two methods, we can implement a method to replace a grid in the game:

typeReplace a grid in the game < a board gameextendsGame, new gridextendsThe contents of the grid, replace the abscissaextendsX-coordinate, replace y-coordinateextendsOrdinate > = Replace a row of the game < a game, replace a grid of a row < a game, replace a grid, replace a grid >, replace a grid >Copy the code

With a grid type to replace a grid in the game, we implement the tag. Clicking on the grid is easy, essentially finding the original grid by its coordinates, using its original attributes plus the changed marked or clicked attributes to create a new grid, and then replacing it:

typeClick on < gameextendsThe game,extendsX-coordinate, verticalextendsOrdinate > = a game [vertical][horizontal]extendsInfer old grid? The old gridextendsGrid content? The old grid ['Clicked'] extends false? The old grid ['marked'] extends false? Replace a box in the game < a board game, construct the box content < old box ['type'], old grid ['Number of bombs around'].true, old grid ['marked'> < span style = "max-width: 100%; clear: both; min-height: 1em;never
  : never

typeTag < a gameextendsThe game,extendsX-coordinate, verticalextendsOrdinate > = a game [vertical][horizontal]extendsInfer old grid? The old gridextendsGrid content? The old grid ['Clicked'] extends false? Replace a box in the game < a board game, construct the box content < old box ['type'], old grid ['Number of bombs around'], old grid ['Clicked'], old grid ['marked'] extends true ? false : true>, horizontal, vertical > : a game:never
: never
Copy the code

One thing to note here is that only unmarked and clicked squares need to be replaced, so there is an extends False in clicking: old [‘ clicked ‘] extends False, old [‘ marked ‘] extends False; Only unclicked squares can be marked, and the marks can be reversed repeatedly.

So we’re ready to play:

typeG2 = Click < game,4.2>

typeR2 = Render game <g2>typeC2 = Check game <g2>Copy the code

Apply colours to a drawing

Basically, the core functions have been implemented, and in order to give the player a better experience, we still need to make a render type to render the current state of the game.

First we need to define the display characters in various states:

typeFlag grid ='🚩'
typeBomb grid ='💣'
typeBlank grid ='🟦'
typeUnclicked grid ='🟪'
typeNumeric grid array = ['0 ️ ⃣'.'1 ️ ⃣'.'2 ️ ⃣'.'3 ️ ⃣'.'4 ️ ⃣'.'5 ️ ⃣'.'6 ️ ⃣'.'7 ️ ⃣'.'8 ️ ⃣']
Copy the code

Second, let’s consider how to render a single grid:

  • Not clicked,
    • Unmarked, 🟪 is displayed
    • Marked to display 🚩
  • By clicking
    • Is a blank grid that shows 🟦
    • It’s a bomb, showing 💣
    • Figure 1️ 2️ 3️ 4 negative 5 Saturday 7 maximum 8 on the number of bombs around

Translated into TypeScript:

typeRender grid < a gridextendsCell contents > = a cell ['Clicked'] extends false? A cell ['marked'] extends true? Flag grid: Unclicked grid: a grid ['type'] extendsBlank? 3. A grid ['type'] extendsThe bomb? Bomb: array of numeric grids [a grid]'Number of bombs around']]
Copy the code

Next render a row of cells:

typeRender a row of cells < a row of cells, render the resultextends string = ' '> = a row of cellsextends[infer first grid,...infer rest grid]? The first cellextendsGrid content? Render one row of cells < remaining cells,`${render result}${render the resultextends ' ' ? ' ' : ' '}${render grid < first grid >}`>
  : ' '
  : ' '
Copy the code

Since we have no control over the IDE’s newlines in TypeScript, we need to render the results as object results (see the method in this article)

typeRender game < a gameextendsGame > = {[key]inRender a grid < key >}Copy the code

PS: Sometimes the keys are not rendered in the order from 0 to 8, so I don’t know what else to do.

Here we can see the rendered game result:

Is the game over

Finally, we need to implement a feature to verify that the current game is over, i.e. the game succeeds or fails by checking whether any 💣 type cells are clicked or all non-bomb cells are clicked.

The game can only have three states:

  • The game failed
  • The game is successful
  • The game can still go on

Let’s first define these three types:

typeGame success ='✔ ️'
typeGame failure ='❌
typeThe game can still continue ='🧹'
Copy the code

Our strategy here is to check each cell, if a cell is checked:

  • By clicking
    • Non-bomb type, the next cell can be checked, and if no previous marker continues, the marker succeeds
    • Bomb type. Return to failure
  • Didn’t click
    • Non-bomb type, check the next box and mark it as continue (if the previous mark was successful, replace it directly)
    • If it is bomb type, check the next grid

After all checks are completed:

  • Encounter failure, it is the game failure
  • If the last mark is continued, the game can continue
  • The last marked success indicates success

With the logic sorted out, we soon have code like this:

typeCheck the grid < a gridextendsCell contents > = a cell ['Clicked'] extends true? A cell ['type'] extendsThe bomb? Game failure: Game success: a grid ['type'] extendsThe bomb? Game success: The game can still be playedtypeCheck for a line < a line, resultextendsGame result = game success > = some lineextends[infer the first grid,...infer the rest]? The first cellextendsGrid content? Check the grid < first grid >extendsInfer the first result? The first resultextendsGame failure? Game failure: the first resultextendsCan the game continue? Check for a line < remaining, game can continue > : resultextendsCan the game continue? Check for a line < remaining, the game can continue > : check for a line < remaining, the game succeeded > :never
    : neverResults:typeCheck game < certain game, resultextendsGame result = game success > = a gameextends[infer] [first line,...infer]? The first lineextendsA row of squares? Check a line < first line >extendsInfer the first result? The first resultextendsGame failure? Game failure: the first resultextendsCan the game continue? Check the game < remaining lines, game can still continue > : resultextendsCan the game continue? Check game < remaining lines, the game can still continue > : check game < remaining lines, the game succeeded > :never
    : neverResults:Copy the code

Above, a game with the most basic minesweeper function is complete

Afterword.

This article is just about the feature implementation, some of the implementation methods are a bit problematic, for example, there should be a better way to check the way the game is implemented (I haven’t thought of it yet).

The code address

Online preview address, mouse over render game return type, you can see the game content.

In addition, there are some functions that need to be implemented, which I haven’t thought clearly about for the time being:

  • Click on the blank grid to spread out the surrounding blank grids
  • By passing in bomb coordinates, the game is constructed
  • .