refactor: flood-fill logic
This commit is contained in:
parent
a1742e3a58
commit
76e95d20da
2 changed files with 80 additions and 33 deletions
|
|
@ -741,7 +741,7 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
||||||
let row = Math.floor(Math.random() * setting.rows)
|
let row = Math.floor(Math.random() * setting.rows)
|
||||||
let col = Math.floor(Math.random() * setting.cols)
|
let col = Math.floor(Math.random() * setting.cols)
|
||||||
const transferMineToCell = grid.rows[row].cells[col]
|
const transferMineToCell = grid.rows[row].cells[col]
|
||||||
if (isMine(transferMineToCell) || isNeighbor(cell, transferMineToCell)) {
|
if (isMine(transferMineToCell) || transferMineToCell === cell || isNeighbor(cell, transferMineToCell)) {
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
mines.add(mineKey(row, col))
|
mines.add(mineKey(row, col))
|
||||||
|
|
@ -792,20 +792,58 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
||||||
cell.appendChild(spanElement)
|
cell.appendChild(spanElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEmpty(cell) {
|
/**
|
||||||
updateCellValue(cell, ' ')
|
* Reveal a single known-non-mine cell. Returns true when the cell is blank
|
||||||
let cellRow = cell.parentNode.rowIndex
|
* (no adjacent mines), which is the signal to keep flooding from it.
|
||||||
let cellCol = cell.cellIndex
|
* @param {HTMLTableCellElement} cell
|
||||||
setStatus(cell, 'empty')
|
*/
|
||||||
//Reveal all adjacent cells as they do not have a mine
|
function revealSafeCell(cell) {
|
||||||
for (let y = Math.max(cellRow-1,0); y <= Math.min(cellRow+1, setting.rows - 1); y++) {
|
// A cell can be re-opened via chording, so only count the first reveal
|
||||||
const rows = grid.rows[y]
|
const wasOpen = getStatus(cell) == 'clicked' || getStatus(cell) == 'empty'
|
||||||
if (!rows) continue
|
cell.className = 'clicked'
|
||||||
for(let x = Math.max(cellCol-1,0); x <= Math.min(cellCol+1, setting.cols - 1); x++) {
|
setStatus(cell, 'clicked')
|
||||||
//Recursive Call
|
if (!wasOpen) revealedSafeCount++
|
||||||
const cell = rows.cells[x]
|
|
||||||
if (cell && !isOpen(cell)) {
|
const mineCount = countMinesAround(cell)
|
||||||
clickCell(cell)
|
if (mineCount == 0) {
|
||||||
|
updateCellValue(cell, ' ')
|
||||||
|
setStatus(cell, 'empty')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCellValue(cell, mineCount.toString())
|
||||||
|
const dataValue = document.createAttribute('data-value')
|
||||||
|
dataValue.value = mineCount.toString()
|
||||||
|
cell.setAttributeNode(dataValue)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iteratively reveal the connected region of blank cells, starting from a
|
||||||
|
* cell that has already been revealed as blank. Uses an explicit queue with
|
||||||
|
* isOpen() as the visited guard instead of recursing back through clickCell,
|
||||||
|
* so there are no redundant per-cell checks and no deep call stack.
|
||||||
|
* @param {HTMLTableCellElement} startCell
|
||||||
|
*/
|
||||||
|
function handleEmpty(startCell) {
|
||||||
|
const queue = [startCell]
|
||||||
|
while (queue.length) {
|
||||||
|
const cell = queue.shift()
|
||||||
|
const cellRow = cell.parentNode.rowIndex
|
||||||
|
const cellCol = cell.cellIndex
|
||||||
|
for (let i = Math.max(cellRow-1,0); i <= Math.min(cellRow+1, setting.rows - 1); i++) {
|
||||||
|
const rows = grid.rows[i]
|
||||||
|
if (!rows) continue
|
||||||
|
for (let j = Math.max(cellCol-1,0); j <= Math.min(cellCol+1, setting.cols - 1); j++) {
|
||||||
|
const neighbor = rows.cells[j]
|
||||||
|
if (!neighbor || neighbor === cell) continue
|
||||||
|
if (isFlagged(neighbor)) {
|
||||||
|
setBusy()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (isOpen(neighbor)) continue
|
||||||
|
// blank cells have no adjacent mines, so every neighbor here is safe
|
||||||
|
if (revealSafeCell(neighbor)) queue.push(neighbor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -814,31 +852,19 @@ 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'
|
|
||||||
setStatus(cell, 'clicked')
|
|
||||||
firstClick = false
|
firstClick = false
|
||||||
|
|
||||||
if (isMine(cell)) {
|
if (isMine(cell)) {
|
||||||
|
cell.className = 'clicked'
|
||||||
|
setStatus(cell, 'clicked')
|
||||||
revealMines()
|
revealMines()
|
||||||
flagsDisplay.innerHTML = '😱'
|
flagsDisplay.innerHTML = '😱'
|
||||||
grid.setAttribute('game-status', 'over')
|
grid.setAttribute('game-status', 'over')
|
||||||
} else {
|
return
|
||||||
if (!wasOpen) revealedSafeCount++
|
|
||||||
const mineCount = countMinesAround(cell)
|
|
||||||
if (mineCount==0) {
|
|
||||||
handleEmpty(cell)
|
|
||||||
} else {
|
|
||||||
updateCellValue(cell, mineCount.toString())
|
|
||||||
const dataValue = document.createAttribute('data-value')
|
|
||||||
dataValue.value = mineCount.toString()
|
|
||||||
cell.setAttributeNode(dataValue)
|
|
||||||
}
|
|
||||||
//Count and display the number of adjacent mines
|
|
||||||
checkLevelCompletion()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (revealSafeCell(cell)) handleEmpty(cell)
|
||||||
|
checkLevelCompletion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,4 +155,25 @@ describe('Minesweeper board', () => {
|
||||||
leftClick(grid.rows[0].cells[2])
|
leftClick(grid.rows[0].cells[2])
|
||||||
expect(grid.getAttribute('game-status')).toBe('done')
|
expect(grid.getAttribute('game-status')).toBe('done')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('flood fill stops at a flagged cell and never reveals it', () => {
|
||||||
|
// 1x4 board with the mine pinned to the last column.
|
||||||
|
vi.spyOn(Math, 'random').mockReturnValue(0.99)
|
||||||
|
const grid = mountCustomGame({ rows: 1, cols: 4, mines: 1, id: 'test', name: 'test' })
|
||||||
|
|
||||||
|
// Flag a safe cell sitting between the blank region and the mine.
|
||||||
|
// (full press + release so the internal right-button flag resets)
|
||||||
|
const flag = grid.rows[0].cells[2]
|
||||||
|
flag.dispatchEvent(new MouseEvent('mousedown', { button: 2, bubbles: true }))
|
||||||
|
flag.dispatchEvent(new MouseEvent('mouseup', { button: 2, bubbles: true }))
|
||||||
|
expect(flag.getAttribute('data-status')).toBe('flagged')
|
||||||
|
|
||||||
|
// Click the far blank cell; the cascade must stop at the flag.
|
||||||
|
leftClick(grid.rows[0].cells[0])
|
||||||
|
|
||||||
|
expect(grid.rows[0].cells[0].getAttribute('data-status')).toBe('empty')
|
||||||
|
expect(flag.getAttribute('data-status')).toBe('flagged')
|
||||||
|
// A safe cell is still hidden (behind the flag), so it is not a win.
|
||||||
|
expect(grid.getAttribute('game-status')).toBe('active')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue