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 col = Math.floor(Math.random() * setting.cols)
|
||||
const transferMineToCell = grid.rows[row].cells[col]
|
||||
if (isMine(transferMineToCell) || isNeighbor(cell, transferMineToCell)) {
|
||||
if (isMine(transferMineToCell) || transferMineToCell === cell || isNeighbor(cell, transferMineToCell)) {
|
||||
continue
|
||||
} else {
|
||||
mines.add(mineKey(row, col))
|
||||
|
|
@ -792,20 +792,58 @@ const Minesweeper = function(appId, version, hooks = undefined) {
|
|||
cell.appendChild(spanElement)
|
||||
}
|
||||
|
||||
function handleEmpty(cell) {
|
||||
/**
|
||||
* Reveal a single known-non-mine cell. Returns true when the cell is blank
|
||||
* (no adjacent mines), which is the signal to keep flooding from it.
|
||||
* @param {HTMLTableCellElement} cell
|
||||
*/
|
||||
function revealSafeCell(cell) {
|
||||
// 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')
|
||||
if (!wasOpen) revealedSafeCount++
|
||||
|
||||
const mineCount = countMinesAround(cell)
|
||||
if (mineCount == 0) {
|
||||
updateCellValue(cell, ' ')
|
||||
let cellRow = cell.parentNode.rowIndex
|
||||
let cellCol = cell.cellIndex
|
||||
setStatus(cell, 'empty')
|
||||
//Reveal all adjacent cells as they do not have a mine
|
||||
for (let y = Math.max(cellRow-1,0); y <= Math.min(cellRow+1, setting.rows - 1); y++) {
|
||||
const rows = grid.rows[y]
|
||||
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 x = Math.max(cellCol-1,0); x <= Math.min(cellCol+1, setting.cols - 1); x++) {
|
||||
//Recursive Call
|
||||
const cell = rows.cells[x]
|
||||
if (cell && !isOpen(cell)) {
|
||||
clickCell(cell)
|
||||
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,32 +852,20 @@ 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
|
||||
|
||||
if (isMine(cell)) {
|
||||
cell.className = 'clicked'
|
||||
setStatus(cell, 'clicked')
|
||||
revealMines()
|
||||
flagsDisplay.innerHTML = '😱'
|
||||
grid.setAttribute('game-status', 'over')
|
||||
} else {
|
||||
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)
|
||||
return
|
||||
}
|
||||
//Count and display the number of adjacent mines
|
||||
|
||||
if (revealSafeCell(cell)) handleEmpty(cell)
|
||||
checkLevelCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Minesweeper
|
||||
|
|
|
|||
|
|
@ -155,4 +155,25 @@ describe('Minesweeper board', () => {
|
|||
leftClick(grid.rows[0].cells[2])
|
||||
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