perf: win check now tracks safe cells revealed instead of scanning the whole grid
This commit is contained in:
parent
a2d8d975ec
commit
a1742e3a58
2 changed files with 49 additions and 9 deletions
|
|
@ -86,6 +86,8 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
|||
let mines = new Set()
|
||||
// Cells currently highlighted, so removeHighlights only resets these (<=9) instead of the whole grid
|
||||
let highlightedCells = []
|
||||
// Count of safe (non-mine) cells revealed, so the win check is O(1) instead of scanning the whole grid
|
||||
let revealedSafeCount = 0
|
||||
|
||||
this.initialize = function() {
|
||||
const headingElement = document.createElement('h1')
|
||||
|
|
@ -194,6 +196,7 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
|||
flagsCount = setting.mines
|
||||
mines.clear()
|
||||
highlightedCells = []
|
||||
revealedSafeCount = 0
|
||||
|
||||
for (let i = 0; i < setting.rows; i++) {
|
||||
let row = grid.insertRow(i)
|
||||
|
|
@ -560,14 +563,8 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
|||
}
|
||||
|
||||
function checkLevelCompletion() {
|
||||
let levelComplete = true
|
||||
for (let i=0; i<setting.rows; i++) {
|
||||
for(let j=0; j<setting.cols; j++) {
|
||||
const cell = grid.rows[i].cells[j]
|
||||
if (!isMine(cell) && cell.innerHTML=='') levelComplete=false
|
||||
}
|
||||
}
|
||||
if (levelComplete && grid.getAttribute('game-status') == 'active') {
|
||||
const safeCells = setting.rows * setting.cols - setting.mines
|
||||
if (revealedSafeCount >= safeCells && grid.getAttribute('game-status') == 'active') {
|
||||
grid.setAttribute('game-status', 'win')
|
||||
revealMines()
|
||||
}
|
||||
|
|
@ -817,6 +814,9 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
|||
function openCell(cell) {
|
||||
if (grid.getAttribute('game-status') != 'active') return
|
||||
|
||||
// A cell can be re-opened via chording, so only count the first reveal
|
||||
const wasOpen = getStatus(cell) == 'clicked' || getStatus(cell) == 'empty'
|
||||
|
||||
cell.className='clicked'
|
||||
setStatus(cell, 'clicked')
|
||||
firstClick = false
|
||||
|
|
@ -826,6 +826,7 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
|||
flagsDisplay.innerHTML = '😱'
|
||||
grid.setAttribute('game-status', 'over')
|
||||
} else {
|
||||
if (!wasOpen) revealedSafeCount++
|
||||
const mineCount = countMinesAround(cell)
|
||||
if (mineCount==0) {
|
||||
handleEmpty(cell)
|
||||
|
|
|
|||
|
|
@ -29,14 +29,24 @@ function everyCell(grid, fn) {
|
|||
describe('Minesweeper board', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
// Fake timers stop the 1ms game clock interval from running during tests.
|
||||
// Fake timers stop the requestAnimationFrame game clock from running during tests.
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
// Mount a game on a custom board injected via the cached 'setting' localStorage key.
|
||||
function mountCustomGame(setting, hooks) {
|
||||
localStorage.setItem('setting', JSON.stringify(setting))
|
||||
document.body.innerHTML = '<div id="app"></div>'
|
||||
const game = new Minesweeper('app', 'dev', hooks)
|
||||
game.initialize()
|
||||
return document.getElementById('grid')
|
||||
}
|
||||
|
||||
it('renders the beginner grid (9x9) by default', () => {
|
||||
const grid = mountGame()
|
||||
expect(grid.rows.length).toBe(9)
|
||||
|
|
@ -116,4 +126,33 @@ describe('Minesweeper board', () => {
|
|||
})
|
||||
expect(highlighted).toBe(1)
|
||||
})
|
||||
|
||||
it('declares a win once every safe cell is revealed', () => {
|
||||
// A mine-free 3x3 board: one click cascades to reveal all 9 safe cells.
|
||||
let finished = null
|
||||
const grid = mountCustomGame(
|
||||
{ rows: 3, cols: 3, mines: 0, id: 'test', name: 'test' },
|
||||
{ levelChanged: () => {}, gameDone: (g) => { finished = g.status } }
|
||||
)
|
||||
|
||||
leftClick(grid.rows[1].cells[1])
|
||||
|
||||
expect(finished).toBe('win')
|
||||
expect(grid.getAttribute('game-status')).toBe('done')
|
||||
everyCell(grid, cell => expect(cell.getAttribute('data-status')).not.toBe('default'))
|
||||
})
|
||||
|
||||
it('does not declare a win until the last safe cell is revealed', () => {
|
||||
// 1x3 board with the single mine pinned to the middle column.
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.5)
|
||||
const grid = mountCustomGame({ rows: 1, cols: 3, mines: 1, id: 'test', name: 'test' })
|
||||
|
||||
// First safe cell: adjacent to the mine, shows "1", no cascade -> not yet won.
|
||||
leftClick(grid.rows[0].cells[0])
|
||||
expect(grid.getAttribute('game-status')).toBe('active')
|
||||
|
||||
// Revealing the remaining safe cell completes the board.
|
||||
leftClick(grid.rows[0].cells[2])
|
||||
expect(grid.getAttribute('game-status')).toBe('done')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue