using System; using System.Collections.Generic; using System.Numerics; using System.Text; using static MinesweeperControl.MinesweeperGame; using static MinesweeperSolver.SolverInfo; namespace MinesweeperSolver { class SolverMain { public static readonly BigInteger PARALLEL_MINIMUM = new BigInteger(10000); public static readonly BigInteger MAX_BRUTE_FORCE_ITERATIONS = new BigInteger(10000000); public const int BRUTE_FORCE_ANALYSIS_TREE_DEPTH = 20; public const int MAX_BFDA_SOLUTIONS = 400; public const int BRUTE_FORCE_ANALYSIS_MAX_NODES = 150000; public const bool PRUNE_BF_ANALYSIS = true; private static readonly Binomial binomial = new Binomial(500000, 500); public static SolverActionHeader FindActions(SolverInfo information) { long time1 = DateTime.Now.Ticks; List actions ; if (information.GetGameStatus() == GameStatus.Lost) { information.Write("Game has been lost already - no valid moves"); return new SolverActionHeader(); } else if (information.GetGameStatus() == GameStatus.Won) { information.Write("Game has been won already - no valid moves"); return new SolverActionHeader(); } else if (information.GetGameStatus() == GameStatus.NotStarted) { information.Write("Game has not yet started - currently unable to provide help"); return new SolverActionHeader(); } // are we walking down a brute force deep analysis tree? BruteForceAnalysis lastBfa = information.GetBruteForceAnalysis(); if (lastBfa != null) { // yes SolverTile expectedMove = lastBfa.GetExpectedMove(); if (expectedMove != null && expectedMove.IsHidden()) { // the expected move wasn't played ! information.Write("The expected Brute Force Analysis move " + expectedMove.AsText() + " wasn't played"); information.SetBruteForceAnalysis(null); } else { SolverAction move = lastBfa.GetNextMove(); if (move != null) { information.Write("Next Brute Force Deep Analysis move is " + move.AsText()); actions = new List(1); actions.Add(move); return BuildActionHeader(information, actions); } } } actions = FindTrivialActions(information); long time2 = DateTime.Now.Ticks; information.Write("Finding Trivial Actions took " + (time2 - time1) + " ticks"); // if we have some actions from the trivial search use them if (actions.Count > 0) { return BuildActionHeader(information, actions); } if (information.GetTilesLeft() == information.GetDeadTiles().Count) { information.Write("All tiles remaining are dead"); // when all the tiles are dead return the first one (they are all equally good or bad) foreach (SolverTile guess in information.GetDeadTiles()) { actions.Add(new SolverAction(guess, ActionType.Clear, 0.5)); // not all 0.5 safe though !! return BuildActionHeader(information, actions); } } // get all the witnessed tiles List witnesses = new List(information.GetWitnesses()); HashSet witnessedSet = new HashSet(); foreach (SolverTile witness in witnesses) { foreach(SolverTile adjTile in information.GetAdjacentTiles(witness)) { if (adjTile.IsHidden()) { witnessedSet.Add(adjTile); } } } List witnessed = new List(witnessedSet); int livingTilesLeft = information.GetTilesLeft(); int livingMinesLeft = information.GetMinesLeft(); int offEdgeTilesLeft = information.GetTilesLeft() - witnessed.Count; //information.Write("Excluded tiles " + information.GetExcludedTiles().Count + " out of " + information.GetTilesLeft()); //information.Write("Excluded witnesses " + information.GetExcludedWitnesses().Count); //information.Write("Excluded mines " + information.GetExcludedMineCount() + " out of " + information.GetMinesLeft()); // if there are no living mines but some living tiles then the living tiles can be cleared if (livingMinesLeft == 0 && livingTilesLeft > 0) { information.Write("There are no living mines left - all living tiles must be clearable"); for (int x = 0; x < information.description.width; x++) { for (int y = 0; y < information.description.height; y++) { SolverTile tile = information.GetTile(x, y); if (tile.IsHidden() && !tile.IsMine()) { actions.Add(new SolverAction(tile, ActionType.Clear, 1)); } } } return BuildActionHeader(information, actions); } SolutionCounter solutionCounter = new SolutionCounter(information, witnesses, witnessed, livingTilesLeft, livingMinesLeft); solutionCounter.Process(); information.Write("Solution counter says " + solutionCounter.GetSolutionCount() + " solutions and " + solutionCounter.getClearCount() + " clears"); //ProbabilityEngine pe = new ProbabilityEngine(information, witnesses, witnessed, information.GetTilesLeft(), information.GetMinesLeft()); ProbabilityEngine pe = new ProbabilityEngine(information, witnesses, witnessed, livingTilesLeft, livingMinesLeft); pe.Process(); long time3 = DateTime.Now.Ticks; information.Write("Probability Engine took " + (time3 - time2) + " ticks"); // have we found any local clears which we can use List localClears = pe.GetLocalClears(); if (localClears.Count > 0) { foreach (SolverTile tile in localClears) { // place each local clear into an action actions.Add(new SolverAction(tile, ActionType.Clear, 1)); } information.Write("The probability engine has found " + localClears.Count + " safe Local Clears"); // add any mines to it List minesFound = pe.GetMinesFound(); foreach (SolverTile tile in minesFound) { // place each mine found into an action information.MineFound(tile); actions.Add(new SolverAction(tile, ActionType.Flag, 0)); } information.Write("The probability engine has found " + minesFound.Count + " mines"); return BuildActionHeader(information, actions); } if (pe.GetBestEdgeProbability() == 1) { actions = pe.GetBestCandidates(1); information.Write("The probability engine has found " + actions.Count + " safe Clears"); // add any mines to it List minesFound = pe.GetMinesFound(); foreach (SolverTile tile in minesFound) { // place each mine found into an action information.MineFound(tile); actions.Add(new SolverAction(tile, ActionType.Flag, 0)); } information.Write("The probability engine has found " + minesFound.Count + " mines"); return BuildActionHeader(information, actions); } // dead edge (all tiles on the edge are dead and there is only one mine count value) if (pe.GetDeadEdge().Count != 0) { SolverTile tile = pe.GetDeadEdge()[0]; information.Write("Probability engine has found a dead area, guessing at " + tile.AsText()); double probability = Combination.DivideBigIntegerToDouble(BigInteger.One, pe.GetDeadEdgeSolutionCount(), 6); actions.Add(new SolverAction(tile, ActionType.Clear, probability)); return BuildActionHeader(information, actions); } // Isolated edges found (all adjacent tiles are also on the edge and there is only one mine count value) if (pe.GetOutcome() == ProbabilityEngine.Outcome.ISOLATED_EDGE) { information.Write("Probability engine has found an isolated area"); Cruncher cruncher = pe.getIsolatedEdgeCruncher(); // determine all possible solutions cruncher.Crunch(); // determine best way to solver them BruteForceAnalysis bfa = cruncher.GetBruteForceAnalysis(); bfa.process(); // if after trying to process the data we can't complete then abandon it if (!bfa.IsComplete()) { information.Write("Abandoned the Brute Force Analysis after " + bfa.GetNodeCount() + " steps"); bfa = null; } else { // otherwise try and get the best long term move information.Write("Built probability tree from " + bfa.GetSolutionCount() + " solutions in " + bfa.GetNodeCount() + " steps"); SolverAction move = bfa.GetNextMove(); if (move != null) { information.SetBruteForceAnalysis(bfa); // save the details so we can walk the tree information.Write("Brute Force Analysis: " + move.AsText()); actions.Add(move); return BuildActionHeader(information, actions); } else if (bfa.GetAllDead()) { SolverTile tile = cruncher.getTiles()[0]; information.Write("Brute Force Analysis has decided all tiles are dead on the Isolated Edge, guessing at " + tile.AsText()); double probability = Combination.DivideBigIntegerToDouble(BigInteger.One, bfa.GetSolutionCount(), 6); actions.Add(new SolverAction(tile, ActionType.Clear, probability)); return BuildActionHeader(information, actions); } else { information.Write("Brute Force Analysis: no move found!"); } } } // after this point we know the probability engine didn't return any certain clears. But there are still some special cases when everything off edge is either clear or a mine // If there are tiles off the edge and they are definitely safe then clear them all, or mines then flag them if (offEdgeTilesLeft > 0 && (pe.GetOffEdgeProbability() == 1 || pe.GetOffEdgeProbability() == 0)) { information.Write("Looking for the certain moves off the edge found by the probability engine"); bool clear; if (pe.GetOffEdgeProbability() == 1) { information.Write("All off edge tiles are clear"); clear = true; } else { information.Write("All off edge tiles are mines"); clear = false; } for (int x=0; x < information.description.width; x++) { for (int y = 0; y < information.description.height; y++) { SolverTile tile = information.GetTile(x, y); if (tile.IsHidden() && !witnessedSet.Contains(tile)) { if (clear) { information.Write(tile.AsText() + " is clear"); actions.Add(new SolverAction(tile, ActionType.Clear, 1)); } else { information.Write(tile.AsText() + " is mine"); information.MineFound(tile); actions.Add(new SolverAction(tile, ActionType.Flag, 0)); } } } } if (actions.Count > 0) { // add any mines to it List minesFound = pe.GetMinesFound(); foreach (SolverTile tile in minesFound) { // place each mine found into an action information.MineFound(tile); actions.Add(new SolverAction(tile, ActionType.Flag, 0)); } information.Write("The probability engine has found " + minesFound.Count + " mines"); return BuildActionHeader(information, actions); } else { Console.WriteLine("No Actions found!"); } } // these are guesses List guesses = pe.GetBestCandidates(1); // we know the Probability Engine completed so hold onto the information for the gui information.SetProbabilityEngine(pe); // if there aren't many possible solutions then do a brute force search if (pe.GetSolutionCount() <= MAX_BFDA_SOLUTIONS) { //if (minesFound.Count > 0) { // information.Write("Not doing a brute force analysis because we found some mines using the probability engine"); // return BuildActionHeader(information, actions); //} // find a set of independent witnesses we can use as the base of the iteration pe.GenerateIndependentWitnesses(); BigInteger expectedIterations = pe.GetIndependentIterations() * SolverMain.Calculate(livingMinesLeft - pe.GetIndependentMines(), livingTilesLeft - pe.GetIndependentTiles()); information.Write("Expected Brute Force iterations " + expectedIterations); // do the brute force if there are not too many iterations if (expectedIterations < MAX_BRUTE_FORCE_ITERATIONS) { List allCoveredTiles = new List(); for (int x = 0; x < information.description.width; x++) { for (int y = 0; y < information.description.height; y++) { SolverTile tile = information.GetTile(x, y); if (tile.IsHidden() && !tile.IsMine()) { allCoveredTiles.Add(tile); } } } WitnessWebIterator[] iterators = BuildParallelIterators(information, pe, allCoveredTiles, expectedIterations); BruteForceAnalysis bfa = Cruncher.PerformBruteForce(information, iterators, pe.GetDependentWitnesses()); bfa.process(); // if after trying to process the data we can't complete then abandon it if (!bfa.IsComplete()) { information.Write("Abandoned the Brute Force Analysis after " + bfa.GetNodeCount() + " steps"); bfa = null; } else { // otherwise try and get the best long term move information.Write("Built probability tree from " + bfa.GetSolutionCount() + " solutions in " + bfa.GetNodeCount() + " steps"); SolverAction move = bfa.GetNextMove(); if (move != null) { information.SetBruteForceAnalysis(bfa); // save the details so we can walk the tree information.Write("Brute Force Analysis: " + move.AsText()); actions.Add(move); return BuildActionHeader(information, actions); } else { information.Write("Brute Force Analysis: no move found!"); } } } else { information.Write("Too many iterations, Brute Force not atempted"); } } if (guesses.Count == 0) { // find an off edge guess if (offEdgeTilesLeft > 0) { information.Write("getting an off edge guess"); SolverTile tile = OffEdgeGuess(information, witnessedSet); SolverAction action = new SolverAction(tile, ActionType.Clear, pe.GetOffEdgeProbability()); information.Write(action.AsText()); actions.Add(action); } else { if (information.GetDeadTiles().Count > 0) { information.Write("Finding a dead tile to guess"); SolverTile tile = null; foreach (SolverTile deadTile in information.GetDeadTiles()) { // get the first dead tile tile = deadTile; break; } SolverAction action = new SolverAction(tile, ActionType.Clear, 0.5); // probability may not be 0.5 actions.Add(action); } } } else if (guesses.Count > 1) { // if we have more than 1 guess then do some tie break logic information.Write("Doing a tie break for " + guesses.Count + " actions"); actions = DoTieBreak(guesses); } else { actions = guesses; } return BuildActionHeader(information, actions); } private static SolverActionHeader BuildActionHeader(SolverInfo information, List actions) { HashSet dead = information.GetDeadTiles(); // add any dead tiles to the list of actions List deadList = new List(); foreach (SolverTile dt in dead) { deadList.Add(new SolverAction(dt, ActionType.Dead, 1)); } return new SolverActionHeader(actions, deadList); } private static List FindTrivialActions(SolverInfo information) { List actions = new List(); // provide actions for known mines which aren't flagged foreach (SolverTile tile in information.GetKnownMines()) { if (!tile.IsFlagged()) { actions.Add(new SolverAction(tile, ActionType.Flag, 0)); // request it is flagged } } actions.AddRange(ScanForTrivialActions(information, information.GetWitnesses())); //actions.AddRange(ScanForTrivialActions(information, information.GetExcludedWitnesses())); information.Write("Found " + actions.Count + " trivial actions"); return actions; } private static List ScanForTrivialActions(SolverInfo information, HashSet set) { List actions = new List(); ; HashSet alreadyProcessed = new HashSet(); foreach (SolverTile tile in set) { AdjacentInfo adjInfo = information.AdjacentTileInfo(tile); if (tile.GetValue() == adjInfo.mines) { // if we have the correct number of mines then the remaining hidden tiles can be cleared foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) { if (!alreadyProcessed.Contains(adjTile) && adjTile.IsHidden() && !adjTile.IsMine()) { alreadyProcessed.Add(adjTile); // avoid marking as cleared more than once //Utility.Write(adjTile.AsText() + " can be cleared"); actions.Add(new SolverAction(adjTile, ActionType.Clear, 1)); } } } if (tile.GetValue() == adjInfo.mines + adjInfo.hidden) { // If Hidden + Mines we already know about = tile value then the rest must be mines foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) { if (!alreadyProcessed.Contains(adjTile) && adjTile.IsHidden() && !adjTile.IsMine()) { alreadyProcessed.Add(adjTile); // avoid marking as a mine more than once //Utility.Write(adjTile.AsText() + " is a mine"); if (!information.MineFound(adjTile)) { actions.Add(new SolverAction(adjTile, ActionType.Flag, 0)); // and request it is flagged } } } } // 2 mines to find and only 3 tiles to put them if (tile.GetValue() - adjInfo.mines == 2 && adjInfo.hidden == 3) { foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) { if (!adjTile.IsHidden() && !adjTile.IsMine() && !adjTile.IsExhausted()) { AdjacentInfo tileAdjInfo = information.AdjacentTileInfo(adjTile); if (adjTile.GetValue() - tileAdjInfo.mines == 1) { // an adjacent tile only needs to find one mine int notAdjacentCount = 0; SolverTile notAdjTile = null; foreach (SolverTile adjTile2 in information.GetAdjacentTiles(tile)) { if (adjTile2.IsHidden() && !adjTile2.IsAdjacent(adjTile)) { notAdjacentCount++; notAdjTile = adjTile2; } } if (notAdjacentCount == 1 && !alreadyProcessed.Contains(notAdjTile)) { // we share all but one tile, so that one tile must contain the extra mine alreadyProcessed.Add(notAdjTile); // avoid marking as a mine more than once if (!information.MineFound(notAdjTile)) { actions.Add(new SolverAction(notAdjTile, ActionType.Flag, 0)); // and request it is flagged break; // restrict to finding one mine at a time (the action of marking the mine throws any further analysis out) } } } } } } // Subtraction method //continue; // skip this bit foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) { if (!adjTile.IsHidden() && !adjTile.IsMine() && !adjTile.IsExhausted()) { AdjacentInfo tileAdjInfo = information.AdjacentTileInfo(adjTile); if (adjTile.GetValue() - tileAdjInfo.mines == tile.GetValue() - adjInfo.mines) { // if the adjacent tile is revealed and shares the same number of mines to find // If all the adjacent tiles adjacent tiles are also adjacent to the original tile bool allAdjacent = true; foreach (SolverTile adjTile2 in information.GetAdjacentTiles(adjTile)) { if (adjTile2.IsHidden() && !adjTile2.IsAdjacent(tile)) { allAdjacent = false; break; } } if (allAdjacent) { //information.Write(tile.AsText() + " can be subtracted by " + adjTile.AsText()); foreach (SolverTile adjTile2 in information.GetAdjacentTiles(tile)) { if (adjTile2.IsHidden() && !adjTile2.IsAdjacent(adjTile) && !alreadyProcessed.Contains(adjTile2)) { alreadyProcessed.Add(adjTile2); // avoid marking as a mine more than once actions.Add(new SolverAction(adjTile2, ActionType.Clear, 1)); } } } } } } } return actions; } private static SolverTile OffEdgeGuess(SolverInfo information, HashSet witnessedSet) { // see if the corners are available SolverTile bestGuess = information.GetTile(0, 0); if (bestGuess.IsHidden() && !witnessedSet.Contains(bestGuess)) { return bestGuess; } bestGuess = information.GetTile(0, information.description.height - 1); if (bestGuess.IsHidden() && !witnessedSet.Contains(bestGuess)) { return bestGuess; } bestGuess = information.GetTile(information.description.width - 1, 0); if (bestGuess.IsHidden() && !witnessedSet.Contains(bestGuess)) { return bestGuess; } bestGuess = information.GetTile(information.description.width - 1, information.description.height - 1); if (bestGuess.IsHidden() && !witnessedSet.Contains(bestGuess)) { return bestGuess; } bestGuess = null; int bestGuessCount = 9; for (int x = 0; x < information.description.width; x++) { for (int y = 0; y < information.description.height; y++) { SolverTile tile = information.GetTile(x, y); if (tile.IsHidden() && !witnessedSet.Contains(tile)) { AdjacentInfo adjInfo = information.AdjacentTileInfo(tile); if (adjInfo.hidden < bestGuessCount) { bestGuess = tile; bestGuessCount = adjInfo.hidden; } } } } return bestGuess; } // do tie break logic private static List DoTieBreak(List actions) { List result = new List(); // find and return a not dead tile foreach (SolverAction sa in actions) { if (!sa.isDead) { result.Add(sa); return result; } } // otherwise return the first one result.Add(actions[0]); return result; } // break a witness web search into a number of non-overlapping iterators private static WitnessWebIterator[] BuildParallelIterators(SolverInfo information, ProbabilityEngine pe, List allCovered, BigInteger expectedIterations) { information.Write("Building parallel iterators"); //information.Write("Non independent iterations = " + pe.GetNonIndependentIterations(mines)); int minesLeft = information.GetMinesLeft(); //the number of witnesses available int totalWitnesses = pe.GetIndependentWitnesses().Count + pe.GetDependentWitnesses().Count; // if there is only one cog then we can't lock it,so send back a single iterator if (pe.GetIndependentWitnesses().Count == 1 && pe.GetIndependentMines() >= minesLeft || expectedIterations.CompareTo(PARALLEL_MINIMUM) < 0 || totalWitnesses == 0) { information.Write("Only a single iterator will be used"); WitnessWebIterator[] one = new WitnessWebIterator[1]; one[0] = new WitnessWebIterator(information, pe.GetIndependentWitnesses(), pe.GetDependentWitnesses(), allCovered, minesLeft, information.GetTilesLeft(), -1); return one; } int witMines = pe.GetIndependentWitnesses()[0].GetMinesToFind(); int squares = pe.GetIndependentWitnesses()[0].GetAdjacentTiles().Count; BigInteger bigIterations = Calculate(witMines, squares); int iter = (int) bigIterations; information.Write("The first cog has " + iter + " iterations, so parallel processing is possible"); WitnessWebIterator[] result = new WitnessWebIterator[iter]; for (int i = 0; i < iter; i++) { // create a iterator with a lock first got at position i result[i] = new WitnessWebIterator(information, pe.GetIndependentWitnesses(), pe.GetDependentWitnesses(), allCovered, minesLeft, information.GetTilesLeft(), i); } return result; } public static BigInteger Calculate(int mines, int squares) { return binomial.Generate(mines, squares); } public static void Initialise() { } } }