let sweepStates = { start: "start", solving: "solving", stuck: "stuck", solved: "solved", death: "death" }; let autoSweepConfig = { doLog: false, isAutoSweepEnabled: false, isRiddleFinderMode: false, isRecordingStepStats: false, isVirtualMode: false, virtualGameConfig: { width: 30, height: 16, bombAmount: 99 }, virtualBatchSize: 1000, baseIdleTime: 0, gameFinishedIdleTime: 0, state: { gameIndex: 0, lastSweepResult: { state: null, solver: null } } }; let autoSweepStats = { gameStats: [] }; disableEndOfGamePrompt(); setKeyDownHandler(); function disableEndOfGamePrompt() { prompt = () => "cancel"; } function setKeyDownHandler() { if (!window.sweepKeyDown) { sweepKeyDown = keyDownHandler; document.addEventListener("keydown", keyDownHandler); } function keyDownHandler(e) { switch (e.key) { case "w": sweepStepGuessing(); break; case "W": sweepStepGuessing(false); break; case "e": sweepStepCertain(); break; case "E": sweepStepCertain(false); break; case "s": startAutoSweep(autoSweepConfig, autoSweepStats); break; case "d": stopAutoSweep(autoSweepConfig); break; case "i": formatLogGameStats(); break; case "o": formatLogGameStatsWithRaw(); break; case "k": resetGameStats(); break; case "l": toggleDoLog(autoSweepConfig); break; } } } function sweepStepCertain(withBoardInteraction = true) { sweepStep(withBoardInteraction, false, "lastForSweepStepCertain" + withBoardInteraction); } function sweepStepGuessing(withBoardInteraction = true) { sweepStep(withBoardInteraction, true, "lastForSweepStepGuessing" + withBoardInteraction); } function sweepStep(withBoardInteraction, withGuessing, lastStatePropName) { let boardState = getBoardState(); if (window[lastStatePropName] !== boardState) { let sweepResult = sweepPage(withGuessing, true); executeInteractions(sweepResult.interactions, withBoardInteraction); window[lastStatePropName] = boardState; } function getBoardState() { let boardState = ""; let squares = document.getElementsByClassName("square"); for (let i = 0; i < squares.length; i++) { if (squares[i].style.display !== "none") { boardState += squares[i].className; } } return boardState; } } function startAutoSweep(config, stats) { config.isAutoSweepEnabled = true; setTimeout(() => autoSweep(config, stats), 0); } function stopAutoSweep(config) { config.isAutoSweepEnabled = false; } function formatLogGameStats(gamesIncluded = null) { formatLogStats(autoSweepStats, gamesIncluded); } function formatLogGameStatsWithRaw(gamesIncluded = null) { formatLogStats(autoSweepStats, gamesIncluded, true); } function formatLogStats(stats, gamesIncluded = null, logRaw = false) { let gameStats = stats.gameStats.slice(0); if (gamesIncluded !== null && gamesIncluded > 0) { gameStats = gameStats.filter((c) => c.index < gamesIncluded); } gameStats = gameStats.filter((c) => c.finishState); if (gameStats.length === 0) { return; } console.log("Stats for first " + gameStats.length + " games"); logWinningPercentage(); logGuessesPerGame(); logTimePerGame(); logTimePerStep(); if (logRaw) { console.log("Raw data: ", gameStats); } function logGuessesPerGame() { let guessesPerGame = gameStats.map((g) => g.guesses); let guessStats = mapStats(guessesPerGame); logStat("Average/Max guesses", guessStats.average.toFixed(2) + " / " + guessStats.max.toFixed(0)); } function logTimePerGame() { let timePerGame = gameStats.map((g) => g.time); let timeStats = mapStats(timePerGame); logStat("Average/Max time", timeStats.average.toFixed(2) + " / " + timeStats.max.toFixed(2) + " ms"); } function logTimePerStep() { let stepTimeStatsMax = gameStats.reduce((a, b) => Math.max(a, b.mostTimeStep), 0); logStat("Highest step time", stepTimeStatsMax.toFixed(2) + " ms"); let stepTimeStatsMax3 = gameStats.reduce((a, b) => Math.max(a, b.mostTime3Step), 0); logStat("Highest [3] time", stepTimeStatsMax3.toFixed(2) + " ms"); } function logWinningPercentage() { let wins = gameStats.reduce((a, b) => a + (b.finishState === sweepStates.solved ? 1 : 0), 0); let winPercentage = (wins / gameStats.length) * 100.0; logStat("Winning percentage", winPercentage.toFixed(2) + "% (" + wins + "/" + gameStats.length + ")"); } function logStat(title, formatStat) { console.log("-> " + title + ":\t\t" + formatStat); } function mapStats(values) { return { min: min(values), max: max(values), average: average(values), median: median(values), sum: sum(values), values: values }; } function sum(values) { return values.reduce((a, b) => a + b); } function min(values) { return values.reduce((a, b) => Math.min(a, b)); } function max(values) { return values.reduce((a, b) => Math.max(a, b)); } function average(values) { return sum(values) / values.length; } function median(values) { let sortedValues = values.slice(0).sort((a, b) => a - b); let half = Math.floor(sortedValues.length / 2); return values.length % 2 ? values[half] : (values[half - 1] + values[half]) / 2.0; } } function resetGameStats(stats = autoSweepStats) { if (stats.gameStats && stats.gameStats.length > 0) { stats.gameStats = stats.gameStats.filter((c) => !c.finishState); } else { stats.gameStats = []; } } function toggleDoLog(config) { config.doLog = !config.doLog; } function startNewGameForAutoSweep(config = autoSweepConfig) { config.state.lastSweepResult.state = null; config.state.lastSweepResult.solver = null; config.state.gameIndex += 1; if (config.isVirtualMode) { restartVirtualGame(config.virtualGameConfig); } else { simulate(document.getElementById("face"), "mousedown"); simulate(document.getElementById("face"), "mouseup"); } } function isGuessingSolver(solver) { return solver !== null && solver.includes("g"); } function autoSweep(config, stats) { let iterations = config.virtualBatchSize; do { if (!config.isAutoSweepEnabled) { return; } let idleTime; let restartNeeded = lastWasNewGameState(config); if (restartNeeded) { if (config.state.lastSweepResult.state === "death" && !isGuessingSolver(config.state.lastSweepResult.solver)) { throw new Error("Died while not guessing!"); } startNewGameForAutoSweep(config); idleTime = 0; } else { idleTime = executeSweepCycle(config, stats); } iterations -= 1; if (!(iterations > 0 && config.isVirtualMode)) { continueAutoSweep(config, stats, idleTime); } } while (iterations > 0 && config.isVirtualMode); function continueAutoSweep(config, stats, idleTime) { if (config.isAutoSweepEnabled) { let timeOutTime = idleTime + config.baseIdleTime; setTimeout(() => autoSweep(config, stats), timeOutTime); } } function executeSweepCycle(config, stats) { let idleTime = 0; let sweepResult = sweepPage(true, config.doLog, config, stats); let isRiddle = !config.isVirtualMode && config.isRiddleFinderMode && sweepResult.solver === "3"; if (isRiddle) { config.isAutoSweepEnabled = false; } else { idleTime = isNewGameState(sweepResult.state) ? config.gameFinishedIdleTime : 0; if (!sweepResult.solver) { sweepResult.solver = config.state.lastSweepResult.solver; } config.state.lastSweepResult = sweepResult; } if (config.isAutoSweepEnabled || isRiddle) { executeInteractions(sweepResult.interactions, !isRiddle, config.isVirtualMode); } return idleTime; } function lastWasNewGameState(config) { return isNewGameState(config.state.lastSweepResult.state); } } function getBombArray(field) { return field.reduce((rowA, rowB) => rowA.concat(rowB.reduce((cellA, cellB) => cellA.concat(cellB.isBomb ? true : false), [])), []); } function isNewGameState(state) { return state === sweepStates.solved || state === sweepStates.death; } function executeInteractions(interactions, withBoardInteraction, isVirtualMode) { if (isVirtualMode) { executeVirtualInteractions(interactions); } else { if (withBoardInteraction) { executeInterationsOnBoard(interactions); } else { formatLogInteractions(interactions); } } function executeInterationsOnBoard(interactions) { interactions.forEach((action) => { if (action.isFlag) { if (action.cell.div.classList.value !== "square bombflagged") { simulate(action.cell.div, "mousedown", 2); simulate(action.cell.div, "mouseup", 2); } } else { simulate(action.cell.div, "mouseup"); } }); } function formatLogInteractions(interactions) { console.log("Interations:"); if (interactions.length > 0) { interactions.forEach((action) => { console.log("-> " + (action.isFlag ? "Flag" : "Reveal") + ":", action.cell.div); }); } else { console.log("-> None"); } } } function getBombAmount() { let optionsForm = $("#options-form"); let checkedBox = optionsForm.find('input[name="field"]:checked'); let optionsRow = checkedBox.parent().parent().parent(); let amountBombsCell = optionsRow.find("td").last(); let bombAmount = Number(amountBombsCell.html()); if (isNaN(bombAmount)) { bombAmount = Number(amountBombsCell.children()[0].value); } return bombAmount; } function executeVirtualInteractions(interactions) { if (!window.virtualGame) { return; } let game = window.virtualGame; let field = window.virtualGame.field; let cells = getCellsFromField(field); if (game.hasStarted) { interactions.forEach((action) => { if (action.isFlag) { action.cell.isFlagged = true; action.cell.isUnknown = false; } else { revealCell(action.cell); } }); } else { revealFirstCell(cells, interactions[0].cell, window.virtualGame.bombAmount); game.hasStarted = true; } function revealFirstCell(cells, cell, bombAmount) { let viableBombCells = getViableBombCells(cells, cell); setBombs(viableBombCells, bombAmount); setDigits(cells); revealCell(cell); } function setBombs(viableBombCells, bombAmount) { for (let i = 0; i < bombAmount; i++) { let bombsLeft = bombAmount - i; let nonBombs = viableBombCells.filter((c) => !c.isBomb); setBomb(nonBombs, bombsLeft); } } function revealCell(cellToReveal) { let revealCells = [cellToReveal]; while (revealCells.length > 0) { let cell = revealCells.pop(); if (cell.isHidden) { cell.isHidden = false; cell.isUnknown = false; cell.isFlagged = false; if (cell.isBomb) { cell.isRevealedBomb = true; cell.value = -1; } else { cell.value = cell.bombValue; cell.isDigit = cell.value > 0; } if (cell.value === 0) { cell.neighbors.forEach((neighborCell) => { if (neighborCell.isHidden) { revealCells.push(neighborCell); } }); } } } } function setDigits(cells) { cells.forEach((cell) => { if (!cell.isBomb) { let bombNeighborAmount = cell.neighbors.reduce((a, b) => a + (b.isBomb ? 1 : 0), 0); cell.bombValue = bombNeighborAmount; } }); } function setBomb(cells, bombAmount) { if (bombAmount < 1) { return; } let chosenIndex = getRandomInt(cells.length); let chosenCell = cells[chosenIndex]; chosenCell.isBomb = true; } function getRandomInt(max) { return Math.floor(window.getRandom() * Math.floor(max)); } function getViableBombCells(cells, cell) { return cells.filter((c) => !(c === cell || cell.neighbors.includes(c))); } function getCellsFromField(field) { return field.reduce((rowA, rowB) => rowA.concat( rowB.reduce((cellA, cellB) => cellA.concat(cellB), []), [] ) ); } } function setWindowSeedRng() { if (!window.seedRng) { window.seedRng = null; window.setSeed = (seed) => (window.seedRng = seed ? new Math.seedrandom(seed) : null); window.getRandom = () => (window.seedRng ? window.seedRng() : Math.random()); } } function restartVirtualGame(config) { setWindowSeedRng(); window.virtualGame = createVirtualGame(config.width, config.height, config.bombAmount); window.virtualGame.field = createVirtualField(window.virtualGame); function createVirtualGame(width, height, bombAmout) { return { width: width, height: height, bombAmount: bombAmout, hasStarted: false }; } function createVirtualField(virtualGame) { let field = []; let createVirtualCell = (x, y) => { return { x: x, y: y, isHidden: true, isUnknown: true, isRevealedBomb: false, value: -1 }; }; for (let y = 0; y < virtualGame.height; y++) { let row = []; for (let x = 0; x < virtualGame.width; x++) { row.push(createVirtualCell(x, y)); } field.push(row); } applyToCells(field, (cell) => { let neighbors = []; applyToNeighbors(field, cell, (neighborCell) => neighbors.push(neighborCell)); cell.neighbors = neighbors; }); return field; } } function getVirtualGame(config) { if (!window.virtualGame) { restartVirtualGame(config); } return { field: window.virtualGame.field, bombAmount: window.virtualGame.bombAmount }; } function sweepPage(withGuessing = true, doLog = true, config = null, stats = null) { let field; let bombAmount; let isVirtualMode = config ? config.isVirtualMode : false; if (isVirtualMode) { let virtualGame = getVirtualGame(config.virtualGameConfig); field = virtualGame.field; bombAmount = virtualGame.bombAmount; } else { field = initializeField(); bombAmount = getBombAmount(); } if (config && stats) { let sweepT0 = performance.now(); let sweepResult = sweep(field, bombAmount, withGuessing, doLog); recordGameStats(config, stats, field, sweepResult, sweepT0); return sweepResult; } else { return sweep(field, bombAmount, withGuessing, doLog); } function recordGameStats(config, stats, field, sweepResult, sweepT0) { let sweepT1 = performance.now(); let sweepTime = sweepT1 - sweepT0; let gameIndex = config.state.gameIndex; if (!stats.gameStats[gameIndex]) { stats.gameStats[gameIndex] = { index: gameIndex }; stats.gameStats[gameIndex].stepStats = []; } let gameStats = stats.gameStats[gameIndex]; if (!gameStats.finishState) { gameStats.stepStats.push({ result: { state: sweepResult.state, solver: sweepResult.solver }, time: sweepTime }); if (isNewGameState(sweepResult.state)) { gameStats.finishState = sweepResult.state; gameStats.bombArray = getBombArray(field); let stepStats = gameStats.stepStats; let wasGuessStep = (step) => step.result.state === sweepStates.solving && isGuessingSolver(step.result.solver); let was3Step = (step) => step.result.state === sweepStates.solving && step.result.solver === "3"; gameStats.guesses = stepStats.reduce((a, b) => a + Number(wasGuessStep(b)), 0); gameStats.time = stepStats.reduce((a, b) => a + b.time, 0); gameStats.mostTimeStep = stepStats.reduce((a, b) => Math.max(a, b.time), 0); gameStats.mostTime3Step = stepStats.reduce((a, b) => Math.max(a, was3Step(b) ? b.time : 0), 0); if (!config.isRecordingStepStats) { delete gameStats.stepStats; } } } } function initializeField() { const openClass = "square open"; const flagClass = "square bombflagged"; const bombRevealedClass = "square bombrevealed"; const bombDeathClass = "square bombdeath"; let field = []; let y = 0; let x = 0; while (true) { let row = []; while (true) { let jDiv = $("#" + (y + 1) + "_" + (x + 1)); if (jDiv.length < 1 || jDiv.css("display") === "none") { break; } let jDivClass = jDiv.attr("class"); let cell = { div: jDiv[0], x: x, y: y }; if (jDivClass.substr(0, openClass.length) === openClass) { let number = jDivClass.substr(openClass.length); cell.value = Number(number); cell.isDigit = cell.value > 0; } else if (jDivClass === bombRevealedClass || jDivClass === bombDeathClass) { cell.isRevealedBomb = true; } else { cell.isHidden = true; cell.value = -1; if (jDivClass === flagClass) { cell.isFlagged = true; } else { cell.isUnknown = true; } } row.push(cell); x += 1; } if (row.length < 1) { break; } y += 1; x = 0; field.push(row); } return field; } } function sweep(fieldToSweep, bombAmount, withGuessing = true, doLog = true) { let interactions = []; let checkResult = checkForAndAddInteractions(); let sweepResult = { interactions: interactions, state: checkResult.state, solver: checkResult.solver }; return sweepResult; function checkForAndAddInteractions() { let field = copyAndInitializeField(fieldToSweep); if (checkBombDeath(field)) { return onBombDeath(); } if (checkStart(field)) { return onStart(field); } if (checkSolved(field)) { return onSolved(); } if (checkTrivialFlags(field) || checkTrivialReveals(field)) { return onStandardSolving("0", "[0] Trivial cases"); } let borderCells = getBorderCells(field); if (checkSuffocations(borderCells)) { return onStandardSolving("1", "[1] Suffocations"); } if (checkDigitsFlagCombinations(borderCells)) { return onStandardSolving("2", "[2] Check digits flag combinations"); } let borderCellGroupings = getBorderCellGroupings(field); let outsideUnknowns = getOutsideUnknowns(field); let flagsLeft = getFlagsLeft(field); if (checkIsolatedUnknowns(borderCellGroupings)) { return onIsolatedUnknowns(outsideUnknowns, flagsLeft); } return checkCombinatorially(field, borderCellGroupings, outsideUnknowns, flagsLeft); } function checkCombinatorially(field, borderCellGroupings, outsideUnknowns, flagsLeft) { let resultInfo = checkAllValidCombinations(field, borderCellGroupings, outsideUnknowns, flagsLeft); if (resultInfo.certainResultFound) { return onCheckCombinatorially(resultInfo, null, sweepStates.solving); } if (withGuessing) { return onCheckCombinatorially(resultInfo, "guessing", sweepStates.solving); } return onCheckCombinatorially(resultInfo, "stuck", sweepStates.stuck); } function checkIsolatedUnknowns(borderCellGroupings) { return borderCellGroupings.length === 0; } function onIsolatedUnknowns(outsideUnknowns, flagsLeft) { let subMessages = []; let state = sweepStates.solving; let mode = null; if (flagsLeft === 0) { outsideUnknowns.forEach((outsideUnknown) => revealCell(outsideUnknown)); subMessages.push("Reveals found - no bombs left"); } else if (withGuessing) { revealCell(outsideUnknowns[0]); let percentage = ((flagsLeft / outsideUnknowns.length) * 100).toFixed(1) + "%"; subMessages.push("Reveal random cell (" + percentage + ")"); mode = "guessing"; } else { state = sweepStates.stuck; mode = "stuck"; } let message = "Check isolated unknowns"; return onTriState(message, "i", subMessages, mode, state); } function getOutsideUnknowns(field) { let outsideUnknowns = []; applyToCells(field, (cell) => { if (cell.isUnknown && !cell.isBorderCell) { cell.borderCellNeighborAmount = 0; cell.neighbors.forEach((neighbor) => { if (neighbor.isBorderCell) { cell.borderCellNeighborAmount += 1; } }); outsideUnknowns.push(cell); } }); return outsideUnknowns; } function getBorderCellGroupings(field) { let allBorderCells = getBorderCells(field); let borderCellGroupings = splitToBorderCellGroupingsAndSort(allBorderCells); return borderCellGroupings; } function splitToBorderCellGroupingsAndSort(borderCellLists) { let unknownsGroupings = findUnknownsGroupings(borderCellLists); let groupingCellLists = createCellLists(unknownsGroupings); sortCellListsUnknowns(groupingCellLists); sortCellLists(groupingCellLists); return groupingCellLists; function findUnknownsGroupings(borderCellLists) { let borderCells = borderCellLists.digits.concat(borderCellLists.unknowns); borderCells.forEach((cell) => (cell.groupingIndex = null)); let getFirstWithoutIndex = () => borderCells.find((borderCell) => borderCell.groupingIndex === null); let unknownsGroupings = []; let startCell = getFirstWithoutIndex(); while (startCell) { let unknownsGrouping = []; let index = unknownsGroupings.length; let addToGrouping = (cell) => { if (cell.groupingIndex === null) { cell.groupingIndex = index; unknownsGrouping.push(cell); cell.neighbors.forEach((neighbor) => addToGrouping(neighbor)); } }; addToGrouping(startCell); unknownsGrouping = unknownsGrouping.filter((cell) => !cell.isDigit); if (unknownsGrouping.length > 0) { unknownsGroupings.push(unknownsGrouping); } startCell = getFirstWithoutIndex(); } return unknownsGroupings; } function addDigitsToGroupings(groupings) { groupings.forEach((grouping) => { grouping.forEach((unknown) => { unknown.neighbors.forEach((digitNeighbor) => { if (!grouping.includes(digitNeighbor)) { grouping.push(digitNeighbor); } }); }); }); } function createCellLists(unknownsGroupings) { addDigitsToGroupings(unknownsGroupings); let cellLists = []; unknownsGroupings.forEach((grouping) => { cellLists.push({ digits: grouping.filter((c) => c.isDigit), unknowns: grouping.filter((c) => !c.isDigit) }); }); return cellLists; } function sortCellListsUnknowns(cellLists) { for (let i = 0; i < cellLists.length; i++) { let unknowns = cellLists[i].unknowns; let digits = cellLists[i].digits; unknowns.forEach((unknown) => (unknown.sortScore = null)); digits.forEach((digit) => { let flagsLeft = digit.value - digit.flaggedNeighborAmount; digit.valueForUnknowns = 1 / binomialCoefficient(digit.unknownNeighborAmount, flagsLeft); }); let digitsSorted = digits.sort((a, b) => -(a.valueForUnknowns - b.valueForUnknowns)); let sortScore = 0; digitsSorted.forEach((digit) => { digit.neighbors.forEach((unknown) => { if (unknowns.includes(unknown) && unknown.sortScore === null) { unknown.sortScore = sortScore; } }); sortScore += 1; }); cellLists[i].unknowns = unknowns.sort((a, b) => a.sortScore - b.sortScore); } } function sortCellLists(cellLists) { cellLists = cellLists.sort((a, b) => { let primary = a.unknowns.length - b.unknowns.length; return primary !== 0 ? primary : -(a.digits.length - b.digits.length); }); } } function createCheckResult(state, solver = null) { return { state: state, solver: solver }; } function onBombDeath() { log("[x] Bomb death"); return createCheckResult(sweepStates.death); } function log() { if (doLog) { console.log.apply(console, arguments); } } function onCheckCombinatorially(resultInfo, mode, resultState) { let message = "Check combinatorially"; let solver = "3"; return onTriState(message, solver, resultInfo.messages, mode, resultState); } function onTriState(message, solver, messages, mode, resultState) { if (mode !== null) { message += " - " + mode; solver += mode[0]; } let formatSolver = "[" + solver + "]"; log(formatSolver, message); messages.forEach((c) => { log("->", formatSolver, c); }); return createCheckResult(resultState, solver); } function onStandardSolving(solver, message) { log(message); return createCheckResult(sweepStates.solving, solver); } function onStart(field) { log("[s]", sweepStates.start); if (withGuessing) { revealFirst(field); } return createCheckResult(sweepStates.start); } function revealFirst(field) { let width = field[0].length; let height = field.length; let x = Math.floor(width / 2); let y = Math.floor(height / 2); revealCell(field[y][x]); } function onSolved() { log("[o]", sweepStates.solved); return createCheckResult(sweepStates.solved); } function checkTrivialReveals(field) { let revealsFound = false; applyToCells(field, (cell) => { if (cell.isDigit && cell.flaggedNeighborAmount === cell.value) { cell.neighbors.forEach((neighbor) => { if (neighbor.isUnknown) { revealCell(neighbor); revealsFound = true; } }); } }); return revealsFound; } function copyAndInitializeField(fieldToSweep) { let field = fieldToSweep.map((rowToSweep) => { let row = rowToSweep.map((cellToSweep) => { return { referenceCell: cellToSweep.referenceCell ?? cellToSweep, x: cellToSweep.x, y: cellToSweep.y, value: cellToSweep.value, isDigit: cellToSweep.isDigit, isRevealedBomb: cellToSweep.isRevealedBomb, isHidden: cellToSweep.isHidden, isFlagged: cellToSweep.isFlagged, isUnknown: cellToSweep.isUnknown }; }); return row; }); setCellNeighborInfo(field); return field; } function setCellNeighborInfo(field) { applyToCells(field, (cell) => { cell.neighbors = []; cell.unknownNeighborAmount = 0; cell.flaggedNeighborAmount = 0; applyToNeighbors(field, cell, (neighborCell) => { if (neighborCell.isUnknown) { cell.unknownNeighborAmount += 1; } else if (neighborCell.isFlagged) { cell.flaggedNeighborAmount += 1; } cell.neighbors.push(neighborCell); }); cell.hiddenNeighborAmount = cell.unknownNeighborAmount + cell.flaggedNeighborAmount; cell.neighborAmount = cell.neighbors.length; }); } function checkAllValidCombinations(field, borderCellGroupings, outsideUnknowns, totalFlagsLeft) { let resultInfo = { certainResultFound: false, messages: [] }; let candidateAmount = borderCellGroupings.reduce((a, b) => a + b.unknowns.length, 0); resultInfo.messages.push("Candidate amount: " + candidateAmount); let groupingCheckResults = []; let leastBombsCount = 0; let checkAllCombinationsT0 = performance.now(); for (let i = 0; i < borderCellGroupings.length; i++) { let groupingFlagsLeft = totalFlagsLeft - leastBombsCount; let searchResult = checkGrouping(borderCellGroupings[i], groupingFlagsLeft); if (searchResult.certainResultFound) { resultInfo.certainResultFound = true; break; } leastBombsCount += searchResult.leastBombs ? searchResult.leastBombs : 0; groupingCheckResults.push(searchResult); } if (!resultInfo.certainResultFound) { let combinedCheckResult = mergeGroupingsCombinationsAndCheck(groupingCheckResults); if (combinedCheckResult.certainResultFound) { resultInfo.certainResultFound = true; } else { handleNoCertainResultFound(combinedCheckResult); } } let checkAllCombinationsT1 = performance.now(); let checkAllCombinationsTime = checkAllCombinationsT1 - checkAllCombinationsT0; resultInfo.messages.push("Check of all combinations took " + checkAllCombinationsTime.toFixed(4) + " milliseconds"); return resultInfo; function clusterCandidates(candidates) { let clusteredCandidateGroups = findClusteredGroups(candidates); let clusteredCandidates = createClusteredCandidates(clusteredCandidateGroups); return clusteredCandidates; function findClusteredGroups(cells) { let clusteredGroups = []; cells.forEach((cell) => { let belongsToCluster = false; clusteredGroups.forEach((cluster) => { let toCompare = cluster[0]; let isEqual = toCompare.neighbors.length === cell.neighbors.length; if (isEqual) { toCompare.neighbors.forEach((toCompareNeighbor) => { if (!cell.neighbors.includes(toCompareNeighbor)) { isEqual = false; } }); } if (isEqual) { belongsToCluster = true; cluster.push(cell); } }); if (!belongsToCluster) { clusteredGroups.push([cell]); } }); return clusteredGroups; } function createClusteredCandidates(clusteredCandidateGroups) { let clusteredCandidates = []; clusteredCandidateGroups.forEach((candidateGroup) => { let clusteredCandidate = candidateGroup[candidateGroup.length - 1]; clusteredCandidate.clusterGroup = candidateGroup; clusteredCandidate.clusterGroup.forEach((clusterCell) => { clusterCell.clusterSize = candidateGroup.length; }); clusteredCandidates.push(clusteredCandidate); }); return clusteredCandidates; } } function clusterDigitNeighbors(digits) { digits.forEach((digit) => (digit.neighbors = digit.neighbors.filter((c) => c.clusterGroup))); } function setupCandidatesForConditions(candidates) { candidates.forEach((candidate, i) => { candidate.assignIndex = i; candidate.conditions = []; candidate.conditionedPeers = []; candidate.conditionAmount = 0; candidate.conditionValue = 0; }); } function getValidCombinationsForNeighbors(neighbors, flagsLeft) { let validCombinations = []; let combination = Array(neighbors.length).fill(0); let lastI = combination.length - 1; while (true) { combination[0] += 1; for (let i = 0; i < lastI; i++) { if (combination[i] > neighbors[i].clusterSize) { combination[i] = 0; combination[i + 1] += 1; } else { break; } } if (combination[lastI] > neighbors[lastI].clusterSize) { break; } if (combination.reduce((a, b) => a + b, 0) === flagsLeft) { validCombinations.push(combination.slice(0)); } } return validCombinations; } function addCandidateConditions(candidates, digits) { clusterDigitNeighbors(digits); setupCandidatesForConditions(candidates); digits.forEach((digit) => { let flagsLeft = digit.value - digit.flaggedNeighborAmount; let neighbors = digit.neighbors; let validCombinations = getValidCombinationsForNeighbors(neighbors, flagsLeft); neighbors.forEach((neighbor) => { let condition = (assignment) => { for (let i = 0; i < validCombinations.length; i++) { let validCombination = validCombinations[i]; let contraintMet = true; for (let j = 0; j < validCombination.length; j++) { let value = assignment[neighbors[j].assignIndex]; if (value !== null && value !== validCombination[j]) { contraintMet = false; break; } } if (contraintMet) { return true; } } return false; }; addConditionedPeers(neighbor, neighbors); neighbor.conditions.push(condition); neighbor.conditionAmount += 1; neighbor.conditionValue += neighbors.reduce((a, b) => a + (b.clusterSize - 1) / (2 + a), 0); }); }); } function addConditionedPeers(candidate, peers) { peers.forEach((peer) => { if (peer !== candidate && !candidate.conditionedPeers.includes(peer)) { candidate.conditionedPeers.push(peer); } }); } function checkGrouping(grouping, groupingFlagsLeft) { let candidates = clusterCandidates(grouping.unknowns); addCandidateConditions(candidates, grouping.digits); let validCombinations = searchValidCombinations(candidates, groupingFlagsLeft); return searchCertainResult(candidates, validCombinations, groupingFlagsLeft); } function createRootAssignmentNode(candidates) { let rootAssignment = Array(candidates.length).fill(null); let rootLegalValues = Array(candidates.length).fill(null); for (let assignIndex = 0; assignIndex < rootAssignment.length; assignIndex++) { let legalValues = []; for (let assignValue = 0; assignValue <= candidates[assignIndex].clusterSize; assignValue++) { if (isValidAssignmentValue(rootAssignment, candidates, assignIndex, assignValue)) { legalValues.push(assignValue); } } rootLegalValues[assignIndex] = legalValues; } return { assignment: rootAssignment, legalValues: rootLegalValues, flagAmount: 0 }; } function searchValidCombinations(candidates, flagsLeft) { let validAssignNodes = [createRootAssignmentNode(candidates)]; let validCombinations = []; while (validAssignNodes.length > 0) { let assignNode = validAssignNodes.pop(); let newNodesAreLeafs = getUnassignedAmount(assignNode.assignment) === 1; let newNodes = searchAssignNode(candidates, assignNode, flagsLeft, newNodesAreLeafs); let associatedArray = newNodesAreLeafs ? validCombinations : validAssignNodes; newNodes.forEach((newNode) => associatedArray.push(newNode)); } return validCombinations; } function searchAssignNode(candidates, sourceNode, flagsLeft, newNodesAreLeafs) { let newNodes = []; let nextValueToSet = findBestValueToSet(candidates, sourceNode); nextValueToSet.legalValues.forEach((legalValue) => { let newAssignment = createArrayWithAssignment(sourceNode.assignment, nextValueToSet.index, legalValue); let newFlagAmount = sourceNode.flagAmount + legalValue; if (newFlagAmount <= flagsLeft) { if (newNodesAreLeafs) { newNodes.push({ values: newAssignment, flagAmount: newFlagAmount }); } else { let validChildNode = findValidChildNode(candidates, sourceNode, newAssignment, newFlagAmount, nextValueToSet); if (validChildNode) { newNodes.push(validChildNode); } } } }); return newNodes; } function getNewLegalValues(candidates, sourceNode, assignment, valueToSet) { let conditionedPeers = candidates[valueToSet.index].conditionedPeers; let newLegalValues = createArrayWithAssignment(sourceNode.legalValues, valueToSet.index, null); for (let peerI = 0; peerI < conditionedPeers.length; peerI++) { let peerIndex = conditionedPeers[peerI].assignIndex; if (newLegalValues[peerIndex] !== null) { let previousLegalValues = newLegalValues[peerIndex]; let legalValues = []; previousLegalValues.forEach((value) => { if (isValidAssignmentValue(assignment, candidates, peerIndex, value)) { legalValues.push(value); } }); if (legalValues.length > 0) { newLegalValues[peerIndex] = legalValues; } else { return null; } } } return newLegalValues; } function findValidChildNode(candidates, sourceNode, assignment, flagAmount, valueToSet) { let newLegalValues = getNewLegalValues(candidates, sourceNode, assignment, valueToSet); if (newLegalValues) { return createAssignNode(assignment, newLegalValues, flagAmount); } } function findBestValueToSet(candidates, sourceNode) { let indexOfBest; let legalValuesOfBest = null; for (let i = 0; i < sourceNode.assignment.length; i++) { if (sourceNode.assignment[i] === null) { let legalValues = sourceNode.legalValues[i]; if (legalValuesOfBest === null || isBetterValueToSet(legalValues, candidates[i], legalValuesOfBest, candidates[indexOfBest])) { legalValuesOfBest = legalValues; indexOfBest = i; } } } return { index: indexOfBest, legalValues: legalValuesOfBest }; } function isBetterValueToSet(legalValues, candidate, bestLegalValues, bestCandidate) { if (legalValues.length < bestLegalValues.length) { return true; } if (legalValues.length === bestLegalValues.length) { if (candidate.conditionAmount > bestCandidate.conditionAmount) { return true; } if (candidate.conditionAmount === bestCandidate.conditionAmount) { if (candidate.conditionValue < bestCandidate.conditionValue) { return true; } } } return false; } function createAssignNode(assignment, legalValues, flagAmount = 0) { return { assignment: assignment, legalValues: legalValues, flagAmount: flagAmount }; } function createArrayWithAssignment(assignment, index, value) { let createdArray = assignment.slice(0); createdArray[index] = value; return createdArray; } function isValidAssignmentValue(assignment, candidates, index, value) { let testAssignment = createArrayWithAssignment(assignment, index, value); return isValidAssignmentChange(testAssignment, candidates, index); } function getUnassignedAmount(assignment) { return assignment.reduce((a, b) => a + (b === null ? 1 : 0), 0); } function isValidAssignmentChange(assignment, candidates, changedIndex) { let isValid = true; let conditions = candidates[changedIndex].conditions; for (let i = 0; i < conditions.length; i++) { let condition = conditions[i]; if (!condition(assignment)) { isValid = false; break; } } return isValid; } function getLeastFlagAmount(validCombinations) { let leastFlags = Number.MAX_VALUE; validCombinations.forEach((c) => (leastFlags = c.flagAmount < leastFlags ? c.flagAmount : leastFlags)); return leastFlags; } function searchCertainResult(candidates, validCombinations, groupingFlagsLeft) { let leastBombs = getLeastFlagAmount(validCombinations); let noBombsLeftForRest = searchNoBombsLeftForRest(leastBombs, groupingFlagsLeft); candidates.forEach((candidate, candidateI) => { candidate.isCertainReveal = true; candidate.isCertainFlag = true; for (let i = 0; i < validCombinations.length; i++) { let value = validCombinations[i].values[candidateI]; if (value !== 0) { candidate.isCertainReveal = false; } if (value !== candidate.clusterSize) { candidate.isCertainFlag = false; } if (!candidate.isCertainReveal && !candidate.isCertainFlag) { break; } } }); let anyInteraction = searchCertainInteraction(candidates); let certainResultFound = noBombsLeftForRest || anyInteraction; return { certainResultFound: certainResultFound, validCombinations: validCombinations, candidates: candidates, leastBombs: leastBombs }; } function searchCertainResultForSummaries(candidates, mergedSummaries) { let totalSummary = { values: Array(candidates.length).fill(0), mergedCount: 0 }; let leastBombs = Number.MAX_VALUE; let mostBombs = 0; mergedSummaries.forEach((summary) => { summary.values.forEach((value, i) => (totalSummary.values[i] += value)); totalSummary.mergedCount += summary.mergedCount; leastBombs = summary.flagAmount < leastBombs ? summary.flagAmount : leastBombs; mostBombs = summary.flagAmount > mostBombs ? summary.flagAmount : mostBombs; }); let noBombsLeftForRest = searchNoBombsLeftForRest(leastBombs, totalFlagsLeft); let allBombsOnRest = searchAllBombsOnRest(mostBombs, totalFlagsLeft); candidates.forEach((candidate, candidateI) => { let value = totalSummary.values[candidateI]; candidate.isCertainReveal = value === 0; candidate.isCertainFlag = value === totalSummary.mergedCount * candidate.clusterSize; }); let anyInteraction = searchCertainInteraction(candidates); let certainResultFound = noBombsLeftForRest || anyInteraction || allBombsOnRest; return { certainResultFound: certainResultFound, mergedSummaries: mergedSummaries, candidates: candidates, leastBombs: leastBombs }; } function searchCertainInteraction(candidates) { let anyCertainReveal = false; let anyCertainFlag = false; candidates.forEach((candidate) => { if (candidate.isCertainReveal) { if (!anyCertainReveal) { resultInfo.messages.push("Reveals found - no bomb in any valid combination"); anyCertainReveal = true; } candidate.clusterGroup.forEach((clusterCell) => revealCell(clusterCell)); } else if (candidate.isCertainFlag) { if (!anyCertainFlag) { resultInfo.messages.push("Flags found - bomb in every valid combination"); anyCertainFlag = true; } candidate.clusterGroup.forEach((clusterCell) => flagCell(clusterCell)); } }); return anyCertainReveal || anyCertainFlag; } function searchNoBombsLeftForRest(leastBombs, flagsLeft) { let noBombsLeftForRest = leastBombs === flagsLeft && outsideUnknowns.length > 0; if (noBombsLeftForRest) { resultInfo.messages.push("Reveals found - no bombs left for non candidates"); applyToCells(field, (cell) => { if (cell.isHidden && !cell.isFlagged && !cell.isBorderCell) { revealCell(cell); } }); } return noBombsLeftForRest; } function searchAllBombsOnRest(mostBombs, flagsLeft) { let remainingBombs = flagsLeft - mostBombs; let allBombsOnRest = outsideUnknowns.length > 0 && outsideUnknowns.length === remainingBombs; if (allBombsOnRest) { resultInfo.messages.push("Flags found - all bombs are on outside unknowns"); applyToCells(field, (cell) => { if (cell.isHidden && !cell.isFlagged && !cell.isBorderCell) { flagCell(cell); } }); } return allBombsOnRest; } function setCheckResultSummaries(groupingCheckResults) { groupingCheckResults.forEach((checkResult) => { let summaries = {}; let summaryList = []; checkResult.validCombinations.forEach((comb) => { let summary; if (summaries.hasOwnProperty(comb.flagAmount)) { summary = summaries[comb.flagAmount]; } else { summary = { values: Array(comb.values.length).fill(0), flagAmount: comb.flagAmount, mergedCount: 0 }; summaries[comb.flagAmount] = summary; summaryList.push(summary); } let occurenceCount = 1; comb.values.forEach((value, i) => (occurenceCount *= binomialCoefficient(checkResult.candidates[i].clusterSize, value))); summary.mergedCount += occurenceCount; comb.values.forEach((value, i) => (summary.values[i] += value * occurenceCount)); }); checkResult.summaries = summaryList; }); } function mergeGroupingsCombinationsAndCheck(groupingCheckResults) { setCheckResultSummaries(groupingCheckResults); let mergedSummaries = []; groupingCheckResults.forEach((checkResult) => { if (mergedSummaries.length > 0) { let newSummaries = {}; let newSummaryList = []; mergedSummaries.forEach((rootSummary) => { checkResult.summaries.forEach((leafSummary) => { let newFlagAmount = rootSummary.flagAmount + leafSummary.flagAmount; if (newFlagAmount <= totalFlagsLeft) { let rootValues = rootSummary.values.slice(0); for (let i = 0; i < rootValues.length; i++) { rootValues[i] *= leafSummary.mergedCount; } let leafValues = leafSummary.values.slice(0); for (let i = 0; i < leafValues.length; i++) { leafValues[i] *= rootSummary.mergedCount; } let newValues = rootValues.concat(leafValues); let newMergedCount = rootSummary.mergedCount * leafSummary.mergedCount; let newSummary = { values: newValues, flagAmount: newFlagAmount, mergedCount: newMergedCount }; if (newSummaries.hasOwnProperty(newFlagAmount)) { let baseSummary = newSummaries[newFlagAmount]; baseSummary.mergedCount += newSummary.mergedCount; newSummary.values.forEach((value, i) => (baseSummary.values[i] += value)); } else { newSummaries[newFlagAmount] = newSummary; newSummaryList.push(newSummary); } } }); }); mergedSummaries = newSummaryList; } else { mergedSummaries = checkResult.summaries; } }); mergedSummaries = mergedSummaries.filter((summary) => summary.flagAmount + outsideUnknowns.length >= totalFlagsLeft); let mergedCandidates = []; groupingCheckResults.forEach((checkResult) => (mergedCandidates = mergedCandidates.concat(checkResult.candidates))); return searchCertainResultForSummaries(mergedCandidates, mergedSummaries, totalFlagsLeft); } function calculateCandidateCellProbs(checkResult) { let candidates = checkResult.candidates; let candidateAmount = candidates.reduce((a, b) => a + b.clusterSize, 0); let unknownAmount = outsideUnknowns.length + candidateAmount; let combinationProbs = checkResult.mergedSummaries; combinationProbs.forEach((prob) => { let distribution = approxHypergeometricDistribution(unknownAmount, totalFlagsLeft, candidateAmount, prob.flagAmount); prob.weight = (prob.mergedCount * distribution) / binomialCoefficient(candidateAmount, prob.flagAmount); for (let i = 0; i < prob.values.length; i++) { prob.values[i] /= prob.mergedCount * candidates[i].clusterSize; } }); let candidateValues = calculateCandidateValues(combinationProbs, candidates); let cellProbs = convertToCellProbs(candidateValues, candidates); return cellProbs; } function convertToCellProbs(candidateValues, candidates) { let cellProbs = []; candidateValues.forEach((value, i) => { candidates[i].clusterGroup.forEach((groupCell) => { cellProbs.push({ percentage: (value * 100).toFixed(2) + "%", fraction: value, candidate: groupCell, clusterRoot: candidates[i] }); }); }); return cellProbs; } function calculateCandidateValues(combinationProbs, candidates) { let totalWeight = 0; combinationProbs = combinationProbs.sort((a, b) => a.weight - b.weight); combinationProbs.forEach((prob) => (totalWeight += prob.weight)); let candidateValues = new Array(candidates.length).fill(0); combinationProbs.forEach((prob) => { prob.values.forEach((value, i) => (candidateValues[i] += value * (prob.weight / totalWeight))); }); return candidateValues; } function calculateOutsiderCellProb(cellProbs) { if (outsideUnknowns.length === 0) { return null; } let averageFlagsInBorder = 0; cellProbs.forEach((cellProb) => (averageFlagsInBorder += cellProb.fraction)); let averageFlagsLeftOutside = totalFlagsLeft - averageFlagsInBorder; let outsideUnknownsFraction = averageFlagsLeftOutside / outsideUnknowns.length; outsideUnknowns.forEach((outsider) => { let probabilityOfZero = 1; outsider.neighbors.forEach((neighbor) => { let probabilityOfBomb; if (neighbor.isBorderCell && neighbor.isUnknown) { probabilityOfBomb = cellProbs.find((cellProb) => { return cellProb.candidate.x === neighbor.x && cellProb.candidate.y === neighbor.y; }).fraction; } else if (neighbor.isFlagged) { probabilityOfBomb = 1; } else if (outsideUnknowns.includes(neighbor)) { probabilityOfBomb = outsideUnknownsFraction; } probabilityOfZero *= 1 - probabilityOfBomb; }); outsider.probabilityOfZero = probabilityOfZero; }); outsideUnknowns = outsideUnknowns.sort((a, b) => -(a.probabilityOfZero - b.probabilityOfZero)); let outsiderCandidate = outsideUnknowns[0]; return { percentage: (outsideUnknownsFraction * 100).toFixed(2) + "%", fraction: outsideUnknownsFraction, candidate: outsiderCandidate, isOutsider: true }; } function handleNoCertainResultFound(checkResult) { let cellProbs = calculateCandidateCellProbs(checkResult); let outsider = calculateOutsiderCellProb(cellProbs); evaluateCellProbs(cellProbs, outsider); } function setCellProbScoresAndSort(cellProbs) { cellProbs.forEach((c) => (c.score = c.fraction)); let sorted = cellProbs.sort((a, b) => a.score - b.score); return sorted; } function validateCellProbs(cellProbs) { cellProbs.forEach((cellProb) => { if (cellProb.fraction <= 0 || cellProb.fraction >= 1) { throw new Error("Impossible fraction found in uncertain mode!"); } }); } function evaluateCellProbs(candidateCellProbs, outsider) { let cellProbs = createCellProbsWithOutsider(candidateCellProbs, outsider); validateCellProbs(cellProbs); cellProbs = setCellProbScoresAndSort(cellProbs); if (withGuessing && cellProbs.length > 0) { let bestScoreCellProb = cellProbs[0]; resultInfo.messages.push("Reveal lowest score cell (" + bestScoreCellProb.percentage + ")"); revealCell(bestScoreCellProb.candidate); } else { resultInfo.messages.push("No certain cell found"); resultInfo.messages.push("Candidates with percentages:"); let counter = 1; let placing = 1; let lastCellProb = null; cellProbs.forEach((cellProb) => { if (lastCellProb && cellProb.fraction > lastCellProb.fraction) { placing = counter; } cellProb.placing = placing; lastCellProb = cellProb; counter += 1; }); cellProbs.forEach((cellProb) => { let message = "#" + cellProb.placing + " " + formatCellProbCoords(cellProb) + ": " + cellProb.percentage + " (Score: " + (cellProb.score * 100).toFixed(3) + ")"; if (cellProb.isOutsider) { message += " - Outsider"; } else if (cellProb.candidate.clusterSize > 1) { message += " - Cluster"; } resultInfo.messages.push(message); if (cellProb.candidate.referenceCell.div) { resultInfo.messages.push(cellProb.candidate.referenceCell.div); } else { resultInfo.messages.push(cellProb.candidate.referenceCell); } }); } function formatCellProbCoords(cellProb) { let cell = cellProb.candidate; return "(" + (cell.y + 1) + "_" + (cell.x + 1) + ")"; } } function createCellProbsWithOutsider(candidateCellProbs, outsider) { let allCellProbs = candidateCellProbs.slice(0); if (outsider) { allCellProbs.push(outsider); } return allCellProbs; } } function getFlagsLeft(field) { let flagsAmount = getFlagsAmount(field); let flagsLeft = bombAmount - flagsAmount; return flagsLeft; } function getFlagsAmount(field) { let flagsAmount = 0; applyToCells(field, (cell) => { if (cell.isFlagged) { flagsAmount += 1; } }); return flagsAmount; } function checkBombDeath(field) { return !trueForAllCells(field, (cell) => !cell.isRevealedBomb); } function checkStart(field) { return trueForAllCells(field, (cell) => cell.isHidden); } function checkSolved(field) { return getHiddenAmount(field) === bombAmount; } function getHiddenAmount(field) { return field.reduce((rowA, rowB) => rowA + rowB.reduce((cellA, cellB) => cellA + (cellB.isHidden ? 1 : 0), 0), 0); } function trueForAllCells(field, condition) { let trueForAll = true; applyToCells(field, (cell) => { if (!condition(cell)) { trueForAll = false; return "break"; } }); return trueForAll; } function getBinaryAssignments(valueAmount, amountOfOnes) { let binaryAssignments = []; let assignment = Array(valueAmount).fill(0); let lastI = assignment.length - 1; while (true) { for (let i = 0; i < lastI; i++) { if (assignment[i] > 1) { assignment[i] = 0; assignment[i + 1] += 1; } else { break; } } if (assignment[lastI] > 1) { break; } if (amountOfOnes === null || assignment.reduce((a, b) => a + b, 0) === amountOfOnes) { binaryAssignments.push(assignment.slice(0)); } assignment[0] += 1; } return binaryAssignments; } function checkDigitsFlagCombinations(borderCells) { let interactionFound = false; let digits = borderCells.digits; digits.forEach((digit) => { let freeSpots = digit.neighbors; let flagAmount = digit.value - digit.flaggedNeighborAmount; let assignments = getBinaryAssignments(freeSpots.length, flagAmount); let validAssignments = []; assignments.forEach((assignment) => { let assignmentValid = true; let flaggedNeighborCounts = {}; for (let i = 0; i < assignment.length && assignmentValid; i++) { if (assignment[i] === 0) { continue; } let freeSpot = freeSpots[i]; freeSpot.neighbors.forEach((digitNeighbor) => { if (assignmentValid && digitNeighbor !== digit) { if (digitNeighbor.value < digitNeighbor.flaggedNeighborAmount + 1) { assignmentValid = false; } else { let index = getCellCoords(digitNeighbor); if (flaggedNeighborCounts.hasOwnProperty(index)) { flaggedNeighborCounts[index] += 1; if (digitNeighbor.value < digitNeighbor.flaggedNeighborAmount + flaggedNeighborCounts[index]) { assignmentValid = false; } } else { flaggedNeighborCounts[index] = 1; } } } }); } if (assignmentValid) { validAssignments.push(assignment); } }); if (validAssignments.length === 0) { throw new Error("No valid assignments for digit."); } let validAssignmentsSum = Array(freeSpots.length).fill(0); validAssignments.forEach((assign) => assign.forEach((value, i) => (validAssignmentsSum[i] += value))); validAssignmentsSum.forEach((assignmentSum, i) => { if (assignmentSum === 0) { revealCell(freeSpots[i]); interactionFound = true; } else if (assignmentSum === validAssignments.length) { flagCell(freeSpots[i]); interactionFound = true; } }); }); return interactionFound; } function getBorderCells(field) { let fieldBorderDigits = []; let borderDigits = []; applyToCells(field, (cell) => { if (cell.isDigit && cell.unknownNeighborAmount > 0) { cell.isBorderCell = true; fieldBorderDigits.push(cell); borderDigits.push(createBorderDigit(cell)); } }); let fieldBorderUnknowns = []; let borderUnknowns = []; fieldBorderDigits.forEach((fieldDigitBorderCell, i) => { fieldDigitBorderCell.neighbors.forEach((neighbor) => { if (neighbor.isUnknown) { if (!fieldBorderUnknowns.includes(neighbor)) { neighbor.isBorderCell = true; fieldBorderUnknowns.push(neighbor); let created = createBorderUnknown(neighbor); borderUnknowns.push(created); borderDigits[i].neighbors.push(created); created.neighbors.push(borderDigits[i]); } else { let indexOfUnknown = fieldBorderUnknowns.indexOf(neighbor); borderDigits[i].neighbors.push(borderUnknowns[indexOfUnknown]); borderUnknowns[indexOfUnknown].neighbors.push(borderDigits[i]); } } }); }); return { digits: borderDigits, unknowns: borderUnknowns }; } function createBorderUnknown(fieldBorderUnknown) { let borderCell = createBorderCell(fieldBorderUnknown); borderCell.isHidden = true; borderCell.isUnknown = true; borderCell.value = -1; return borderCell; } function createBorderDigit(fieldBorderDigit) { let borderCell = createBorderCell(fieldBorderDigit); borderCell.isDigit = true; borderCell.value = fieldBorderDigit.value; return borderCell; } function createBorderCell(fieldBorderCell) { return { referenceCell: fieldBorderCell.referenceCell, x: fieldBorderCell.x, y: fieldBorderCell.y, unknownNeighborAmount: fieldBorderCell.unknownNeighborAmount, flaggedNeighborAmount: fieldBorderCell.flaggedNeighborAmount, neighbors: [] }; } function checkSuffocations(borderCells) { let suffocationsFound = false; let unknowns = borderCells.unknowns; unknowns.forEach((assumedFlag) => { let filledDigits = []; assumedFlag.neighbors.forEach((digitNeighbor) => { if (digitNeighbor.flaggedNeighborAmount + 1 === digitNeighbor.value) { filledDigits.push(digitNeighbor); } }); if (filledDigits.length === 0) { return; } let filledDigitsUnknownNeighbors = []; filledDigits.forEach((filledDigit) => { filledDigit.neighbors.forEach((unknownNeighbor) => { if (unknownNeighbor !== assumedFlag && !filledDigitsUnknownNeighbors.includes(unknownNeighbor)) { filledDigitsUnknownNeighbors.push(unknownNeighbor); } }); }); if (filledDigitsUnknownNeighbors.length === 0) { return; } let suffocationFound = false; let suffocateCounts = {}; filledDigitsUnknownNeighbors.forEach((unknownNeighbor) => { unknownNeighbor.neighbors.forEach((digitToSuffocate) => { if (!suffocationFound && !filledDigits.includes(digitToSuffocate)) { let flagsLeft = digitToSuffocate.value - digitToSuffocate.flaggedNeighborAmount; if (flagsLeft > digitToSuffocate.unknownNeighborAmount - 1) { suffocationFound = true; } else { let index = getCellCoords(digitToSuffocate); if (suffocateCounts.hasOwnProperty(index)) { suffocateCounts[index] += 1; if (flagsLeft > digitToSuffocate.unknownNeighborAmount - suffocateCounts[index]) { suffocationFound = true; } } else { suffocateCounts[index] = 1; } } } }); }); if (suffocationFound) { revealCell(assumedFlag); suffocationsFound = true; } }); return suffocationsFound; } function getCellCoords(cell) { return cell.x + "-" + cell.y; } function checkTrivialFlags(field) { let flagsFound = false; applyToCells(field, (cell) => { if (cell.isDigit && cell.hiddenNeighborAmount === cell.value && cell.flaggedNeighborAmount !== cell.value) { cell.neighbors.forEach((neighborCell) => { if (neighborCell.isUnknown) { flagCell(neighborCell); flagsFound = true; } }); } }); return flagsFound; } function revealCell(cell) { addInteraction(cell.referenceCell, false); } function flagCell(cell) { addInteraction(cell.referenceCell, true); } function addInteraction(referenceCell, isFlag) { let duplicate = interactions.find((c) => c.cell === referenceCell && c.isFlag === isFlag); if (!duplicate) { interactions.push({ cell: referenceCell, isFlag: isFlag }); } } } function applyToNeighbors(matrix, cell, action) { for (let yOffset = -1; yOffset <= 1; yOffset++) { for (let xOffset = -1; xOffset <= 1; xOffset++) { if (yOffset === 0 && xOffset === 0) { continue; } let y = cell.y + xOffset; let x = cell.x + yOffset; if (y >= 0 && y < matrix.length && x >= 0 && x < matrix[cell.y].length) { let isBreak = action(matrix[y][x]) === "break"; if (isBreak) { return; } } } } } function applyToCells(matrix, action) { for (let y = 0; y < matrix.length; y++) { for (let x = 0; x < matrix[y].length; x++) { let isBreak = action(matrix[y][x]) === "break"; if (isBreak) { return; } } } } function simulate(element, eventName, mouseButton) { let eventMatchers = { HTMLEvents: /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, MouseEvents: /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/ }; let defaultOptions = { pointerX: 0, pointerY: 0, button: mouseButton ? mouseButton : 0, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, bubbles: true, cancelable: true }; let options = extend(defaultOptions, arguments[2] || {}); let oEvent, eventType = null; for (let name in eventMatchers) { if (eventMatchers[name].test(eventName)) { eventType = name; break; } } if (!eventType) { throw new SyntaxError("Only HTMLEvents and MouseEvents interfaces are supported"); } if (document.createEvent) { oEvent = document.createEvent(eventType); if (eventType === "HTMLEvents") { oEvent.initEvent(eventName, options.bubbles, options.cancelable); } else { oEvent.initMouseEvent( eventName, options.bubbles, options.cancelable, document.defaultView, options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element ); } element.dispatchEvent(oEvent); } else { options.clientX = options.pointerX; options.clientY = options.pointerY; let evt = document.createEventObject(); oEvent = extend(evt, options); element.fireEvent("on" + eventName, oEvent); } return element; function extend(destination, source) { for (let property in source) { destination[property] = source[property]; } return destination; } } function approxHypergeometricDistribution(N, M, n, k) { return n * 20 <= N ? binomialDistribution(n, M / N, k) : hypergeometricDistribution(N, M, n, k); } function hypergeometricDistribution(N, M, n, k) { return (binomialCoefficient(M, k) * binomialCoefficient(N - M, n - k)) / binomialCoefficient(N, n); } function binomialDistribution(n, p, k) { return binomialCoefficient(n, k) * Math.pow(p, k) * Math.pow(1 - p, n - k); } function binomialCoefficient(n, k) { let coefficient = 1; if (k > n - k) { k = n - k; } for (let i = 0; i < k; ++i) { coefficient *= n - i; coefficient /= i + 1; } return coefficient; }