Files
BH2023-Minesweeper/references/Minesweeper/MineSweeperSolver/src/minesweeper/solver/Solver.java
2023-09-28 20:24:18 +08:00

2151 lines
70 KiB
Java

/*
* 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<Action[]> {
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<Location> bfdaStartLocations = null;
private List<Location> 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<Location> 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<EvaluatedLocation> 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<Location> start) {
this.bfdaStartLocations = start;
}
List<Location> 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<CandidateLocation> 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<Location> 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<Adversarial<CandidateLocation>> 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<? extends Location> locs, BigDecimal value) {
FinalMoves fm;
if (locs.isEmpty()) {
System.err.println("Nothing to identify");
}
List<Action> 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 extends Location> T getLowest(Collection<T> 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<Location> 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<Location> 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<Location, InformationLocation> runTileAnalysis(ProgressMonitor pm) throws Exception {
Map<Location, InformationLocation> result = new HashMap<>();
// query the game State object to get the current board position
boardState.process();
int unrevealed = boardState.getTotalUnrevealedCount();
List<Location> 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<Location> 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<CandidateLocation> 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<? extends Location> 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<? extends Location> witnesses) {
int count = 0;
List<Location> square;
List<Location> 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<Location> square = boardState.getAdjacentUnrevealedSquares(superLocation);
// now get the witnesses
List<Location> 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<Location> 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<Location> mines, List<Location> 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<Location> 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<Location> square, final List<? extends Location> 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<Location> 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<? extends Location> witness, List<Location> 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<? extends Location> 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<CandidateLocation> 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;
}
}