2151 lines
70 KiB
Java
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;
|
|
}
|
|
|
|
}
|