From 97a94a72139d32894f44757d5eed521eefac0ed0 Mon Sep 17 00:00:00 2001 From: Nice Zombies Date: Wed, 29 Jan 2025 11:02:59 +0100 Subject: [PATCH] Early 50/50 checking (#24) * Early 50/50 checking * Find 50/50 loops * Add early 50/50 setting * Always show dead tiles * Disable full BFDA for hint refresh * No pseudos * Use full probability * Ignore dead tiles * Modern bulk run --------- Co-authored-by: Wannes Boeykens --- Minesweeper/client/BruteForceAnalysis.js | 6 +- Minesweeper/client/main.js | 72 ++++++++++------- Minesweeper/client/solver_main.js | 62 ++++++++++++++- .../client/solver_probability_engine.js | 79 ++++++++++++++----- index.html | 3 +- 5 files changed, 166 insertions(+), 56 deletions(-) diff --git a/Minesweeper/client/BruteForceAnalysis.js b/Minesweeper/client/BruteForceAnalysis.js index ca28839..6174626 100644 --- a/Minesweeper/client/BruteForceAnalysis.js +++ b/Minesweeper/client/BruteForceAnalysis.js @@ -214,7 +214,7 @@ class BruteForceAnalysis { alive.zeroSolutions = valueCount[0]; living.push(alive); } else { - console.log(BruteForceGlobal.allTiles[i].asText() + " is dead with value " + minValue); + this.writeToConsole(BruteForceGlobal.allTiles[i].asText() + " is dead with value " + minValue); this.deadTiles.push(BruteForceGlobal.allTiles[i]); // store the dead tiles } @@ -242,7 +242,7 @@ class BruteForceAnalysis { //solver.display("first best move is " + loc.display()); const prob = 1 - (bestLiving.mineCount / this.currentNode.getSolutionSize()); - console.log("mines = " + bestLiving.mineCount + " solutions = " + this.currentNode.getSolutionSize()); + this.writeToConsole("mines = " + bestLiving.mineCount + " solutions = " + this.currentNode.getSolutionSize()); for (let i = 0; i < bestLiving.children.length; i++) { if (bestLiving.children[i] == null) { //solver.display("Value of " + i + " is not possible"); @@ -255,7 +255,7 @@ class BruteForceAnalysis { } else { probText = bestLiving.children[i].getProbability(); } - console.log("Value of " + i + " leaves " + bestLiving.children[i].getSolutionSize() + " solutions and winning probability " + probText + " (work size " + bestLiving.children[i].work + ")"); + this.writeToConsole("Value of " + i + " leaves " + bestLiving.children[i].getSolutionSize() + " solutions and winning probability " + probText + " (work size " + bestLiving.children[i].work + ")"); } const action = new Action(loc.getX(), loc.getY(), prob, ACTION_CLEAR); diff --git a/Minesweeper/client/main.js b/Minesweeper/client/main.js index d72d6dd..8aa14fb 100644 --- a/Minesweeper/client/main.js +++ b/Minesweeper/client/main.js @@ -344,8 +344,10 @@ async function startup() { board.setStarted(); } - //bulkRun(21, 12500); // seed '21' Played 12500 won 5192 - //bulkRun(321, 10000); // seed 321 played 10000 won 4142 + //bulkRun(21, 12500, false); // seed '21' Played 12500 won 5192 + //bulkRun(321, 10000, false); // seed 321 played 10000 won 4142 + //bulkRun(0, 1000, false); // classic: seed '0' Won 424/1000 (42.40%) + //bulkRun(0, 1000, true); // modern: seed '0' Won 546/1000 (54.60%) showMessage("Welcome to minesweeper solver dedicated to Annie"); } @@ -428,6 +430,7 @@ function propertiesClose() { BruteForceGlobal.PRUNE_BF_ANALYSIS = document.getElementById("pruneBruteForce").checked; + SolverGlobal.EARLY_FIFTY_FIFTY_CHECKING = document.getElementById("early5050").checked; SolverGlobal.CALCULATE_LONG_TERM_SAFETY = document.getElementById("useLTR").checked; SolverGlobal.PRUNE_GUESSES = document.getElementById("pruneGuesses").checked; @@ -455,6 +458,7 @@ function propertiesOpen() { document.getElementById("pruneBruteForce").checked = BruteForceGlobal.PRUNE_BF_ANALYSIS; + document.getElementById("early5050").checked = SolverGlobal.EARLY_FIFTY_FIFTY_CHECKING; document.getElementById("useLTR").checked = SolverGlobal.CALCULATE_LONG_TERM_SAFETY; document.getElementById("pruneGuesses").checked = SolverGlobal.PRUNE_GUESSES; @@ -481,6 +485,7 @@ function saveSettings() { settings.version = SETTINGS_VERSION; settings.pruneBruteForce = BruteForceGlobal.PRUNE_BF_ANALYSIS; settings.pruneGuesses = SolverGlobal.PRUNE_GUESSES; + settings.early5050 = SolverGlobal.EARLY_FIFTY_FIFTY_CHECKING; settings.useLTR = SolverGlobal.CALCULATE_LONG_TERM_SAFETY; settings.maxAnalysisBfSolutions = BruteForceGlobal.ANALYSIS_BFDA_THRESHOLD; @@ -511,6 +516,10 @@ function loadSettings() { SolverGlobal.PRUNE_GUESSES = settings.pruneGuesses; } + if (settings.early5050 != null) { + SolverGlobal.EARLY_FIFTY_FIFTY_CHECKING = settings.early5050; + } + if (settings.useLTR != null) { SolverGlobal.CALCULATE_LONG_TERM_SAFETY = settings.useLTR; } @@ -996,12 +1005,14 @@ function showDownloadLink(show, url) { } -async function bulkRun(runSeed, size) { +async function bulkRun(runSeed, size, modern) { const options = {}; options.playStyle = PLAY_STYLE_NOFLAGS; options.verbose = false; options.advancedGuessing = true; + options.fullProbability = true; + options.hardcore = false; const startTime = Date.now(); @@ -1009,7 +1020,16 @@ async function bulkRun(runSeed, size) { let won = 0; const rng = JSF(runSeed); // create an RNG based on the seed - const startIndex = 0; + + let startIndex; + let gameType; + if (modern) { + startIndex = 93; + gameType = "zero" + } else { + startIndex = 0; + gameType = "safe" + } while (played < size) { @@ -1017,11 +1037,9 @@ async function bulkRun(runSeed, size) { const gameSeed = rng() * Number.MAX_SAFE_INTEGER; - console.log(gameSeed); + const game = new ServerGame(0, 30, 16, 99, startIndex, gameSeed, gameType); - const game = new ServerGame(0, 30, 16, 99, startIndex, gameSeed, "safe"); - - const board = new Board(0, 30, 16, 99, gameSeed, "safe"); + const board = new Board(0, 30, 16, 99, gameSeed, gameType); let tile = game.getTile(startIndex); @@ -1053,15 +1071,17 @@ async function bulkRun(runSeed, size) { } else { // otherwise we're trying to clear - tile = game.getTile(board.xy_to_index(action.x, action.y)); + if (i == 0 || action.prob == 1) { + tile = game.getTile(board.xy_to_index(action.x, action.y)); - revealedTiles = game.clickTile(tile); + revealedTiles = game.clickTile(tile); - if (revealedTiles.header.status != IN_PLAY) { // if won or lost nothing more to do - break; - } + if (revealedTiles.header.status != IN_PLAY) { // if won or lost nothing more to do + break; + } - applyResults(board, revealedTiles); + applyResults(board, revealedTiles); + } if (action.prob != 1) { // do no more actions after a guess break; @@ -1071,15 +1091,16 @@ async function bulkRun(runSeed, size) { } - console.log(revealedTiles.header.status); - if (revealedTiles.header.status == WON) { won++; } + const winPercentage = (won / played * 100).toFixed(2); + console.log("Won " + won + "/" + played + " (" + winPercentage + "%)"); + } - console.log("Played " + played + " won " + won); + console.log("Bulk run finished in " + (Date.now() - startTime) + " milliseconds") } async function playAgain() { @@ -1927,6 +1948,7 @@ async function replayForward(replayType) { options.fullProbability = true; options.advancedGuessing = false; options.verbose = false; + options.hardcore = false; let hints; let other; @@ -2139,6 +2161,7 @@ async function replayBackward(replayType) { options.fullProbability = true; options.advancedGuessing = false; options.verbose = false; + options.hardcore = false; let hints; let other; @@ -2267,14 +2290,10 @@ async function doAnalysis(fullBFDA) { options.playStyle = PLAY_STYLE_NOFLAGS_EFFICIENCY; } - if (docOverlay.value != "none") { - options.fullProbability = true; - } else { - options.fullProbability = false; - } - + options.fullProbability = true; options.guessPruning = guessAnalysisPruning; options.fullBFDA = fullBFDA; + options.hardcore = docHardcore.checked; const solve = await solver(board, options); // look for solutions const hints = solve.actions; @@ -3126,11 +3145,8 @@ async function handleSolver(solverStart, headerId) { options.playStyle = PLAY_STYLE_NOFLAGS_EFFICIENCY; } - if (docOverlay.value != "none" || docHardcore.checked) { - options.fullProbability = true; - } else { - options.fullProbability = false; - } + options.fullProbability = true; + options.hardcore = docHardcore.checked; let hints; let other; diff --git a/Minesweeper/client/solver_main.js b/Minesweeper/client/solver_main.js index d19e015..bc54b5e 100644 --- a/Minesweeper/client/solver_main.js +++ b/Minesweeper/client/solver_main.js @@ -20,6 +20,7 @@ const PLAY_STYLE_NOFLAGS_EFFICIENCY = 4; class SolverGlobal { static PRUNE_GUESSES = true; // Determines whether calculations continue after the tile can no longer be the best + static EARLY_FIFTY_FIFTY_CHECKING = true; // Determines whether 50/50 checking is done when there are safe tiles static CALCULATE_LONG_TERM_SAFETY = true; // Switches 50/50 influence processing on or off, also most pseudo-50/50 detection } @@ -241,7 +242,7 @@ async function solver(board, options) { // add any trivial moves we've found if (options.fullProbability || options.playStyle == PLAY_STYLE_EFFICIENCY || options.playStyle == PLAY_STYLE_NOFLAGS_EFFICIENCY) { - console.log("Skipping trivial analysis since Probability Engine analysis is required") + writeToConsole("Skipping trivial analysis since Probability Engine analysis is required") } else { result.push(...trivial_actions(board, witnesses)); } @@ -375,7 +376,62 @@ async function solver(board, options) { totalSafe++; } showMessage("The solver has found " + totalSafe + " safe files." + formatSolutions(pe.finalSolutionsCount)); - return new EfficiencyHelper(board, witnesses, witnessed, result, options.playStyle, pe, allCoveredTiles).process(); + result = new EfficiencyHelper(board, witnesses, witnessed, result, options.playStyle, pe, allCoveredTiles).process() + + if (!options.noGuessingMode) { + // See if there are any unavoidable 2 tile 50/50 guesses + if (SolverGlobal.EARLY_FIFTY_FIFTY_CHECKING && !options.hardcore && minesLeft > 1) { + //const unavoidable5050a = pe.checkForUnavoidable5050(); + let unavoidable5050a; + if (options.playStyle == PLAY_STYLE_EFFICIENCY || options.playStyle == PLAY_STYLE_NOFLAGS_EFFICIENCY) { + unavoidable5050a = pe.checkForUnavoidable5050(); + } else { + unavoidable5050a = pe.checkForUnavoidable5050OrPseudo(); + } + + if (unavoidable5050a != null) { + + const actions = []; + for (const tile of unavoidable5050a) { + // Check if the pseudo 50/50 isn't resolved by the local clears + if (tile.probability != 0 && tile.probability != 1) { + actions.push(new Action(tile.getX(), tile.getY(), tile.probability, ACTION_CLEAR)); + } + } + + if (actions.length != 0) { + const returnActions = tieBreak(pe, actions, null, null, false); + + const recommended = returnActions[0]; + result.unshift(...returnActions); + if (recommended.prob == 0.5) { + showMessage(recommended.asText() + " is an unavoidable 50/50 guess." + formatSolutions(pe.finalSolutionsCount)); + } else { + showMessage(recommended.asText() + " is an unavoidable 50/50 guess, or safe." + formatSolutions(pe.finalSolutionsCount)); + } + + // combine the dead tiles from the probability engine and the unavoidable 5050s + for (let deadTile of pe.deadTiles) { + let found = false; + for (let returnAction of returnActions) { + if (deadTile.isEqual(returnAction)) { + found = true; + break; + } + } + if (!found) { + deadTiles.push(deadTile); + } + } + + return addDeadTiles(result, deadTiles, pe.minesFound); + } + } + } + result = addDeadTiles(result, pe.getDeadTiles(), pe.minesFound); + } + + return result; } @@ -693,7 +749,7 @@ async function solver(board, options) { // identify the dead tiles for (let tile of deadTiles) { // show all dead tiles - if (tile.probability != 0) { + if (tile.probability != 0 && tile.probability != 1) { const action = new Action(tile.getX(), tile.getY(), tile.probability); action.dead = true; result.push(action); diff --git a/Minesweeper/client/solver_probability_engine.js b/Minesweeper/client/solver_probability_engine.js index 80ae8e6..5e7aae2 100644 --- a/Minesweeper/client/solver_probability_engine.js +++ b/Minesweeper/client/solver_probability_engine.js @@ -295,12 +295,16 @@ class ProbabilityEngine { // try and connect 2 or links together to form an unavoidable 50/50 for (let link of links) { - if (!link.processed && (link.closed1 && !link.closed2 || !link.closed1 && link.closed2)) { // this is the XOR operator, so 1 and only 1 of these is closed + if (!link.processed && (!link.closed2 || !link.closed1)) { let openTile; + let openTile2; let extensions = 0; if (!link.closed1) { openTile = link.tile1; + if (!link.closed2) { + openTile2 = link.tile2; + } } else { openTile = link.tile2; } @@ -317,15 +321,22 @@ class ProbabilityEngine { if (!extension.processed) { if (extension.tile1.isEqual(openTile)) { - extensions++; extension.processed = true; noMatch = false; // accumulate the trouble tiles as we progress; link.trouble.push(...extension.trouble); - area5050.push(extension.tile2); // tile2 is the new tile - if (extension.closed2) { + if (openTile2 == null || !extension.tile1.isEqual(openTile2)) { + extensions++; + area5050.push(extension.tile2); // tile2 is the new tile + } + + if (extension.closed2 && openTile2 != null) { + openTile = openTile2; + openTile2 = null; + } + else if (extension.closed2 || openTile2 != null && extension.tile2.isEqual(openTile2)) { if (extensions % 2 == 0 && this.noTrouble(link, area5050)) { this.writeToConsole("Tile " + openTile.asText() + " is an unavoidable guess, with " + extensions + " extensions"); return this.notDead(area5050); @@ -339,15 +350,22 @@ class ProbabilityEngine { break; } if (extension.tile2.isEqual(openTile)) { - extensions++; extension.processed = true; noMatch = false; // accumulate the trouble tiles as we progress; link.trouble.push(...extension.trouble); - area5050.push(extension.tile1); // tile 1 is the new tile - if (extension.closed1) { + if (openTile2 == null || !extension.tile1.isEqual(openTile2)) { + extensions++; + area5050.push(extension.tile1); // tile 1 is the new tile + } + + if (extension.closed1 && openTile2 != null) { + openTile = openTile2; + openTile2 = null; + } + else if (extension.closed1 || openTile2 != null && extension.tile1.isEqual(openTile2)) { if (extensions % 2 == 0 && this.noTrouble(link, area5050)) { this.writeToConsole("Tile " + openTile.asText() + " is an unavoidable guess, with " + extensions + " extensions"); return this.notDead(area5050); @@ -428,7 +446,7 @@ class ProbabilityEngine { // try and connect 2 or links together to form an unavoidable 50/50 for (let link of links) { - if (!link.processed && (link.closed1 && !link.closed2 || !link.closed1 && link.closed2)) { // this is the XOR operator, so 1 and only 1 of these is closed + if (!link.processed && (!link.closed2 || !link.closed1)) { const chain = new Chain(); chain.whole5050.push(link.tile1); @@ -442,7 +460,11 @@ class ProbabilityEngine { let extensions = 0; if (!link.closed1) { chain.openTile = link.tile1; - } else { + if (!link.closed2) { + chain.openTile2 = link.tile2; + } + } + else { chain.openTile = link.tile2; } @@ -470,7 +492,6 @@ class ProbabilityEngine { if (!extension.processed && !(chain.pseudo && extension.pseudo)) { // can't add another pseudo link to an already pseudo chain if (extension.tile1.isEqual(chain.openTile)) { - extensions++; extension.processed = true; noMatch = false; @@ -487,13 +508,21 @@ class ProbabilityEngine { // accumulate the trouble tiles as we progress; chain.trouble.push(...extension.trouble); - chain.whole5050.push(extension.tile2); // tile2 is the new tile - if (!extension.dead2) { - chain.living5050.push(extension.tile2); + if (chain.openTile2 == null || !extension.tile2.isEqual(chain.openTile2)) { + extensions++; + chain.whole5050.push(extension.tile2); // tile2 is the new tile + + if (!extension.dead2) { + chain.living5050.push(extension.tile2); + } } - if (extension.closed2) { + if (extension.closed2 && chain.openTile2 != null) { + chain.openTile = chain.openTile2; + chain.openTile2 = null; + } + else if (extension.closed2 || chain.openTile2 != null && extension.tile2.isEqual(chain.openTile2)) { if (extensions % 2 == 0 && this.noTrouble(chain, chain.whole5050)) { this.writeToConsole("Tile " + chain.openTile.asText() + " is an unavoidable guess, with " + extensions + " extensions"); @@ -515,7 +544,6 @@ class ProbabilityEngine { break; } if (extension.tile2.isEqual(chain.openTile)) { - extensions++; extension.processed = true; noMatch = false; @@ -532,13 +560,21 @@ class ProbabilityEngine { // accumulate the trouble tiles as we progress; chain.trouble.push(...extension.trouble); - chain.whole5050.push(extension.tile1); // tile 1 is the new tile - if (!extension.dead1) { - chain.living5050.push(extension.tile1); + if (chain.openTile2 == null || !extension.tile1.isEqual(chain.openTile2)) { + extensions++; + chain.whole5050.push(extension.tile1); // tile 1 is the new tile + + if (!extension.dead1) { + chain.living5050.push(extension.tile1); + } } - if (extension.closed1) { + if (extension.closed1 && chain.openTile2 != null) { + chain.openTile = chain.openTile2; + chain.openTile2 = null; + } + else if (extension.closed1 || chain.openTile2 != null && extension.tile1.isEqual(chain.openTile2)) { if (extensions % 2 == 0 && this.noTrouble(chain, chain.whole5050)) { this.writeToConsole("Tile " + chain.openTile.asText() + " is an unavoidable guess, with " + extensions + " extensions"); @@ -568,7 +604,7 @@ class ProbabilityEngine { } - if (noMatch) { + if (noMatch && chain.openTile2 == null) { chains.push(chain); } @@ -969,7 +1005,7 @@ class ProbabilityEngine { this.recursions++; if (this.recursions % 1000 == 0) { - console.log("Probability Engine recursision = " + this.recursions); + this.writeToConsole("Probability Engine recursision = " + this.recursions); } const result = []; @@ -2377,6 +2413,7 @@ class Chain { this.pseudoTiles = []; this.openTile = null; + this.openTile2 = null; this.pseudo = false; this.trouble = []; diff --git a/index.html b/index.html index 9a6a0f3..f40e350 100644 --- a/index.html +++ b/index.html @@ -37,7 +37,7 @@

Opening on start - + @@ -224,6 +224,7 @@

+