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()
|
let mines = new Set()
|
||||||
// Cells currently highlighted, so removeHighlights only resets these (<=9) instead of the whole grid
|
// Cells currently highlighted, so removeHighlights only resets these (<=9) instead of the whole grid
|
||||||
let highlightedCells = []
|
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() {
|
this.initialize = function() {
|
||||||
const headingElement = document.createElement('h1')
|
const headingElement = document.createElement('h1')
|
||||||
|
|
@ -194,6 +196,7 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
||||||
flagsCount = setting.mines
|
flagsCount = setting.mines
|
||||||
mines.clear()
|
mines.clear()
|
||||||
highlightedCells = []
|
highlightedCells = []
|
||||||
|
revealedSafeCount = 0
|
||||||
|
|
||||||
for (let i = 0; i < setting.rows; i++) {
|
for (let i = 0; i < setting.rows; i++) {
|
||||||
let row = grid.insertRow(i)
|
let row = grid.insertRow(i)
|
||||||
|
|
@ -560,14 +563,8 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkLevelCompletion() {
|
function checkLevelCompletion() {
|
||||||
let levelComplete = true
|
const safeCells = setting.rows * setting.cols - setting.mines
|
||||||
for (let i=0; i<setting.rows; i++) {
|
if (revealedSafeCount >= safeCells && grid.getAttribute('game-status') == 'active') {
|
||||||
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') {
|
|
||||||
grid.setAttribute('game-status', 'win')
|
grid.setAttribute('game-status', 'win')
|
||||||
revealMines()
|
revealMines()
|
||||||
}
|
}
|
||||||
|
|
@ -817,6 +814,9 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
||||||
function openCell(cell) {
|
function openCell(cell) {
|
||||||
if (grid.getAttribute('game-status') != 'active') return
|
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'
|
cell.className='clicked'
|
||||||
setStatus(cell, 'clicked')
|
setStatus(cell, 'clicked')
|
||||||
firstClick = false
|
firstClick = false
|
||||||
|
|
@ -826,6 +826,7 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
||||||
flagsDisplay.innerHTML = '😱'
|
flagsDisplay.innerHTML = '😱'
|
||||||
grid.setAttribute('game-status', 'over')
|
grid.setAttribute('game-status', 'over')
|
||||||
} else {
|
} else {
|
||||||
|
if (!wasOpen) revealedSafeCount++
|
||||||
const mineCount = countMinesAround(cell)
|
const mineCount = countMinesAround(cell)
|
||||||
if (mineCount==0) {
|
if (mineCount==0) {
|
||||||
handleEmpty(cell)
|
handleEmpty(cell)
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,24 @@ function everyCell(grid, fn) {
|
||||||
describe('Minesweeper board', () => {
|
describe('Minesweeper board', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear()
|
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()
|
vi.useFakeTimers()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers()
|
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', () => {
|
it('renders the beginner grid (9x9) by default', () => {
|
||||||
const grid = mountGame()
|
const grid = mountGame()
|
||||||
expect(grid.rows.length).toBe(9)
|
expect(grid.rows.length).toBe(9)
|
||||||
|
|
@ -116,4 +126,33 @@ describe('Minesweeper board', () => {
|
||||||
})
|
})
|
||||||
expect(highlighted).toBe(1)
|
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