/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package minesweeper.solver; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import Asynchronous.Asynchronous; import minesweeper.gamestate.GameStateModel; import minesweeper.gamestate.MoveMethod; import minesweeper.solver.RolloutGenerator.Adversarial; import minesweeper.solver.coach.CoachModel; import minesweeper.solver.coach.CoachSilent; import minesweeper.solver.constructs.CandidateLocation; import minesweeper.solver.constructs.EvaluatedLocation; import minesweeper.solver.constructs.InformationLocation; import minesweeper.solver.constructs.WitnessData; import minesweeper.solver.iterator.Iterator; import minesweeper.solver.iterator.SequentialIterator; import minesweeper.solver.settings.PlayStyle; import minesweeper.solver.settings.SolverSettings; import minesweeper.solver.settings.SolverSettings.GuessMethod; import minesweeper.solver.utility.Binomial; import minesweeper.solver.utility.Logger; import minesweeper.solver.utility.Logger.Level; import minesweeper.solver.utility.ProgressMonitor; import minesweeper.structure.Action; import minesweeper.structure.Area; import minesweeper.structure.Location; /** * * @author David */ public class Solver implements Asynchronous { public final static String VERSION = "1.05"; // used to hold valid moves which are about to be passed out of the solver private class FinalMoves { Action[] result = new Action[0]; int suppressedFlags = 0; // number of place flag moves suppressed because of playing Flag Free boolean moveFound = false; // this is set to true if a move is found, even if it is suppressed private FinalMoves(Action...actions) { result = actions; moveFound = (actions.length > 0); } } private class LoopCheck implements Runnable { private boolean finished = false; @Override public void run() { int countDown = 100; while (countDown > 0 && !finished) { try { Thread.sleep(20); } catch (InterruptedException e) { } countDown--; } if (!finished) { System.out.println(myGame.showGameKey() + " might be looping"); } } public void finishedOkay() { finished = true; } } public final static int DP = 20; public final static BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100); final static BigDecimal OFF_EDGE_TOLERENCE = new BigDecimal("0.95"); // was 0.98 --- consider off edge tiles which if they are above the threshold of the best on edge tile final static boolean PRUNE_BF_ANALYSIS = true; final static boolean CONSIDER_HIGH_DENSITY_STRATEGY = false; public final static BigDecimal PROGRESS_VALUE = new BigDecimal("0.20"); // how much 100% Progress is worth as a proportion of Safety final static BigDecimal PROB_ENGINE_HARD_TOLERENCE = new BigDecimal("0.90"); // consider tiles on the edge with a threshold of this from the best value final static BigDecimal PROGRESS_MULTIPLIER = BigDecimal.ONE.add(PROGRESS_VALUE); //final static BigDecimal OFF_EDGE_TOLERENCE = BigDecimal.ONE.subtract(PROGRESS_VALUE); // consider off edge tiles which if they are above the threshold of the best on edge tile final static BigDecimal PROB_ENGINE_TOLERENCE = BigDecimal.ONE.subtract(PROGRESS_VALUE).max(PROB_ENGINE_HARD_TOLERENCE); //final static BigDecimal PROB_ENGINE_TOLERENCE = new BigDecimal("0.85"); // for experimental tiebreak // won't play the book opening on start if false //protected final static boolean PLAY_OPENING = true; /** * If the number of iterations is less than this then process sequential else go parallel */ final static BigInteger PARALLEL_MINIMUM = new BigInteger("10000"); final static int CORES = Runtime.getRuntime().availableProcessors(); // a binomial coefficient generator which allows up to (choose n from 1000000) and builds a cache of everything up to (choose n from 100) static Binomial binomialEngine = new Binomial(1000000, 500); protected final SolverSettings preferences; protected final Logger logger; // the class that knows the real board layout, which squares have been revealed and where the flags are private final GameStateModel myGame; // a class which can be used to display the summary information generated by the solver private final CoachModel coachDisplay; // a class which holds the solves current view of the board private final BoardState boardState; private ProbabilityEngineModel pe; private BruteForce bf; private BruteForceAnalysisModel bruteForceAnalysis; private LocationEvaluator evaluateLocations; private BigDecimal offEdgeProb; private List bfdaStartLocations = null; private List allWitnesses; private Area allWitnessedSquares; private Area deadLocations; // work areas private boolean[] workRestNotFlags; private boolean[] workRestNotClear; private Location overriddenStartLocation; private final boolean interactive; private FinalMoves answer; private PlayStyle playStyle = PlayStyle.FLAGGED; // used to indicate that the solver shouldn't bother placing flags on the board // this is considered expert tactics because it reduces the number of mouse actions. //private boolean flagFree = false; // playing chords will make the solver run slower, but should result in less moves // it is suggested to play chords if playing an external boarding using mouse controller and you wish to look impressive //private boolean playChords = false; // Shows the best tree in sysout from Brute Force Deep analysis private boolean showProbabilityTree = false; // won't play the book opening on start if false private boolean playOpening = true; private boolean early5050Check = false; // If we are only interested in the win rate we can cheat when we encounter isolated edges // if there is x chance of surviving the edge then private boolean winRateOnly = false; private BigDecimal winValue = BigDecimal.ONE; /** * Start the solver without a coach display */ public Solver(GameStateModel myGame, SolverSettings preferences, boolean interactive) { this(myGame, preferences, new CoachSilent(), interactive); } /** * Start the solver with a coach display * @param myGame * @param preferences * @param interactive */ public Solver(GameStateModel myGame, SolverSettings preferences, CoachModel coachDisplay, boolean interactive) { this.myGame = myGame; this.interactive = interactive; this.preferences = preferences.lockSettings(); if (this.interactive) { this.logger = new Logger(Level.INFO, "Solver"); } else { this.logger = new Logger(Level.ERROR, "Solver"); } this.overriddenStartLocation = preferences.getStartLocation(); this.boardState = new BoardState(this); boardState.process(); logger.log(Level.INFO, "Running with %d Cores", CORES); logger.log(Level.INFO, "Max memory available to JVM %d", Runtime.getRuntime().maxMemory()); logger.log(Level.INFO, "Free Memory available to JVM %d", Runtime.getRuntime().freeMemory()); logger.log(Level.INFO, "Solving game %s", myGame.showGameKey()); this.coachDisplay = coachDisplay; List witnesses = new ArrayList<>(500); for (int x=0; x < myGame.getWidth(); x++) { for (int y=0; y < myGame.getHeight(); y++) { Location l = boardState.getLocation(x,y); if (myGame.query(l) != GameStateModel.FLAG && myGame.query(l) != GameStateModel.HIDDEN) { witnesses.add(l); } } } logger.log(Level.DEBUG, "Found %d witnesses already in the game", witnesses.size()); } // Start of Asynchronous methods @Override public void start() { LoopCheck check = new LoopCheck(); Thread checkThread = new Thread(check); checkThread.start(); int loopSafe = 0; answer = newProcess(); while (answer.moveFound && answer.result.length == 0) { if (loopSafe++ >= 5) { this.logger.log(Level.WARN, "LOOPSAFE check!! - exiting the processing after %d iterations", loopSafe); break; } logger.log(Level.DEBUG, "There are no moves provided ( %d have been supressed) - rerunning the solver", answer.suppressedFlags ); answer = newProcess(); } check.finishedOkay(); } @Override public void requestStop() { } @Override public Action[] getResult() { return answer.result; } // end of Asynchronous methods public BigDecimal getWinValue() { return this.winValue; } /** * Return a list of Tiles which were considered when picking a guess * @return */ public List getEvaluatedLocations() { if (evaluateLocations == null) { return null; } else { return evaluateLocations.getEvaluatedLocations(); } } /** * Return an Area containing locations which are determined to be dead * @return */ public Area getDeadLocations() { if (deadLocations == null) { return null; } else { return deadLocations; } } /** * True indicates the solver shouldn't place flags * @param flagFree */ //public void setFlagFree(boolean flagFree) { // this.flagFree = flagFree; //} //public boolean isFlagFree() { // return this.flagFree; //} /** * True indicates the solver should play the opening move at the start * @param flagFree */ public void setPlayOpening(boolean playOpening) { this.playOpening = playOpening; } /** * Use this to override the default start location (which depends on the game type being played) * @param startLocation */ //public void setStartLocation(Location startLocation) { // overriddenStartLocation = startLocation; //} /** * Sets the solver play style */ public void setPlayStyle(PlayStyle playStyle) { this.playStyle = playStyle; } public PlayStyle getPlayStyle() { return this.playStyle; } public void setShowProbabilityTree(boolean showTree) { this.showProbabilityTree = showTree; } public boolean isShowProbabilityTree() { return this.showProbabilityTree; } public void setBFDAStartLocations(List start) { this.bfdaStartLocations = start; } List bfdaStartLocations() { return bfdaStartLocations; } private FinalMoves newProcess() { FinalMoves fm = doNewProcess(); if (fm.result.length > 0) { newLine("---------- Recommended Move ----------"); newLine(fm.result[0].toString()); newLine("---------- Analysis Ended -----------"); } if (boardState.getTestMoveBalance() != 0) { this.logger.log(Level.ERROR, "Test moves are not being set and reset in pairs!! Balance = %d", boardState.getTestMoveBalance()); } return fm; } private FinalMoves doNewProcess() { this.logger.log(Level.INFO, "--- Starting Analysis ---"); Action[] result = null; FinalMoves fm = new FinalMoves(); long time1 = System.currentTimeMillis(); coachDisplay.clearScreen(); pe = null; bf = null; evaluateLocations = null; deadLocations = null; if (myGame.getGameState() == GameStateModel.LOST) { topLine("The game has been lost, so no further analysis is possible"); if (myGame.supports3BV()) { newLine("3BV value " + myGame.getTotal3BV()); newLine("3BV solved " + myGame.getCleared3BV()); newLine("Action Count " + myGame.getActionCount()); double eff = ((10000 * myGame.getCleared3BV()) / myGame.getActionCount()) / 100d; newLine("Efficiency is " + eff + "%"); } return fm; } if (myGame.getGameState() == GameStateModel.WON) { topLine("The game has been won, so no further analysis is required"); if (myGame.supports3BV()) { newLine("3BV value " + myGame.getTotal3BV()); newLine("3BV solved " + myGame.getCleared3BV()); newLine("Action Count " + myGame.getActionCount()); double eff = ((10000 * myGame.getTotal3BV()) / myGame.getActionCount()) / 100d; newLine("Efficiency is " + eff + "%"); } return fm; } // query the game State object to get the current board position boardState.process(); // being asked to start the game if (myGame.getGameState() == GameStateModel.NOT_STARTED && playOpening) { if (myGame.safeOpening()) { offEdgeProb = BigDecimal.ONE; } else { offEdgeProb = BigDecimal.ONE.subtract(BigDecimal.valueOf(myGame.getMinesLeft()).divide(BigDecimal.valueOf(myGame.getHidden()), Solver.DP, RoundingMode.HALF_UP)); } fm = guess(null); newLine("This is the first move"); newLine("Note: if you aren't accepting guesses nothing will happen!"); newLine("---------- Recommended Move ----------"); newLine(fm.result[0].toString()); newLine("---------- Analysis Ended -----------"); return fm; } // are we walking down a brute force deep analysis tree? if (bruteForceAnalysis != null) { Location expectedMove = bruteForceAnalysis.getExpectedMove(); if (bruteForceAnalysis.isShallow() || expectedMove == null) { // if the analysis was shallow then don't rely on it bruteForceAnalysis = null; } else { if (expectedMove != null && !boardState.isRevealed(expectedMove)) { // we haven't played the recommended move - so the analysis is probably useless this.logger.log(Level.INFO, "The expected Brute Force Analysis move %s wasn't played", expectedMove ); bruteForceAnalysis = null; } else { if (myGame.query(expectedMove) != 0) { Action move = bruteForceAnalysis.getNextMove(boardState); if (move != null) { this.logger.log(Level.INFO, "Brute Force Deep Analysis move is %s", move); newLine("-------- Brute Force Deep Analysis Tree --------"); newLine(move.toString()); newLine("-------- Brute Force Deep Analysis Tree---------"); return new FinalMoves(move); } } else { this.logger.log(Level.INFO, "After a zero the board can be in an unexpected state, so cancelling Brute Force Analysis moves"); bruteForceAnalysis = null; } } } } int unrevealed = boardState.getTotalUnrevealedCount(); allWitnesses = boardState.getAllLivingWitnesses(); allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses); newLine("----------- Game Situation -----------"); newLine("There are " + allWitnesses.size() + " witness(es)"); newLine("There are " + allWitnessedSquares.size() + " square(s) witnessed, out of " + unrevealed); if (unrevealed == 0) { newLine("Nothing to analyse!"); //return fm; } // are the flags in the correct place? if (coachDisplay.analyseFlags() && boardState.getTotalFlagCount() > 0) { //newLine("---------- Flag Analysis -----------"); if (boardState.getConfirmedMineCount() == boardState.getTotalFlagCount()) { coachDisplay.setOkay(); newLine("All " + boardState.getTotalFlagCount() + " flags have been confirmed as correct"); } else { newLine((boardState.getTotalFlagCount() - boardState.getConfirmedMineCount()) + " flags can not be confirmed as correct"); if (boardState.validateData()) { coachDisplay.setWarn(); } else { newLine("At least 1 flag is definitely wrong!"); coachDisplay.setError(); } } } else { coachDisplay.setOkay(); } int totalMinesConfirmed = boardState.getConfirmedMineCount(); // Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them WitnessWeb wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations()); int obvious = 0; int lessObvious = 0; obvious = findTrivialActions(wholeEdge.getPrunedWitnesses()); long time2 = System.currentTimeMillis(); lessObvious = findLocalActions(wholeEdge.getPrunedWitnesses()); long time3 = System.currentTimeMillis(); // output some text describing the results int displayObvious = obvious + boardState.getUnplayedMoves(MoveMethod.TRIVIAL); int displayLessObvious = lessObvious + boardState.getUnplayedMoves(MoveMethod.LOCAL); newLine("----------- Basic Analysis -----------"); newLine("There are " + displayObvious + " trivial moves found in " + (time2 - time1) + " milliseconds"); newLine("There are " + displayLessObvious + " locally certain moves found in " + (time3 - time2) + " milliseconds"); this.logger.log(Level.INFO, "There are %d trivial / locally discoverable certain moves", (displayObvious + displayLessObvious)); if (this.playStyle.efficiency || early5050Check) { fm = new FinalMoves(new Action[0]); // we can't do the probability engine if extra mines have been found ... so return empty and try again if (boardState.getConfirmedMineCount() != totalMinesConfirmed) { fm.moveFound = true; } else { fm.moveFound = false; // otherwise push on to the probability engine } } else { fm = new FinalMoves(boardState.getActions().toArray(new Action[0])); if (obvious + lessObvious > 0) { // in flag free mode we can find moves which we don't play fm.moveFound = true; } } int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount(); if (interactive) { // can be expensive to do this, so only if we are actually going to display it BigInteger comb = combination(minesLeft, unrevealed); this.logger.log(Level.INFO, "Combinations: choose %d from %d gives %d", minesLeft, unrevealed, comb); } // leave at this point if we have got something to do if (fm.moveFound) { return fm; } // If no trivial, local, or unavoidable guess then use the probability engine // find (some) dead locations on the board - these can be ignored when looking for a good guess deadLocations = Area.EMPTY_AREA; this.logger.log(Level.INFO, "----- Starting probability engine -----"); pe = new ProbabilityEngineFast(boardState, wholeEdge, unrevealed, minesLeft); pe.process(); // get the new deadLocations with any found by the probability engine deadLocations = pe.getDeadLocations(); offEdgeProb = pe.getOffEdgeProb(); if (offEdgeProb.compareTo(BigDecimal.ONE) > 0) { this.logger.log(Level.ERROR, "Game %s has probability off edge of %f", myGame.showGameKey(), offEdgeProb); } else { this.logger.log(Level.INFO, "Probability off edge is %f", offEdgeProb); } this.logger.log(Level.INFO, "All Dead %b and dead locations %d", pe.allDead(), deadLocations.size() ); // if all the locations are dead then just use any one (unless there is only one solution) if (pe.allDead() && deadLocations.size() != 0) { if (pe.getSolutionCount().compareTo(BigInteger.ONE) == 0) { this.logger.log(Level.INFO, "Only one solution left"); } else { this.logger.log(Level.INFO, "All locations are dead"); } if (winRateOnly) { BigDecimal chance = BigDecimal.ONE.divide(new BigDecimal(pe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP); fm = identifyLocations(deadLocations.getLocations(), chance); if (fm != null) { return fm; } } // if there are no squares next to a witness then just guess if (allWitnessedSquares.getLocations().isEmpty()) { return guess(wholeEdge); } // pick any tile since they're all dead Location picked = null; for (Location tile: allWitnessedSquares.getLocations()) { // get any tile if (pe.getProbability(tile).signum() != 0) { picked = tile; break; } } CandidateLocation cl = new CandidateLocation(picked.x, picked.y, pe.getProbability(picked), 0, 0); Action a = cl.buildAction(MoveMethod.GUESS); // let the boardState decide what to do with this action boardState.setAction(a); result = boardState.getActions().toArray(new Action[0]); fm = new FinalMoves(result); return fm; } // fetch the best candidates from the edge. If high density only get the best tiles List bestCandidates; if (boardState.isHighDensity()) { bestCandidates = pe.getBestCandidates(BigDecimal.ONE, true); //} else if (preferences.isExperimentalScoring()) { // bestCandidates = pe.getBestCandidates(BigDecimal.valueOf(0.8d), true); } else { bestCandidates = pe.getBestCandidates( PROB_ENGINE_TOLERENCE, true); } List allUnrevealedSquares = null; BigDecimal offEdgeCutoff = pe.getBestOnEdgeProb().multiply(Solver.OFF_EDGE_TOLERENCE); this.logger.log(Level.INFO, "Off edge threshold is %f", offEdgeCutoff); // are clears off the edge within the permitted cut-off? boolean addOffEdgeOptions = (offEdgeProb.compareTo(offEdgeCutoff) > 0); this.logger.log(Level.INFO, "Probability Engine processing took %d milliseconds", pe.getDuration()); this.logger.log(Level.INFO, "----- Probability engine finished -----"); newLine("------ Probability Engine Analysis ------"); newLine("There are " + pe.getIndependentGroups() + " independent edges on the board"); newLine("Probability Engine processing took " + pe.getDuration() + " milliseconds"); if (pe.getSolutionCount().bitLength() < 40) { newLine("There are " + pe.getSolutionCount() + " candidate solutions remaining"); } boolean certainClearFound = pe.foundCertainty(); // look for unavoidable 50/50 here if we are doing early checks // -7317529077410525620 if (early5050Check) { FiftyFiftyHelper fiftyFiftyHelper = null; if (preferences.isDo5050Check()) { fiftyFiftyHelper = new FiftyFiftyHelper(boardState, wholeEdge, deadLocations); Location findFifty = fiftyFiftyHelper.findUnavoidable5050(pe.getMines()); if (findFifty != null) { Action a = new Action(findFifty, Action.CLEAR, MoveMethod.UNAVOIDABLE_GUESS, "Fifty-Fifty", pe.getProbability(findFifty)); fm = new FinalMoves(a); newLine("--------- Unavoidable Guess ---------"); newLine("An unavoidable guess has been found - playing now to save time"); this.logger.log(Level.DEBUG, "Fifty/Fifty found in game %s : %s", myGame.showGameKey(), fm.result[0] ); return fm; } } } // if there are no certain moves then process any Isolated non-dead edges we have found if (!certainClearFound && !pe.getIsolatedEdges().isEmpty()) { this.logger.log(Level.INFO, "Processing an Isolated edge"); newLine("--------- Isolated Area ---------"); newLine("An isolated area has been found which can be processed separately"); // get the smallest isolated area to solve BruteForce cruncher = null; for (BruteForce c: pe.getIsolatedEdges()) { if (cruncher == null || c.getTileCount() < cruncher.getTileCount()) { cruncher = c; } } // determine all possible solutions cruncher.process(); if (cruncher.hasRun()) { // determine best way to solver them BruteForceAnalysisModel bfa = cruncher.getBruteForceAnalysis(); if (bfa != null) { bfa.process(); // if after trying to process the data we can't complete then abandon it if (!bfa.isComplete()) { this.logger.log(Level.INFO, "%s Abandoned the Brute Force Analysis after %d steps", myGame.showGameKey(), bfa.getNodeCount() ); bfa = null; } else { // otherwise try and get the best long term move //TODO if (winRateOnly) { fm = identifyLocations(bfa.getLocations(), bfa.getSolveChance()); if (fm != null) { return fm; } } bruteForceAnalysis = bfa; // by setting this we will walk the tree until completed in subsequent solver calls newLine("Built probability tree from " + bruteForceAnalysis.getSolutionCount() + " solutions in " + bruteForceAnalysis.getNodeCount() + " steps"); Action move = bruteForceAnalysis.getNextMove(boardState); if (move != null) { this.logger.log(Level.INFO, "%s Brute Force Analysis: %s", myGame.showGameKey(), move); //newLine("Brute Force Analysis move is " + move.asString()); fm = new FinalMoves(move); return fm; } else { if (bruteForceAnalysis.allDead()) { this.logger.log(Level.INFO, "All Brute Force Analysis moves are dead"); // otherwise pick one of the ones on the edge Location picked = getLowest(bruteForceAnalysis.getDeadLocations().getLocations(), Area.EMPTY_AREA); if (picked != null) { fm = new FinalMoves(new Action(picked, Action.CLEAR, MoveMethod.GUESS, "", pe.getProbability(picked))); return fm; } } this.logger.log(Level.WARN, "Game %s Brute Force Analysis: no move found!", myGame.showGameKey()); } } } else { this.logger.log(Level.WARN, "Game %s Brute Force analysis class is null", myGame.showGameKey()); } } else { this.logger.log(Level.INFO, "Game %s Brute Force did not run", myGame.showGameKey()); } } if (bestCandidates.isEmpty()) { newLine("The probability engine found no candidate moves on the edge"); newLine("Probability off the edge is " + Action.FORMAT_2DP.format(offEdgeProb.multiply(ONE_HUNDRED)) + "%"); } else { newLine("The probability engine found " + bestCandidates.size() + " candidate moves on the edge"); } BigDecimal safeDensity = BigDecimal.valueOf( (double) (unrevealed - minesLeft) / (double) unrevealed); this.logger.log(Level.INFO, "Safe density %f", safeDensity); BigDecimal safeDensity3 = new BigDecimal(pe.getSolutionCount()).multiply(safeDensity).multiply(safeDensity).multiply(safeDensity); this.logger.log(Level.INFO, "BFDA Solution value %f", safeDensity3); // do brute force if the number of candidate solutions is not greater than the allowable maximum //boolean doBruteForce = (pe.getSolutionCount().compareTo(BigInteger.valueOf(preferences.getBruteForceMaxSolutions())) <= 0); boolean doBruteForce = (pe.getSolutionCount().compareTo(BigInteger.valueOf(preferences.getBruteForceMaxSolutions())) <= 0) || (safeDensity3.compareTo(BigDecimal.valueOf(preferences.getBruteForceVariableSolutions())) <= 0); //boolean certainFlagFound = !pe.getMines().isEmpty(); BruteForceAnalysisModel incompleteBFA = null; // this is used to carry forward an analysis run which didn't complete // Probability engine says there are few enough candidate solutions to do a Brute force deep analysis - so lets try if (doBruteForce && !certainClearFound) { this.logger.log(Level.INFO, "----- Brute Force starting -----"); newLine("----------- Brute Force Analysis -----------"); allUnrevealedSquares = boardState.getAllUnrevealedSquares(); WitnessWeb wholeBoard = new WitnessWeb(boardState, wholeEdge.getPrunedWitnesses(), allUnrevealedSquares); bf = new BruteForce(this, boardState, wholeBoard, minesLeft, preferences.getBruteForceMaxIterations(), pe.getSolutionCount().intValue(), "Game"); bf.process(); if (bf.hasRun()) { newLine("Found " + bf.getSolutionCount() + " candidate solutions from " + bf.getIterations() + " iterations"); // Interpret the brute force data if we have some this.bruteForceAnalysis = bf.getBruteForceAnalysis(); if (!bf.hasCertainClear() && bruteForceAnalysis != null) { // if we haven't found some 100% clears and we can do a deeper analysis bruteForceAnalysis.process(); // if all the locations are dead then just use any one if (bruteForceAnalysis.allDead()) { this.logger.log(Level.INFO, "Brute force deep analysis has detected that all locations are dead"); // if there are no squares next to a witness then just guess if (allWitnessedSquares.getLocations().isEmpty()) { return guess(wholeEdge); } // otherwise pick one of the ones on the edge Location picked = null; for (Location l: allWitnessedSquares.getLocations()) { if (pe.getProbability(l).signum() != 0) { // pick a tile which isn't a mine picked = l; break; } } if (picked != null) { CandidateLocation cl = new CandidateLocation(picked.x, picked.y, pe.getProbability(picked), 0, 0); Action a = cl.buildAction(MoveMethod.GUESS); // let the boardState decide what to do with this action boardState.setAction(a); result = boardState.getActions().toArray(new Action[0]); fm = new FinalMoves(result); return fm; } } // if after trying to process the data we can't complete then abandon it if (!bruteForceAnalysis.isComplete()) { this.logger.log(Level.INFO, "Game %s Abandoned the Brute Force Analysis after %d steps, %d of %d moves analysed", myGame.showGameKey(), bruteForceAnalysis.getNodeCount(), bruteForceAnalysis.getMovesProcessed(), bruteForceAnalysis.getMovesToProcess()); incompleteBFA = bruteForceAnalysis; // remember the incomplete analysis bruteForceAnalysis = null; } else { // otherwise try and get the best long term move deadLocations = bruteForceAnalysis.getDeadLocations(); // do win rate only if (winRateOnly) { fm = identifyLocations(bruteForceAnalysis.getLocations(), bruteForceAnalysis.getSolveChance()); if (fm != null) { return fm; } } newLine("Built probability tree from " + bruteForceAnalysis.getSolutionCount() + " solutions in " + bruteForceAnalysis.getNodeCount() + " steps"); Action move = bruteForceAnalysis.getNextMove(boardState); if (move != null) { this.logger.log(Level.DEBUG, "Brute Force Analysis move: %s", move); fm = new FinalMoves(move); } else { this.logger.log(Level.WARN, "Game %s Brute Force Analysis: no move found!", myGame.showGameKey()); } } } // if we didn't find a BFDA move (too many solutions or too many nodes searched) if (!fm.moveFound) { if (bestCandidates.isEmpty()) { newLine("Brute Force didn't find any moves...?"); } else if (bestCandidates.get(0).getProbability().compareTo(BigDecimal.ONE) == 0) { newLine("There are " + bestCandidates.size() + " certain moves"); } else { newLine("There are no certain moves, so use the best guess"); } } } else { newLine("Brute Force rejected - too many iterations to analyse"); } this.logger.log(Level.INFO, "----- Brute Force finished -----"); } // look for unavoidable 50/50 FiftyFiftyHelper fiftyFiftyHelper = null; if (!certainClearFound && !fm.moveFound && !early5050Check) { if (preferences.isDo5050Check()) { fiftyFiftyHelper = new FiftyFiftyHelper(boardState, wholeEdge, deadLocations); Location findFifty = fiftyFiftyHelper.findUnavoidable5050(pe.getMines()); if (findFifty != null) { Action a = new Action(findFifty, Action.CLEAR, MoveMethod.UNAVOIDABLE_GUESS, "Fifty-Fifty", pe.getProbability(findFifty)); fm = new FinalMoves(a); newLine("--------- Unavoidable Guess ---------"); newLine("An unavoidable guess has been found - playing now to save time"); this.logger.log(Level.DEBUG, "Fifty/Fifty found in game %s : %s", myGame.showGameKey(), fm.result[0] ); return fm; } } } // look for pseudo 50-50 guess LongTermRiskHelper ltr = null; if (!certainClearFound && !fm.moveFound) { ltr = new LongTermRiskHelper(boardState, wholeEdge, pe); if (preferences.isDo5050Check()) { //Location findFifty = fiftyFiftyHelper.process(pe); Location findFifty = ltr.findInfluence(); if (findFifty != null) { Action a = new Action(findFifty, Action.CLEAR, MoveMethod.UNAVOIDABLE_GUESS, "Fifty-Fifty", pe.getProbability(findFifty)); fm = new FinalMoves(a); newLine("--------- Unavoidable Guess ---------"); newLine("An unavoidable guess has been found - playing now to save time"); this.logger.log(Level.DEBUG, "Fifty Fifty %s : %s", myGame.showGameKey(), fm.result[0]); return fm; } } } // evaluate positions if (preferences.getGuessMethod() == GuessMethod.SECONDARY_SAFETY_PROGRESS) { evaluateLocations = new SecondarySafetyEvaluator(this, boardState, wholeEdge, pe, incompleteBFA, ltr); } else { evaluateLocations = new ProgressEvaluator(this, boardState, wholeEdge, pe); } // if we have few enough solutions do an adversarial rollout if (!fm.moveFound && !certainClearFound && !pe.isBestGuessOffEdge() && pe.getSolutionCount().compareTo(BigInteger.valueOf(preferences.getRolloutSolutions())) < 0) { this.logger.log(Level.INFO, "Doing adversarial rollout"); long nanoStart = System.nanoTime(); WitnessWeb arWholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations()); RolloutGenerator rolloutGenerator = new RolloutGenerator(boardState, arWholeEdge, unrevealed, minesLeft); rolloutGenerator.process(); List> rolloutResult = rolloutGenerator.adversarial(bestCandidates); fm = new FinalMoves(rolloutResult.get(0).original.buildAction(MoveMethod.ROLLOUT)); long nanoEnd = System.nanoTime(); this.logger.log(Level.INFO, "Adversarial rollout took %f milli seconds", + (nanoEnd - nanoStart) / 1000000 ); } // if we haven't got a move from the BFDA if (!fm.moveFound) { // no certain moves and we aren't doing tiebreaks if (!certainClearFound && !preferences.isDoTiebreak()) { // if off edge is better than on edge if (pe.isBestGuessOffEdge()) { fm = guess(wholeEdge); } else { // take the first move if (bestCandidates.size() != 0) { for (CandidateLocation cl: bestCandidates) { Action move = cl.buildAction(MoveMethod.PROBABILITY_ENGINE); // let the boardState decide what to do with this action boardState.setAction(move); break; } } else { for (CandidateLocation cl: pe.getBestCandidates(BigDecimal.ZERO, false)) { // get the best guess even if dead Action move = cl.buildAction(MoveMethod.PROBABILITY_ENGINE); // let the boardState decide what to do with this action boardState.setAction(move); break; } } Action[] moves = boardState.getActions().toArray(new Action[0]); fm = new FinalMoves(moves); } return fm; } else if (addOffEdgeOptions && !certainClearFound) { // evaluate the off edge moves this.logger.log(Level.INFO, "Adding the off edge super locations to the candidate moves"); if (allUnrevealedSquares == null) { // defer this until we need it, can be expensive allUnrevealedSquares = boardState.getAllUnrevealedSquares(); } this.logger.log(Level.DEBUG, "About to evaluate best candidates -->"); evaluateLocations.addLocations(bestCandidates); evaluateLocations.evaluateOffEdgeCandidates(allUnrevealedSquares); evaluateLocations.evaluateLocations(); this.logger.log(Level.DEBUG, "<-- Done"); evaluateLocations.showResults(); Action[] moves = evaluateLocations.bestMove(); fm = new FinalMoves(moves); } else if (certainClearFound) { // if there is only one solution or the solutions are certainties // bestCandidates.size() == 1 || certainClearFound // register all the moves for (CandidateLocation cl: bestCandidates) { Action move = cl.buildAction(MoveMethod.PROBABILITY_ENGINE); // let the boardState decide what to do with this action boardState.setAction(move); } // if all the off edge tiles are safe add them into the board state if (pe.getOffEdgeProb().compareTo(BigDecimal.ONE) == 0) { int mineCountClears = 0; for (Location loc: boardState.getAllUnrevealedSquares()) { if (!allWitnessedSquares.contains(loc)) { boardState.setAction(new Action(loc, Action.CLEAR, MoveMethod.PROBABILITY_ENGINE, "", BigDecimal.ONE)); mineCountClears++; } } //if (mineCountClears > 0 && boardState.getMines() - boardState.getConfirmedFlagCount() > 8) { // this.logger.log(Level.ALWAYS, "Seed %s has large mine count", myGame.getSeed()); //} } // if we have a certain clear then also register all the mines if (certainClearFound) { this.logger.log(Level.INFO, "Found %d mines using the probability engine", pe.getMines().size()); for (Location loc: pe.getMines()) { // let the boardState decide what to do with this action boardState.setAction(new Action(loc, Action.FLAG, MoveMethod.PROBABILITY_ENGINE, "", BigDecimal.ONE)); } } if (this.playStyle.efficiency) { EfficiencyHelper eff = new EfficiencyHelper(boardState, wholeEdge, boardState.getActions(), pe); if (this.playStyle.flagless) { fm = new FinalMoves(eff.processNF().toArray(new Action[0])); } else { fm = new FinalMoves(eff.process().toArray(new Action[0])); } } else { fm = new FinalMoves(boardState.getActions().toArray(new Action[0])); } } else { // evaluate which of the best candidates to choose this.logger.log(Level.DEBUG, "About to evaluate best candidates -->"); evaluateLocations.addLocations(bestCandidates); evaluateLocations.evaluateLocations(); this.logger.log(Level.DEBUG, "<-- Done"); evaluateLocations.showResults(); Action[] moves = evaluateLocations.bestMove(); fm = new FinalMoves(moves); } // if still no move then guess if (!fm.moveFound) { newLine("No certain, or high probability moves found, guess away from a witness"); fm = guess(wholeEdge); } } return fm; } /** * Identify which tiles are safe and which are mines by cheating */ private FinalMoves identifyLocations(Collection locs, BigDecimal value) { FinalMoves fm; if (locs.isEmpty()) { System.err.println("Nothing to identify"); } List acts = this.myGame.identifyLocations(locs); if (acts != null) { for (Action a: acts) { this.boardState.setAction(a); } this.winValue = this.winValue.multiply(value); //fm = new FinalMoves(acts.toArray(new Action[0])); fm = new FinalMoves(this.boardState.getActions().toArray(new Action[0])); } else { fm = null; } return fm; } /** * Returns the tile with the lowest hash code. This results in a consistent tile being returned. */ protected T getLowest(Collection targets, Area deadLocations) { T lowest = null; T lowestLiving = null; for (T tile: targets) { if (lowest == null || lowest.hashCode() > tile.hashCode()) { lowest = tile; } if (deadLocations != null && !deadLocations.contains(tile)) { if (lowestLiving == null || lowestLiving.hashCode() > tile.hashCode()) { lowestLiving = tile; } } } if (lowestLiving != null) { return lowestLiving; } else { return lowest; } } /** * This method will find the number of solutions which satisfy the constraints on the board */ public BigInteger getSolutionCount() throws Exception { // query the game State object to get the current board position boardState.process(); int unrevealed = boardState.getTotalUnrevealedCount(); List allWitnesses = boardState.getAllLivingWitnesses(); Area allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses); // Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them WitnessWeb wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations()); if (!wholeEdge.isWebValid()) { this.logger.log(Level.WARN, "Web is invalid"); throw new Exception("Board is invalid"); } int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount(); this.logger.log(Level.INFO, "Mines left=%d, Unrevealed=%d, Witnesses=%d, Witnessed tiles=%d", minesLeft, unrevealed, allWitnesses.size(), allWitnessedSquares.size()); SolutionCounter counter = new SolutionCounter(boardState, wholeEdge, unrevealed, minesLeft); counter.process(Area.EMPTY_AREA); return counter.getSolutionCount(); } /** * This method will rollout generator which can be used to construct random boards from this position */ public RolloutGenerator getRolloutGenerator() throws Exception { // query the game State object to get the current board position boardState.process(); int unrevealed = boardState.getTotalUnrevealedCount(); List allWitnesses = boardState.getAllLivingWitnesses(); Area allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses); // Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them WitnessWeb wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations()); if (!wholeEdge.isWebValid()) { this.logger.log(Level.WARN, "Web is invalid"); throw new Exception("Board is invalid"); } int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount(); this.logger.log(Level.INFO, "Mines left=%d, Unrevealed=%d, Witnesses=%d, Witnessed tiles=%d", minesLeft, unrevealed, allWitnesses.size(), allWitnessedSquares.size()); RolloutGenerator generator = new RolloutGenerator(boardState, wholeEdge, unrevealed, minesLeft); generator.process(); return generator; } /** * This method will find use the probability engine to get all the unrevealed tiles chance of being a mine */ public Map runTileAnalysis(ProgressMonitor pm) throws Exception { Map result = new HashMap<>(); // query the game State object to get the current board position boardState.process(); int unrevealed = boardState.getTotalUnrevealedCount(); List allWitnesses = boardState.getAllLivingWitnesses(); Area allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses); // Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them WitnessWeb wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations(), Logger.NO_LOGGING); if (!wholeEdge.isWebValid()) { this.logger.log(Level.WARN, "Web is invalid"); throw new Exception("Board is invalid"); } int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount(); this.logger.log(Level.INFO, "Mines left=%d, Unrevealed=%d, Witnesses=%d, Witnessed tiles=%d", minesLeft, unrevealed, allWitnesses.size(), allWitnessedSquares.size()); int maxProgress = boardState.getGameWidth() * boardState.getGameHeight() + 1; pm.SetMaxProgress("Processing", maxProgress); int progress = 0; ProbabilityEngineModel pe = new ProbabilityEngineFast(boardState, wholeEdge, unrevealed, minesLeft, Logger.NO_LOGGING); pe.process(); // if the tile is flagged and we agree there is a mine there then mark it as discovered for (Location mine: pe.getMines()) { //if (boardState.isFlagOnBoard(mine)) { boardState.setMineFound(mine); InformationLocation il = new InformationLocation(mine.x,mine.y); il.setSafety(BigDecimal.ZERO); il.calculate(); result.put(il, il); //} } System.out.println(pe.getMines().size()); //this.logger.log(Level.INFO, "Mines found=%d", pe.getMines().size()); // if we found some mines then recalculate the witness web if (pe.getMines().size() > 0) { boardState.process(); unrevealed = boardState.getTotalUnrevealedCount(); allWitnesses = boardState.getAllLivingWitnesses(); allWitnessedSquares = boardState.getUnrevealedArea(allWitnesses); minesLeft = myGame.getMines() - boardState.getConfirmedMineCount(); // Build a web of all the witnesses still useful and all the un-revealed tiles adjacent to them wholeEdge = new WitnessWeb(boardState, allWitnesses, allWitnessedSquares.getLocations(), Logger.NO_LOGGING); //pe = new ProbabilityEngineFast(boardState, wholeEdge, unrevealed, minesLeft, Logger.NO_LOGGING); //pe.process(); } pm.setProgress(++progress); LongTermRiskHelperOld ltr = new LongTermRiskHelperOld(boardState, wholeEdge, pe); ltr.findRisks(); if (pe.getSolutionCount().signum() == 0) { throw new Exception("This board has no solutions"); } else { for (int i=0; i < boardState.getGameWidth(); i++) { for (int j=0; j < boardState.getGameHeight(); j++) { if (boardState.isUnrevealed(i,j)) { InformationLocation il = new InformationLocation(i,j); il.setSafety(pe.getProbability(il)); doFullEvaluateTile(wholeEdge, pe, il, ltr); il.calculate(); result.put(il, il); } pm.setProgress(++progress); } } } return result; } private void doFullEvaluateTile(WitnessWeb wholeEdge, ProbabilityEngineModel probEngine, InformationLocation tile, LongTermRiskHelperOld ltr) { List superset = boardState.getAdjacentUnrevealedSquares(tile); int minesGot = boardState.countAdjacentConfirmedFlags(tile); int minMines = minesGot; int maxMines = minesGot + superset.size(); //boardState.getLogger().log(Level.ALWAYS, tile + " ==> " + minMines + " - " + maxMines); // expected number of clears if we clear here to start with is 1 x our own probability //BigDecimal expectedClears = tile.getProbability(); //boardState.display(tile.display() + " has " + linkedTiles + " linked tiles"); BigDecimal progressProb = BigDecimal.ZERO; BigDecimal secondarySafety = BigDecimal.ZERO; BigDecimal longTermSafety = BigDecimal.ZERO; for (int i = minMines; i <= maxMines; i++) { //SolutionCounter counter = validateLocationUsingSolutionCounter(wholeEdge, tile, i, probEngine.getDeadLocations()); ProbabilityEngineModel counter = runProbabilityEngine(wholeEdge, tile, i).pe; BigInteger sol = counter.getSolutionCount(); int clears = counter.getLivingClearCount(); if (sol.signum() != 0) { BigDecimal prob = new BigDecimal(sol).divide(new BigDecimal(probEngine.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP); List bestCandidates = counter.getBestCandidates(BigDecimal.ONE, true); BigDecimal safety; if (bestCandidates.size() == 0 ) { safety = counter.getOffEdgeProb(); } else { safety = bestCandidates.get(0).getProbability(); } this.logger.log(Level.INFO, "Tile %s value %d has %d living clears with probability %f and secondary safety %f", tile, i, clears, prob, safety); longTermSafety = longTermSafety.add(prob.multiply(ltr.getLongTermSafety(tile, counter))); // add all the weighted long term safety values together secondarySafety = secondarySafety.add(prob.multiply(safety)); if (clears != 0) { progressProb = progressProb.add(prob); } // store the information tile.setByValue(i, clears, prob); } else { this.logger.log(Level.INFO, "Tile %s value %d with probability zero", tile, i); } } tile.setSecondarySafety(secondarySafety); if (tile.getSafety().signum() != 0) { longTermSafety = longTermSafety.divide(tile.getSafety(), Solver.DP, RoundingMode.HALF_UP); } else { longTermSafety = BigDecimal.ZERO; } tile.setLongTermSafety(longTermSafety); } private int findTrivialActions(List witnesses) { int count = 0; for (Location loc: witnesses) { if (isObviousClear(loc)) { //boolean accepted = boardState.setChordLocation(loc); for (Location l: boardState.getAdjacentSquaresIterable(loc)) { if (boardState.isUnrevealed(l)) { if (!boardState.alreadyActioned(l)) { count++; boardState.setAction(new Action(l, Action.CLEAR, MoveMethod.TRIVIAL, "", BigDecimal.ONE)); } } } } else if (isObviousFlag(loc)) { for (Location l: boardState.getAdjacentSquaresIterable(loc)) { if (boardState.isUnrevealed(l)) { if (!boardState.alreadyActioned(l)) { count++; boardState.setAction(new Action(l, Action.FLAG, MoveMethod.TRIVIAL, "", BigDecimal.ONE)); //boardState.setFlagConfirmed(l); } } } } } return count; } private boolean isObviousClear(Location loc) { //if (boardState.isRevealed(x,y) && boardState.getWitnessValue(x,y) != 0) { int flags = boardState.countAdjacentConfirmedFlags(loc); // if we have all the flags and there is something to clear if (boardState.getWitnessValue(loc) == flags && boardState.countAdjacentUnrevealed(loc) > 0) { return true; } //} return false; } private boolean isObviousFlag(Location loc) { //if (boardState.isRevealed(x,y) && boardState.getWitnessValue(x,y) != 0) { int flags = boardState.countAdjacentConfirmedFlags(loc); int free = boardState.countAdjacentUnrevealed(loc); // if we only have space for the flags and there is some space if (boardState.getWitnessValue(loc) == flags + free && free > 0) { return true; } //} return false; } private int findLocalActions(List witnesses) { int count = 0; List square; List witness; for (Location loc: witnesses) { int flags = boardState.countAdjacentConfirmedFlags(loc); int free = boardState.countAdjacentUnrevealed(loc); // if there are still some flags to find and there are // too many places for it to be obvious ... if (free > 0 && boardState.getWitnessValue(loc) > flags && boardState.getWitnessValue(loc) < flags + free) { // get the un-revealed squares square = boardState.getAdjacentUnrevealedSquares(loc); // now get the witnesses witness = boardState.getWitnesses(square); // and crunch the result if (witness.size() > 1) { CrunchResult output = crunch(square, witness, new SequentialIterator(boardState.getWitnessValue(loc) - flags, square.size()), false, null); count = count + checkBigTally(output, MoveMethod.LOCAL, ""); count = count + checkWitnesses(output, MoveMethod.LOCAL, ""); } } } return count; } /** * Checks whether this location can have the value using a localised check. Returns number squares which can be cleared. -1 means impossible situation */ /* protected int validateLocationUsingLocalCheck(Location superLocation, int value) { int clearCount = 0; int minesToFit = value - boardState.countAdjacentConfirmedFlags(superLocation); if (minesToFit == 0) { return boardState.countAdjacentUnrevealed(superLocation); //return true; } else if (minesToFit < 0) { return -1; //return false; } // make the move boardState.setWitnessValue(superLocation, value); // get the un-revealed squares List square = boardState.getAdjacentUnrevealedSquares(superLocation); // now get the witnesses List witnesses = boardState.getWitnesses(square); // and crunch the result try { if (witnesses.size() > 1) { //display(i + " " + j + " board " + board[i][j] + " flags = " + flags + " free = " + free); CrunchResult output = crunch(square, witnesses, new SequentialIterator(boardState.getWitnessValue(superLocation) - boardState.countAdjacentConfirmedFlags(superLocation), square.size()), false, null); if (output.bigGoodCandidates.signum() > 0) { for (int i=0; i < output.bigTally.length; i++) { if (output.bigTally[i].signum() == 0) { Location l = output.getSquare().get(i); if (!boardState.alreadyActioned(l)) { clearCount++; } } } display(superLocation.display() + " Clear count = " + clearCount); return clearCount; //return true; } else { return -1; //return false; } } else { return -1; //return false; } } finally { boardState.clearWitness(superLocation); } } */ /** * Checks whether this location can have the value using the solution counter */ protected SolutionCounter validateLocationUsingSolutionCounter(WitnessWeb wholeEdge, Location superLocation, int value, Area deadLocations) { // make the move boardState.setWitnessValue(superLocation, value); // create a new list of witnesses List witnesses = new ArrayList<>(wholeEdge.getPrunedWitnesses().size() + 1); witnesses.addAll(wholeEdge.getPrunedWitnesses()); witnesses.add(superLocation); Area witnessed = boardState.getUnrevealedArea(witnesses); WitnessWeb edge = new WitnessWeb(boardState, witnesses, witnessed.getLocations(), Logger.NO_LOGGING); int unrevealed = boardState.getTotalUnrevealedCount() - 1; // this is one less, because we have added a witness int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount(); SolutionCounter counter = new SolutionCounter(boardState, edge, unrevealed, minesLeft); counter.process(deadLocations); // undo the move boardState.clearWitness(superLocation); return counter; } /** * Checks whether this board state is valid */ protected SolutionCounter validatePosition(WitnessWeb wholeEdge, List mines, List noMines, Area deadLocations) { // add the mines for (Location mine: mines) { boardState.setMineFound(mine); } Area witnessed = boardState.getUnrevealedArea(wholeEdge.getPrunedWitnesses()); WitnessWeb edge = new WitnessWeb(boardState, wholeEdge.getPrunedWitnesses(), witnessed.getLocations(), Logger.NO_LOGGING); int unrevealed = boardState.getTotalUnrevealedCount() - mines.size(); // this is less, because we have added some mines. int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount(); SolutionCounter counter = new SolutionCounter(boardState, edge, unrevealed, minesLeft); //System.out.println("Unrevealed " + unrevealed + ", witnessed " + witnessed.size() + ", minesLeft " + minesLeft); // add the no mines if (noMines != null) { for (Location noMine: noMines) { if (!counter.setMustBeEmpty(noMine)) { this.logger.log(Level.WARN, "%s failed to set Must Be Empty", noMine); } } } counter.process(deadLocations); // remove the mines for (Location mine: mines) { boardState.unsetMineFound(mine); } return counter; } class RunPeResult { ProbabilityEngineModel pe; boolean found5050 = false; } /** * Runs the probability engine for a position with one extra witness than where we currently are */ protected RunPeResult runProbabilityEngine(WitnessWeb wholeEdge, Location location, int value) { // make the move boardState.setWitnessValue(location, value); // create a new list of witnesses List witnesses = new ArrayList<>(wholeEdge.getPrunedWitnesses().size() + 1); witnesses.addAll(wholeEdge.getPrunedWitnesses()); witnesses.add(location); Area witnessed = boardState.getUnrevealedArea(witnesses); WitnessWeb edge = new WitnessWeb(boardState, witnesses, witnessed.getLocations(), Logger.NO_LOGGING); int unrevealed = boardState.getTotalUnrevealedCount() - 1; // this is one less, because we have added a witness int minesLeft = myGame.getMines() - boardState.getConfirmedMineCount(); RunPeResult result = new RunPeResult(); result.pe = new ProbabilityEngineFast(boardState, edge, unrevealed, minesLeft); result.pe.process(); // looking for created 50/50s doesn't seem to help the win rate /* Location findFifty = new FiftyFiftyHelper(boardState, wholeEdge, deadLocations).findUnavoidable5050(result.pe.getMines()); if (findFifty != null) { //this.logger.log(Level.WARN, "Fifty/Fifty created in game %s : %s", myGame.showGameKey(), findFifty ); result.found5050 = true; } */ // undo the move boardState.clearWitness(location); return result; } protected CrunchResult crunch(final List square, final List witness, Iterator iterator, boolean calculateDistribution, BruteForceAnalysisModel bfa) { this.logger.log(Level.DEBUG, "Crunching %d Mines in %d Tiles with %d Witnesses", iterator.getBalls(), square.size(), witness.size()); // the distribution is the number of times a square reveals as the number 0-8 BigInteger[][] bigDistribution = null; if (calculateDistribution) { bigDistribution = new BigInteger[square.size()][9]; for (int i=0; i < square.size(); i++) { for (int j=0; j < 9; j++) { bigDistribution[i][j] = BigInteger.ZERO; } } } BigInteger bign = BigInteger.ZERO; // determine the witness type int[] witnessGood1 = generateWitnessType(witness, square); // encapsulate the witness data final WitnessData[] witnessData = new WitnessData[witness.size()]; for (int i=0; i < witness.size(); i++) { WitnessData d = new WitnessData(); d.location = witness.get(i); d.witnessGood = witnessGood1[i]; d.witnessRestClear = true; d.witnessRestFlag = true; d.currentFlags = boardState.countAdjacentConfirmedFlags(d.location); d.alwaysSatisfied = iterator.witnessAlwaysSatisfied(d.location); witnessData[i] = d; } /* for (int i=0; i < square.length; i++) { display("Square " + i + " is " + square[i].display()); } */ int[] sample = iterator.getSample(); int[] tally = new int[square.size()]; int candidates = 0; // define work areas workRestNotFlags = new boolean[witnessData.length]; workRestNotClear = new boolean[witnessData.length]; while (sample != null) { if (checkSample(sample, square, witnessData, bigDistribution, bfa)) { for (int i=0; i < sample.length; i++) { tally[sample[i]]++; } candidates++; } sample = iterator.getSample(); } BigInteger[] bigTally = new BigInteger[square.size()]; for (int i = 0; i < bigTally.length; i++) { bigTally[i] = BigInteger.valueOf(tally[i]); } bign = BigInteger.valueOf(candidates); // store all the information we have gathered into this object for // later analysis CrunchResult output = new CrunchResult(); output.setSquare(square); output.bigDistribution = bigDistribution; output.originalNumMines = iterator.getBalls(); output.bigGoodCandidates = bign; output.bigTally = bigTally; // return data on the witnesses output.witness = new Location[witnessData.length]; output.witnessGood = new int[witnessData.length]; output.witnessRestClear = new boolean[witnessData.length]; output.witnessRestFlags = new boolean[witnessData.length]; for (int i=0; i < witnessData.length; i++) { output.witness[i] = witnessData[i].location; output.witnessGood[i] = witnessData[i].witnessGood; output.witnessRestClear[i] = witnessData[i].witnessRestClear; output.witnessRestFlags[i] = witnessData[i].witnessRestFlag; } return output; } // this checks whether the positions of the mines are a valid candidate solution protected boolean checkSample(final int[] sample, final List square, WitnessData[] witnessData, BigInteger[][] bigDistribution, BruteForceAnalysisModel bfa) { /* String s= ""; for (int i = 0; i < sample.length; i++) { s = s + " " + sample[i]; } display(s); */ for (int i=0; i < witnessData.length; i++) { workRestNotFlags[i] = false; workRestNotClear[i] = false; } // get the location of the mines Location[] mine = new Location[sample.length]; for (int i=0; i < sample.length; i++) { mine[i] = square.get(sample[i]); } for (int i=0; i < witnessData.length; i++) { if (!witnessData[i].alwaysSatisfied) { int flags1 = witnessData[i].currentFlags; int flags2 = 0; // count how many candidate mines are next to this witness for (int j = 0; j < mine.length; j++) { if (mine[j].isAdjacent(witnessData[i].location)) { flags2++; } } int flags3 = boardState.getWitnessValue(witnessData[i].location); // if the candidate solution puts more flags around the witness // than it says it has then the solution is invalid if (flags3 < flags1 + flags2) { WitnessData d = witnessData[0]; witnessData[0] = witnessData[i]; witnessData[i] = d; return false; } // if this is a 'good' witness and the number of flags around it // does not match with it exactly then the solution is invalid if (witnessData[i].witnessGood == 0 && flags3 != flags1 + flags2) { WitnessData d = witnessData[0]; witnessData[0] = witnessData[i]; witnessData[i] = d; return false; } if (flags3 != flags1 + flags2) { workRestNotClear[i] = true; } if (flags3 != flags1 + flags2 + witnessData[i].witnessGood) { workRestNotFlags[i] = true; } } else { // always satisfied means flag3 = flag1 + flag2, so the checks above can be simplified to if (witnessData[i].witnessGood != 0) { workRestNotFlags[i] = true; } } } // if it is a good candidate solution then the witness information is valid for (int i=0; i < witnessData.length; i++) { if (workRestNotClear[i]) { witnessData[i].witnessRestClear = false; } if (workRestNotFlags[i]) { witnessData[i].witnessRestFlag = false; } } //if it is a good solution then calculate the distribution if required if (bfa != null && !bfa.tooMany()) { byte[] solution = new byte[square.size()]; for (int i=0; i < square.size(); i++) { boolean isMine = false; for (int j=0; j < sample.length; j++) { if (i == sample[j]) { isMine = true; break; } } // if we are a mine then it doesn't matter how many mines surround us if (!isMine) { byte flags2 = (byte) boardState.countAdjacentConfirmedFlags(square.get(i)); // count how many candidate mines are next to this square for (Location mine1 : mine) { if (mine1.isAdjacent(square.get(i))) { flags2++; } } solution[i] = flags2; if (bigDistribution != null) { bigDistribution[i][flags2] = bigDistribution[i][flags2].add(BigInteger.ONE); } } else { solution[i] = GameStateModel.MINE; } } bfa.addSolution(solution); } return true; } // a witness is a 'good' witness if all its adjacent free squares are also // contained in the set of squares being analysed. A 'good' witness must // always be satisfied for the candidate solution to be valid. // this method returns the number of squares around the witness not being // analysed - a good witness has a value of zero protected int[] generateWitnessType(List witness, List square) { int[] result = new int[witness.size()]; for (int i=0; i < witness.size(); i++) { result[i] = 0; for (Location l: boardState.getAdjacentUnrevealedSquares(witness.get(i))) { boolean found = false; for (Location squ: square) { if (l.equals(squ)) { found = true; break; } } if (!found) { result[i]++; } } } return result; } // do the tally check using the BigInteger values private int checkBigTally(CrunchResult output, MoveMethod method, String comment) { int result=0; // if there were no good candidates then there is nothing to check if (output.bigGoodCandidates.signum() == 0) { return 0; } // check the tally information to see if we have a square where a // mine is always present or never present for (int i=0; i < output.bigTally.length; i++) { if (output.bigTally[i].compareTo(output.bigGoodCandidates) == 0) { Location l = output.getSquare().get(i); if (!boardState.alreadyActioned(l)) { result++; boardState.setAction(new Action(l, Action.FLAG, method, comment, BigDecimal.ONE)); } } else if (output.bigTally[i].signum() == 0) { Location l = output.getSquare().get(i); if (!boardState.alreadyActioned(l)) { result++; boardState.setAction(new Action(l, Action.CLEAR, method, comment, BigDecimal.ONE)); //display("clear found at " + x + " " + y); } } } return result; } // in some cases we learn more about the other witnesses during the crunch // this only happens for local search. private int checkWitnesses(CrunchResult output, MoveMethod method, String comment) { int result = 0; // check the witnesses to see if they have discovered something for (int i=0; i < output.witnessRestFlags.length; i++) { if (output.witnessGood[i] != 0) { if (output.witnessRestFlags[i]) { //display("**** CheckWitnesses has found a FLAG " + output.witness[i].display()); result = result + restKnown(output.witness[i], output.getSquare(), Action.FLAG, method, comment); } if (output.witnessRestClear[i]) { //display("**** CheckWitnesses has found a CLEAR " + output.witness[i].display()); result = result + restKnown(output.witness[i], output.getSquare(), Action.CLEAR, method, comment); } } } return result; } private int restKnown(Location witness, List square, int action, MoveMethod method, String comment) { int result=0; for (Location l: boardState.getAdjacentSquaresIterable(witness)) { // find all the unflagged and unrevealed squares if (!boardState.isRevealed(l) && !boardState.isConfirmedMine(l)) { boolean found = false; for (Location k: square) { if (l.equals(k)) { found = true; break; } } if (!found && !boardState.alreadyActioned(l)) { Action act; if (action == Action.FLAG) { act = new Action(l, Action.FLAG, method, comment, BigDecimal.ONE); //boardState.setFlagConfirmed(act); } else { act = new Action(l, Action.CLEAR, method, comment, BigDecimal.ONE); } result++; boardState.setAction(act); //display("Discovered witness information at " + x1 + " " + y1); } } } return result; } /** * Find the best guess off the edge * @return */ private FinalMoves guess(WitnessWeb wholeEdge) { Action action = null; this.logger.log(Level.INFO, "Picking a guess"); // get the starting move if we are at the start of the game if (myGame.getGameState() == GameStateModel.NOT_STARTED && playOpening) { if (overriddenStartLocation != null) { action = new Action(overriddenStartLocation, Action.CLEAR, MoveMethod.BOOK, "", offEdgeProb); } else { action = new Action(myGame.getStartLocation(), Action.CLEAR, MoveMethod.BOOK, "", offEdgeProb); } } // if there is no book move then look for a guess off the edge if (action == null) { List list = new ArrayList<>(); for (int i=0; i < myGame.getWidth(); i++) { for (int j=0; j < myGame.getHeight(); j++) { // if we are an unrevealed square and we aren't on the contour // then store the location if (boardState.isUnrevealed(i,j)) { Location l = this.boardState.getLocation(i, j); // if we aren't on the edge and there are some adjacent squares if ((wholeEdge == null || !wholeEdge.isOnWeb(l))) { list.add(new CandidateLocation(l.x, l.y, offEdgeProb, boardState.countAdjacentUnrevealed(l), boardState.countAdjacentConfirmedFlags(l))); } } } } // sort into most favourable order //Collections.sort(list, CandidateLocation.SORT_BY_PROB_FLAG_FREE); Collections.sort(list, CandidateLocation.SORT_BY_PROB_FREE_FLAG); if (list.isEmpty()) { return new FinalMoves(); } // ... and pick the first one action = list.get(0).buildAction(MoveMethod.GUESS); } // this will check there isn't a flag blocking the move boardState.setAction(action); return new FinalMoves(boardState.getActions().toArray(new Action[0])); } public BigDecimal getProbability(int x, int y) { if (boardState.isConfirmedMine(x, y)) { return BigDecimal.ZERO; } else if (bf != null && bf.hasRun()) { return bf.getProbability(x, y); } else if (pe != null) { return pe.getProbability(this.boardState.getLocation(x,y)); } else { return boardState.getProbability(x, y); } } protected void topLine(final String s) { coachDisplay.clearScreen(); coachDisplay.writeLine(s); } protected void newLine(final String s) { coachDisplay.writeLine(s); } protected void display(String text) { if (interactive) { displayAlways(text); } } protected void displayAlways(String text) { System.out.println(text); } public void kill() { this.logger.log(Level.DEBUG, "Killing the Solver Object"); coachDisplay.kill(); } /** * calculate the number of distinct ways mines can be placed in squares */ public static BigInteger combination(int mines, int squares) { try { return binomialEngine.generate(mines, squares); } catch (Exception e) { System.out.println("Error calculating the binomial coefficient"); e.printStackTrace(); //throw new RuntimeException("Error calculating the binomial coefficient", e); return BigInteger.ONE; } } public GameStateModel getGame() { return myGame; } }