Add References to git

This commit is contained in:
2023-09-28 20:23:18 +08:00
parent 4d51eb339c
commit 50b5f8c2c1
276 changed files with 56093 additions and 2 deletions

1
.gitignore vendored
View File

@ -2,5 +2,4 @@
.vscode/
build/
src/std/
info/
tmp/

File diff suppressed because it is too large Load Diff

BIN
info/Minesweeper.pdf Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -0,0 +1 @@
/bin/

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Asynchronous</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,11 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.8

View File

@ -0,0 +1,19 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package Asynchronous;
/**
*
* This interface defines an object which can be run asynchronously
*/
public interface Asynchronous<V> {
public void start();
public void requestStop();
public V getResult();
}

View File

@ -0,0 +1,189 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package Monitor;
import Asynchronous.Asynchronous;
/**
*
* @author David
*/
public class AsynchMonitor {
private class Process extends Thread {
private final int index;
private final Asynchronous item;
private volatile boolean started = false;
private volatile boolean completed = false;
public Process(int index, Asynchronous item) {
super("Process " + index);
this.index = index;
this.item = item;
this.setDaemon(true);
}
@Override
public void run() {
if (started) {
System.out.println("trying to start an already running task");
return;
}
started = true;
item.start();
completed = true;
taskCompleted(index);
}
public boolean isStarted() {
return started;
}
public boolean isCompleted() {
return completed;
}
}
private Asynchronous[] items;
private Process[] process;
private boolean finished = false;
private boolean started = false;
private Thread initThread;
private int maxThreads = 100;
private volatile int startedCount; // volatile ensures it is updated fro other threads
// create a monitor for the provided tasks
public AsynchMonitor(Asynchronous... items) {
this.items = items;
this.process = new Process[items.length];
//this.complete = new boolean[items.length];
for (int i=0; i < items.length; i++) {
process[i] = new Process(i, items[i]);
}
}
// kick off each of the sub tasks
public void start() throws Exception {
if (started) {
throw new Exception("Processes already started exception");
}
started = true;
finished = false;
startedCount = Math.min(maxThreads, items.length);
// can't use started count because it might get updated by a quick finisher
int stop = startedCount;
// start the initial processes checking that they haven't already been started
// by a quick finisher
for (int i=0; i < stop; i++) {
if (!process[i].isStarted()) {
process[i].start();
}
}
}
public void setMaxThreads(int max) {
this.maxThreads = max;
}
public void startAndWait() throws Exception {
try {
start();
} catch (Exception ex) {
throw ex;
}
suspend();
}
public void suspend() {
initThread = Thread.currentThread();
while (!finished) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//System.out.println("interrupted");
}
}
}
private synchronized void taskCompleted(int index) {
//System.out.println("Sub task " + index + " finished");
//complete[index] = true;
// look for more to start up but domn't include the initial start ups
for (int i=startedCount; i < process.length; i++) {
if (!process[i].isStarted()) {
//System.out.println("Starting process " + i + " out of " + process.length);
startedCount = i + 1;
process[i].start();
break;
}
}
/*
for (Process process: process) {
if (!process.isStarted()) {
process.start();
break;
}
}
*/
boolean allDone = true;
for (Process process: process) {
if (!process.isCompleted()) {
allDone = false;
break;
}
}
// if we are all done then wake the requested thread
if (allDone && !finished) {
//System.out.println("All the sub tasks have completed");
finished = true;
if (initThread != null) {
initThread.interrupt();
}
}
}
public boolean isFinished() {
return finished;
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Images</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
</buildSpec>
<natures>
</natures>
</projectDescription>

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry combineaccessrules="false" exported="true" kind="src" path="/MinesweeperGameState"/>
<classpathentry combineaccessrules="false" exported="true" kind="src" path="/Asynchronous"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -0,0 +1 @@
/bin/

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>MineSweeperSolver</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,888 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import minesweeper.gamestate.GameStateModel;
import minesweeper.gamestate.MoveMethod;
import minesweeper.solver.BoardStateCache.AdjacentSquares;
import minesweeper.solver.BoardStateCache.Cache;
import minesweeper.solver.utility.Logger;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Action;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
public class BoardState {
private final static int[] DX = {0, 1, 1, 1, 0, -1, -1, -1};
private final static int[] DY = {-1, -1, 0, 1, 1, 1, 0, -1};
//private AdjacentSquares[][] adjacentLocations1;
//private AdjacentSquares[][] adjacentLocations2;
private int[][] board;
private boolean[][] revealed;
private boolean[][] confirmedMine;
private boolean[][] flagOnBoard;
//private int[][] clearAll;
private int[][] adjFlagsConfirmed;
private int[][] adjFlagsOnBoard;
private int[][] adjUnrevealed;
// this holds the actions made against each location and a list of actions generated this turn
private Action[][] action;
private List<Action> actionList = new ArrayList<Action>();
private final GameStateModel myGame;
private final Solver solver;
private final int height;
private final int width;
private int totalFlags = 0;
private int confirmedMinesTotal = 0;
private int numOfHidden = 0;
private int[] unplayedMoves;
private int testMoveBalance = 0;
private Set<Location> livingWitnesses = new HashSet<>();
private final Cache cache;
public BoardState(Solver solver) {
this.solver = solver;
this.myGame = solver.getGame();
this.width = myGame.getWidth();
this.height = myGame.getHeight();
confirmedMine = new boolean[myGame.getWidth()][myGame.getHeight()];
adjFlagsConfirmed = new int[myGame.getWidth()][myGame.getHeight()];
adjUnrevealed = new int[myGame.getWidth()][myGame.getHeight()];
revealed = new boolean[myGame.getWidth()][myGame.getHeight()];
board = new int[myGame.getWidth()][myGame.getHeight()];
flagOnBoard = new boolean[myGame.getWidth()][myGame.getHeight()];
adjFlagsOnBoard = new int[myGame.getWidth()][myGame.getHeight()];
action = new Action[myGame.getWidth()][myGame.getHeight()];
// look up the adjacent squares details
cache = BoardStateCache.getInstance().getAdjacentSquares1(myGame.getWidth(), myGame.getHeight());
//adjacentLocations1 = cache.adjacentLocations1;
//adjacentLocations2 = cache.adjacentLocations2;
final int bottom = myGame.getHeight() - 1;
final int right = myGame.getWidth() - 1;
// set up how many adjacent locations there are to each square - they are all unrevealed to start with
for (int x=0; x < width; x++) {
for (int y=0; y < height; y++) {
int adjacent = 8;
// corners
if (x == 0 && y == 0 || x == 0 && y == bottom || x == right && y == 0 || x == right && y == bottom) {
adjacent = 3;
// the edge
} else if (x == 0 || y == 0 || x == right || y == bottom){
adjacent = 5;
}
adjUnrevealed[x][y] = adjacent;
}
}
}
public void process() {
totalFlags = 0;
confirmedMinesTotal = 0;
numOfHidden = 0;
// clear down this array, which is a lot faster then defining it fresh
for (int i=0; i < width; i++) {
for (int j=0; j < height; j++) {
adjFlagsOnBoard[i][j] = 0;
}
}
// clear down the moves we collected last turn
actionList.clear();
// load up what we can see on the board
for (int i=0; i < width; i++) {
for (int j=0; j < height; j++) {
Location location = getLocation(i, j);
flagOnBoard[i][j] = false; // until proven otherwise
int info = myGame.query(location);
Action act = action[i][j];
// if the move isn't a certainty then don't bother with it. The opening book is a certainty on the first move, but isn't really if the player plays somewhere else.
if (act != null && (!act.isCertainty() || act.getMoveMethod() == MoveMethod.BOOK)) {
action[i][j] = null;
act = null;
}
if (info != GameStateModel.HIDDEN) {
if (info == GameStateModel.FLAG) {
totalFlags++;
flagOnBoard[i][j] = true;
// inform its neighbours they have a flag on the board
for (int k=0; k < DX.length; k++) {
if (i + DX[k] >= 0 && i + DX[k] < width && j + DY[k] >= 0 && j + DY[k] < height) {
adjFlagsOnBoard[i + DX[k]][j + DY[k]]++;
}
}
if (confirmedMine[i][j]) { // mine found by solver
confirmedMinesTotal++;
} else {
numOfHidden++; // flag on the board but we can't confirm it
}
// if the board is a flag, but we are 100% sure its a clear then remove the flag
// then clear the square
if (act != null && act.getAction() == Action.CLEAR && act.isCertainty()) {
actionList.add(new Action(act, Action.FLAG, MoveMethod.CORRECTION, "Remove flag", BigDecimal.ONE, 0));
actionList.add(act);
}
} else {
// if this is a new unrevealed location then set it up and inform it's neighbours they have one less unrevealed adjacent location
if (!revealed[i][j]) {
livingWitnesses.add(location); // add this to living witnesses
//display("Location (" + i + "," + j + ") is revealed");
revealed[i][j] = true;
board[i][j] = info;
for (int k=0; k < DX.length; k++) {
if (i + DX[k] >= 0 && i + DX[k] < width && j + DY[k] >= 0 && j + DY[k] < height) {
adjUnrevealed[i + DX[k]][j + DY[k]]--;
}
}
}
}
} else {
if ((solver.getPlayStyle().flagless || solver.getPlayStyle().efficiency) && confirmedMine[i][j]) { // if we are playing flags free then all confirmed mines are consider to be flagged
confirmedMinesTotal++;
totalFlags++;
} else {
numOfHidden++;
}
// if we have an action against this location which we are 100% sure about then do it
if (act != null && act.isCertainty()) {
if ((solver.getPlayStyle().flagless || solver.getPlayStyle().efficiency) && act.getAction() == Action.FLAG) {
// unless the we are playing flag free and it's a flag
} else {
actionList.add(act);
}
}
}
}
}
List<Location> toRemove = new ArrayList<>();
for (Location wit: livingWitnesses) {
if (this.countAdjacentUnrevealed(wit) == 0) {
//display("Location " + wit.display() + " is now a dead witness");
toRemove.add(wit);
}
}
livingWitnesses.removeAll(toRemove);
// this sorts the moves by when they were discovered
Collections.sort(actionList, Action.SORT_BY_MOVE_NUMBER);
unplayedMoves = new int[MoveMethod.values().length];
// accumulate how many unplayed moves there are by method
for (Action a: actionList) {
unplayedMoves[a.getMoveMethod().ordinal()]++;
}
getLogger().log(Level.INFO, "Moves left to play is %d", actionList.size());
for (int i=0; i < unplayedMoves.length; i++) {
if (unplayedMoves[i] != 0) {
getLogger().log(Level.INFO, " %s has %d moves unplayed",MoveMethod.values()[i], unplayedMoves[i]);
}
}
}
protected int getGameWidth() {
return width;
}
protected int getGameHeight() {
return height;
}
protected int getMines() {
return this.myGame.getMines();
}
//public void setAction(Action a) {
// setAction(a, true);
//}
/**
* Register the action against the location(x,y);
* Optionally add the action to the list of actions to play this turn
*/
public void setAction(Action a) {
//display("Setting action at " + a.display());
if (action[a.x][a.y] != null) {
return;
}
action[a.x][a.y] = a;
if (a.getAction() == Action.FLAG) {
setMineFound(a);
}
if ((solver.getPlayStyle().flagless || solver.getPlayStyle().efficiency) && a.getAction() == Action.FLAG) {
// if it is flag free or efficiency and we have discovered a mine then don't flag it
} else if (isFlagOnBoard(a) && a.getAction() == Action.FLAG) {
// if the flag is already on the board then nothing to do
} else if (isFlagOnBoard(a) && a.getAction() == Action.CLEAR) {
// if a flag is blocking the clear move then remove the flag first
actionList.add(new Action(a, Action.FLAG, MoveMethod.CORRECTION, "Remove flag", BigDecimal.ONE, 0));
actionList.add(a);
} else {
actionList.add(a);
}
}
protected boolean alreadyActioned(Location l) {
return alreadyActioned(l.x, l.y);
}
protected boolean alreadyActioned(int x, int y) {
return (action[x][y] != null);
}
protected List<Action> getActions() {
return this.actionList;
}
/**
* This will consider chords when returning the moves to play
*/
/*
protected List<Action> getActionsWithChords() {
// if we aren't using chords or none are available then skip all this expensive processing
if (chordLocations.isEmpty() || !solver.isPlayChords()) {
return getActions();
}
List<Action> actions = new ArrayList<>();
boolean[][] processed = new boolean[myGame.getWidth()][myGame.getHeight()];
// sort the most beneficial chords to the top
Collections.sort(chordLocations, ChordLocation.SORT_BY_BENEFIT_DESC);
List<ChordLocation> toDelete = new ArrayList<>();
for (ChordLocation cl: chordLocations) {
int benefit = 0;
int cost = 0;
for (Location l: getAdjacentSquaresIterable(cl)) {
// flag not yet on board
if (!processed[l.x][l.y] && isConfirmedFlag(l) && !isFlagOnBoard(l)) {
cost++;
}
if (!processed[l.x][l.y] && isUnrevealed(l)) {
benefit++;
}
}
if (benefit - cost > 1) {
for (Location l: getAdjacentSquaresIterable(cl)) {
// flag not yet on board
if (!processed[l.x][l.y] && isConfirmedFlag(l) && !isFlagOnBoard(l)) {
actions.add(new Action(l, Action.FLAG, MoveMethod.TRIVIAL, "Place flag", BigDecimal.ONE, 0));
}
// flag on board in error
if (!processed[l.x][l.y] && !isConfirmedFlag(l) && isFlagOnBoard(l)) {
actions.add(new Action(l, Action.FLAG, MoveMethod.CORRECTION, "Remove flag", BigDecimal.ONE, 0));
}
processed[l.x][l.y] = true;
}
// now add the clear all
actions.add(new Action(cl, Action.CLEARALL, MoveMethod.TRIVIAL, "Clear All", BigDecimal.ONE, 1));
} else {
//toDelete.add(cl);
}
}
chordLocations.removeAll(toDelete);
// now add the the actions that haven't been resolved by a chord play
for (Action act: actionList) {
if (!processed[act.x][act.y]) {
processed[act.x][act.y] = true;
}
actions.add(act);
}
return actions;
}
*/
/**
* Get the probability of a mine being in this square (based upon the actions still pending)
*/
protected BigDecimal getProbability(int x, int y) {
for (Action act: actionList) {
if (act.x == x && act.y== y) {
if (act.getAction() == Action.FLAG) {
return BigDecimal.ZERO;
} else if (act.getAction() == Action.CLEAR) {
return act.getBigProb();
}
}
}
return null;
}
//protected int getActionsCount() {
// return this.actionList.size();
//}
/**
* Add a isolated dead tile
*/
//protected void addIsolatedDeadTile(Location loc) {
// isolatedDeadTiles.add(loc);
//}
//public int getIsolatedDeadTileCount() {
// return this.isolatedDeadTiles.size();
//}
/**
* Returns and removes the first Isolated Dead Tile in the set
*/
//public Location getIsolatedDeadTile() {
// for (Location loc: isolatedDeadTiles) {
// //isolatedDeadTiles.remove(loc);
// return loc;
// }
// return null;
//}
protected List<Location> getWitnesses(Collection<? extends Location> square) {
return new ArrayList<Location>(getWitnessesArea(square).getLocations());
}
/**
* From the given locations, generate all the revealed squares that can witness these locations
*/
protected Area getWitnessesArea(Collection<? extends Location> square) {
Set<Location> work = new HashSet<>(10);
for (Location loc: square) {
for (Location adj: this.getAdjacentSquaresIterable(loc)) {
// determine the number of distinct witnesses
if (isRevealed(adj)) {
work.add(adj);
}
}
}
return new Area(work);
}
/**
* From the given locations, generate an area containing all the un-revealed squares around them
*/
protected Area getUnrevealedArea(List<? extends Location> witnesses) {
return new Area(getUnrevealedSquaresDo(witnesses));
}
/**
* From the given locations, generate all the un-revealed squares around them
*/
protected List<Location> getUnrevealedSquares(List<? extends Location> witnesses) {
return new ArrayList<Location>(getUnrevealedSquaresDo(witnesses));
}
private Set<Location> getUnrevealedSquaresDo(List<? extends Location> witnesses) {
Set<Location> work = new HashSet<>(witnesses.size() * 3);
for (Location loc: witnesses) {
for (Location adj: this.getAdjacentSquaresIterable(loc)) {
if (isUnrevealed(adj)) {
work.add(adj);
}
}
}
return work;
}
/**
* Return all the unrevealed Locations on the board
*/
protected List<Location> getAllUnrevealedSquares() {
ArrayList<Location> work = new ArrayList<>(width * height);
for (int i=0; i < width; i++) {
for (int j=0; j < height; j++) {
if (isUnrevealed(i,j)) {
work.add(getLocation(i,j));
}
}
}
return work;
}
protected List<Location> getAllLivingWitnesses() {
return new ArrayList<>(livingWitnesses);
}
/**
* Return a list of Unrevealed Locations adjacent to this one
*/
protected List<Location> getAdjacentUnrevealedSquares(Location loc) {
return getAdjacentUnrevealedSquares(loc, 1);
}
/**
* Return an Area of un-revealed Locations adjacent to this one
*/
protected Area getAdjacentUnrevealedArea(Location loc) {
return new Area(getAdjacentUnrevealedSquares(loc, 1));
}
/**
* Return a list of Unrevealed Locations adjacent to this one
*/
protected List<Location> getAdjacentUnrevealedSquares(Location loc, int size ) {
ArrayList<Location> work = new ArrayList<>();
for (Location a: getAdjacentSquaresIterable(loc, size)) {
if (isUnrevealed(a)) {
work.add(a);
}
}
return work;
}
/**
* Return the Location for this position, avoids having to instantiate one
*/
protected Location getLocation(int x, int y) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
return null;
} else {
return cache.getLocation(x, y);
}
}
/**
* This returns the adjacent squares to a depth of size away. So (l,2) returns the 24 squares 2 deep surrounding l.
*/
protected Iterable<Location> getAdjacentSquaresIterable(Location loc, int size) {
if (size == 1) {
return getAdjacentSquaresIterable(loc);
} else {
return cache.adjacentLocations2[loc.x][loc.y];
}
}
protected Iterable<Location> getAdjacentSquaresIterable(Location loc) {
return cache.adjacentLocations1[loc.x][loc.y];
}
/**
* Done as part of validating Locations, must be undone using clearWitness()
*/
protected void setWitnessValue(Location l, int value) {
board[l.x][l.y] = value;
revealed[l.x][l.y] = true;
testMoveBalance++;
}
/**
* Done as part of validating Locations
*/
protected void clearWitness(Location l) {
board[l.x][l.y] = 0;
revealed[l.x][l.y] = false;
testMoveBalance--;
}
/**
* Method to read our own array of reveal values.
*/
protected int getWitnessValue(Location l) {
return getWitnessValue(l.x, l.y);
}
/**
* Method to read our own array of reveal values.
*/
protected int getWitnessValue(int x, int y) {
if (isUnrevealed(x,y)) {
throw new RuntimeException("Trying to get a witness value for an unrevealed square");
}
return board[x][y];
}
/**
* indicates a flag is on the board, but the solver can't confirm it
*/
protected boolean isUnconfirmedFlag(Location l) {
return isUnconfirmedFlag(l.x, l.y);
}
/**
* indicates a flag is on the board, but the solver can't confirm it
*/
protected boolean isUnconfirmedFlag(int x, int y) {
return flagOnBoard[x][y] && !confirmedMine[x][y];
}
/**
* indicates a flag is on the board
*/
protected boolean isFlagOnBoard(Location l) {
return isFlagOnBoard(l.x, l.y);
}
/**
* indicates a flag is on the board
*/
protected boolean isFlagOnBoard(int x, int y) {
return flagOnBoard[x][y];
}
protected void setFlagOnBoard(Location l) {
setFlagOnBoard(l.x, l.y);
}
protected void setFlagOnBoard(int x, int y) {
flagOnBoard[x][y] = true;
}
protected void setMineFound(Location loc) {
if (isConfirmedMine(loc)) {
return;
}
confirmedMinesTotal++;
confirmedMine[loc.x][loc.y] = true;
// if the flag isn't already on the board then this is also another on the total of all flags
if (!flagOnBoard[loc.x][loc.y]) {
totalFlags++;
}
// let all the adjacent squares know they have one more flag next to them and one less unrevealed location
for (Location a: getAdjacentSquaresIterable(loc)) {
adjFlagsConfirmed[a.x][a.y]++;
adjUnrevealed[a.x][a.y]--;
}
}
protected void unsetMineFound(Location loc) {
if (!isConfirmedMine(loc)) {
return;
}
confirmedMinesTotal--;
confirmedMine[loc.x][loc.y] = false;
// if the flag isn't already on the board then this is also another on the total of all flags
if (!flagOnBoard[loc.x][loc.y]) {
totalFlags--;
}
// let all the adjacent squares know they have one less mine next to them and one more unrevealed location
for (Location a: getAdjacentSquaresIterable(loc)) {
adjFlagsConfirmed[a.x][a.y]--;
adjUnrevealed[a.x][a.y]++;
}
}
/**
* Since Flag Free is a thing, we can't rely on the GameState to tell us where the flags are,
* so this replaces that.
*/
protected boolean isConfirmedMine(Location l) {
return isConfirmedMine(l.x, l.y);
}
/**
* Since Flag Free is a thing, we can't rely on the GameState to tell us where the flags are,
* so this replaces that.
*/
protected boolean isConfirmedMine(int x, int y) {
return confirmedMine[x][y];
}
/**
* Returns whether the location is revealed
*/
protected boolean isRevealed(Location l) {
return isRevealed(l.x, l.y);
}
/**
* Returns whether the location is revealed
*/
protected boolean isRevealed(int x, int y) {
return revealed[x][y];
}
/**
* Returns whether the location is unrevealed (neither revealed nor a confirmed flag)
*/
protected boolean isUnrevealed(Location l) {
return isUnrevealed(l.x, l.y);
}
/**
* Returns whether the location is unrevealed (neither revealed nor a confirmed flag)
*/
protected boolean isUnrevealed(int x, int y) {
return !confirmedMine[x][y] && !revealed[x][y];
}
/**
* count how many confirmed flags are adjacent to this square
*/
protected int countAdjacentConfirmedFlags(Location l) {
return countAdjacentConfirmedFlags(l.x, l.y);
}
/**
* count how many confirmed flags are adjacent to this square
*/
protected int countAdjacentConfirmedFlags(int x, int y) {
return adjFlagsConfirmed[x][y];
}
/**
* count how many flags are adjacent to this square on the board
*/
protected int countAdjacentFlagsOnBoard(Location l) {
return countAdjacentFlagsOnBoard(l.x, l.y);
}
/**
* count how many flags are adjacent to this square on the board
*/
protected int countAdjacentFlagsOnBoard(int x, int y) {
return adjFlagsOnBoard[x][y];
}
/**
* count how many confirmed and unconfirmed flags are adjacent to this square
*/
protected int countAllFlags(int x, int y) {
int result = 0;
for (int i=0; i < DX.length; i++) {
if (x + DX[i] >= 0 && x + DX[i] < width && y + DY[i] >= 0 && y + DY[i] < height) {
if (confirmedMine[x + DX[i]][y + DY[i]] || flagOnBoard[x + DX[i]][y + DY[i]]) {
result++;
}
}
}
return result;
}
/**
* count how many adjacent squares are neither flags nor revealed
*/
protected int countAdjacentUnrevealed(Location l) {
return countAdjacentUnrevealed(l.x, l.y);
}
/**
* count how many adjacent squares are neither confirmed flags nor revealed
*/
protected int countAdjacentUnrevealed(int x, int y) {
return adjUnrevealed[x][y];
}
/**
* count how many squares are neither confirmed flags nor revealed
*/
protected int getTotalUnrevealedCount() {
return numOfHidden;
}
/**
* Returns the number of mines the solver knows the location of
*/
protected int getConfirmedMineCount() {
return confirmedMinesTotal;
}
/**
* Number of flags on the board both confirmed and unconfirmed
*/
protected int getTotalFlagCount() {
return this.totalFlags;
}
protected int getUnplayedMoves(MoveMethod method) {
return unplayedMoves[method.ordinal()];
}
// check for flags which can be determined to be wrong
protected boolean validateData() {
for (int i=0; i < width; i++) {
for (int j=0; j < height; j++) {
// if there is an unconfirmed flag on the board but the solver
// thinks it is clear then the flag is wrong
if (isUnconfirmedFlag(i,j) && action[i][j] != null && action[i][j].getAction() == Action.CLEAR) {
getLogger().log(Level.INFO, "Flag in Error at (%d, %d) confirmed CLEAR", i, j);
return false;
}
if (isRevealed(i,j) && getWitnessValue(i,j) != 0) {
int flags = countAllFlags(i,j);
// if we have too many flags by a revealed square then a mistake has been made
if (getWitnessValue(i,j) < flags) {
getLogger().log(Level.INFO, "Flag in Error at witness (%d, %d) Overloads: Flags %d" ,i, j, flags);
return false;
}
}
}
}
return true;
}
/**
* Returns true if the board is consider high mine density
* @return
*/
public boolean isHighDensity() {
int minesLeft = getMines() - getConfirmedMineCount();
int tilesLeft = getTotalUnrevealedCount();
return (minesLeft * 5 > tilesLeft * 2) && Solver.CONSIDER_HIGH_DENSITY_STRATEGY;
}
public int getTestMoveBalance() {
return this.testMoveBalance;
}
protected Logger getLogger() {
return solver.logger;
}
//protected void display(String text) {
// solver.display(text);
//}
protected Solver getSolver() {
return solver;
}
}

View File

@ -0,0 +1,134 @@
package minesweeper.solver;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import minesweeper.structure.Location;
public class BoardStateCache {
private final static int[] DX = {0, 1, 1, 1, 0, -1, -1, -1};
private final static int[] DY = {-1, -1, 0, 1, 1, 1, 0, -1};
protected class Cache {
private int width;
private int height;
protected Location[][] locations;
protected AdjacentSquares[][] adjacentLocations1;
protected AdjacentSquares[][] adjacentLocations2;
protected Location getLocation(int x, int y) {
return locations[x][y];
}
}
// iterator for adjacent squares
protected class AdjacentSquares implements Iterable<Location> {
private Location loc;
private final int size;
private List<Location> locations;
AdjacentSquares(Cache cache, Location l, int width, int height, int size) {
this.loc = l;
this.size = size;
if (size == 1) {
locations = new ArrayList<>(8);
for (int i=0; i < DX.length; i++) {
if (loc.x + DX[i] >= 0 && loc.x + DX[i] < width && loc.y + DY[i] >= 0 && loc.y + DY[i] < height) {
locations.add(cache.getLocation(loc.x + DX[i], loc.y + DY[i]));
}
}
} else {
int startX = Math.max(0, loc.x - this.size);
int endX = Math.min(width - 1, loc.x + this.size);
int startY = Math.max(0, loc.y - this.size);
int endY = Math.min(height - 1, loc.y + this.size);
locations = new ArrayList<>((this.size * 2 - 1) * (this.size * 2 - 1));
for (int i=startX; i <= endX; i++) {
for (int j=startY; j <= endY; j++) {
if (i == loc.x && j == loc.y) {
// don't send back the central location
} else {
locations.add(cache.getLocation(i,j));
}
}
}
}
}
@Override
public Iterator<Location> iterator() {
return locations.iterator();
}
}
private static List<Cache> cacheAdjSqu = new ArrayList<>();
private static BoardStateCache me;
public synchronized Cache getAdjacentSquares1(int width, int height) {
for (Cache cache: cacheAdjSqu) {
if (cache.height == height && cache.width == width) {
return cache;
}
}
Cache cache = new Cache();
cache.height = height;
cache.width = width;
cache.locations = new Location[width][height];
// Create a Location for each entry yon the board
for (int x=0; x < width; x++) {
for (int y=0; y < height; y++) {
cache.locations[x][y] = new Location(x,y);
}
}
cache.adjacentLocations1 = new AdjacentSquares[width][height];
cache.adjacentLocations2 = new AdjacentSquares[width][height];
// set up how many adjacent locations there are to each square - they are all unrevealed to start with
for (int x=0; x < width; x++) {
for (int y=0; y < height; y++) {
cache.adjacentLocations1[x][y] = new AdjacentSquares(cache, cache.getLocation(x,y), width, height, 1);
cache.adjacentLocations2[x][y] = new AdjacentSquares(cache, cache.getLocation(x,y), width, height, 2);
}
}
cacheAdjSqu.add(cache);
return cache;
}
public static synchronized BoardStateCache getInstance() {
if (me == null) {
me = new BoardStateCache();
}
return me;
}
}

View File

@ -0,0 +1,464 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import Monitor.AsynchMonitor;
import minesweeper.solver.constructs.CandidateLocation;
import minesweeper.solver.constructs.Square;
import minesweeper.solver.constructs.Witness;
import minesweeper.solver.iterator.WitnessWebIterator;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Location;
public class BruteForce {
//private final static BigDecimal ZERO_THRESHOLD = new BigDecimal("0.25");
private final WitnessWeb web;
private final Solver solver;
private final BoardState boardState;
private final int mines;
private final BigInteger maxIterations;
private final int bfMaxSolutions;
private CrunchResult crunchResult;
private boolean hasRun = false;
private boolean certainClear = false;
//private final List<SuperLocation> zeroLocations = new ArrayList<>();
private final List<CandidateLocation> results = new ArrayList<>();
private final String scope;
private BigInteger iterations;
private BruteForceAnalysisModel bruteForceAnalysis;
public BruteForce(Solver solver, BoardState boardState, WitnessWeb web, int mines, BigInteger maxIterations, int bfMaxSolutions, String scope) {
this.solver = solver;
this.boardState = boardState;
this.maxIterations = maxIterations;
this.bfMaxSolutions = bfMaxSolutions;
this.scope = scope;
this.web = web;
this.mines = mines;
}
public void process() {
solver.logger.log(Level.INFO, "Brute force on %d Squares with %d mines", web.getSquares().size(), mines);
// if we have no mines to place then everything must be a clear
if (mines == 0 ) {
solver.logger.log(Level.INFO, "brute force but already found all the mines - clear all the remaining squares");
for (Square squ: web.getSquares()) {
results.add(new CandidateLocation(squ.x, squ.y, BigDecimal.ONE, boardState.countAdjacentUnrevealed(squ), boardState.countAdjacentConfirmedFlags(squ)));
}
iterations = BigInteger.ONE;
hasRun = true;
return;
}
// now doing this logic 'just in time' rather than always at witness web generation
web.generateIndependentWitnesses();
// and crunch the result if we have something to check against
if (web.getPrunedWitnesses().size() >= 0) {
iterations = web.getIterations(mines);
if (iterations.compareTo(maxIterations) <= 0) {
//display("Brute Force about to process " + iterations + " iterations");
WitnessWebIterator[] iterators = buildParallelIterators(mines, iterations);
this.bruteForceAnalysis = new BruteForceAnalysis(solver, iterators[0].getLocations(), bfMaxSolutions, scope, solver.bfdaStartLocations());
crunchResult = crunchParallel(web.getSquares(), web.getPrunedWitnesses(), true, iterators);
// if there are too many to process then don't bother
if (this.bruteForceAnalysis != null && this.bruteForceAnalysis.tooMany()) {
this.bruteForceAnalysis = null;
}
int actIterations = 0;
for (WitnessWebIterator i: iterators) {
actIterations = actIterations + i.getIterations();
}
solver.logger.log(Level.DEBUG, "Expected iterations %d Actual iterations %d", iterations, actIterations);
solver.logger.log(Level.INFO, "Found %d candidate solutions in the %s", crunchResult.bigGoodCandidates, scope);
certainClear = findCertainClear(crunchResult);
if (certainClear) {
this.bruteForceAnalysis = null;
}
hasRun = true;
//TODO zero additional mines calculater - do we want it?
/*
if (crunchResult.bigGoodCandidates.signum() != 0) {
BigInteger hwm = BigInteger.ZERO;
int best = -1;
for (int i=0; i < crunchResult.getSquare().size(); i++) {
Location loc = crunchResult.getSquare().get(i);
int adjacentMines = boardState.countAdjacentConfirmedFlags(loc);
BigDecimal prob = new BigDecimal(crunchResult.bigDistribution[i][adjacentMines]).divide(new BigDecimal(crunchResult.bigGoodCandidates), Solver.DP, RoundingMode.HALF_UP);
if (prob.compareTo(ZERO_THRESHOLD) >= 0) {
SuperLocation zl = new SuperLocation(loc.x, loc.y, boardState.countAdjacentUnrevealed(loc), adjacentMines);
zl.setProbability(prob);
zeroLocations.add(zl);
}
if (crunchResult.bigDistribution[i][adjacentMines].compareTo(hwm) > 0) {
hwm = crunchResult.bigDistribution[i][adjacentMines];
best = i;
}
}
if (best != -1) {
BigDecimal prob = new BigDecimal(hwm).divide(new BigDecimal(crunchResult.bigGoodCandidates), Solver.DP, RoundingMode.HALF_UP);
boardState.display("Location " + crunchResult.getSquare().get(best).display() + " is a 'zero additional mines' with probability " + prob);
}
}
*/
} else {
if (maxIterations.signum() != 0) {
solver.logger.log(Level.INFO, "Brute Force too large with %d iterations", iterations);
}
}
} else {
solver.logger.log(Level.INFO, "Brute Force not performed since there are no witnesses");
}
}
// break a witness web search into a number of non-overlapping iterators
private WitnessWebIterator[] buildParallelIterators(int mines, BigInteger totalIterations) {
solver.logger.log(Level.DEBUG, "Building parallel iterators");
//WitnessWebIterator[] result1 = new WitnessWebIterator[1];
//result1[0] = new WitnessWebIterator(web, mines);
//return result1;
solver.logger.log(Level.DEBUG, "Non independent iterations %d", web.getNonIndependentIterations(mines));
// if there is only one cog then we can't lock it,so send back a single iterator
if (web.getIndependentWitnesses().size() == 1 && web.getIndependentMines() >= mines || totalIterations.compareTo(Solver.PARALLEL_MINIMUM) < 0
|| web.getPrunedWitnesses().size() == 0 || solver.preferences.isSingleThread()) {
solver.logger.log(Level.DEBUG, "Only a single iterator will be used");
WitnessWebIterator[] result = new WitnessWebIterator[1];
result[0] = new WitnessWebIterator(web, mines);
return result;
}
int witMines = web.getIndependentWitnesses().get(0).getMines();
int squares = web.getIndependentWitnesses().get(0).getSquares().size();
BigInteger bigIterations = Solver.combination(witMines, squares);
int iter = bigIterations.intValue();
solver.logger.log(Level.DEBUG, "The first cog has %d iterations, so parallel processing is possible", iter);
WitnessWebIterator[] result = new WitnessWebIterator[iter];
for (int i=0; i < iter; i++) {
result[i] = new WitnessWebIterator(web, mines, i); // create a iterator with a lock first got at position i
}
return result;
}
// process the iterators in parallel
private CrunchResult crunchParallel(List<Square> square, List<Witness> witness, boolean calculateDistribution, WitnessWebIterator... iterator) {
solver.logger.log(Level.DEBUG, "At parallel iterator processing");
Cruncher[] crunchers = new Cruncher[iterator.length];
for (int i=0; i < iterator.length; i++) {
crunchers[i] = new Cruncher(boardState, iterator[i].getLocations(), witness, iterator[i], false, bruteForceAnalysis);
}
AsynchMonitor monitor = new AsynchMonitor(crunchers);
monitor.setMaxThreads(Solver.CORES);
try {
monitor.startAndWait();
} catch (Exception ex) {
solver.logger.log(Level.ERROR, "Parallel processing caused an error: %s", ex.getMessage());
ex.printStackTrace();
}
CrunchResult[] results = new CrunchResult[crunchers.length];
for (int i=0; i < crunchers.length; i++) {
results[i] = crunchers[i].getResult();
}
CrunchResult result = CrunchResult.bigMerge(results);
return result;
}
private boolean findCertainClear(CrunchResult output) {
// if there were no good candidates then there is nothing to check
if (output.bigGoodCandidates.signum() == 0) {
return false;
}
// check the tally information to see if we have a square where a
// mine is never present
for (int i=0; i < output.bigTally.length; i++) {
if (output.bigTally[i].signum() == 0) {
return true;
}
}
return false;
}
/*
// do the tally check using the BigInteger values
private List<CandidateLocation> checkBigTally(CrunchResult output) {
List<CandidateLocation> result = new ArrayList<>();
// if there were no good candidates then there is nothing to check
if (output.bigGoodCandidates.compareTo(BigInteger.ZERO) == 0) {
return result;
}
// 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 there is always a mine here then odds of clear is zero
if (output.bigTally[i].compareTo(output.bigGoodCandidates) == 0) {
int x = output.getSquare().get(i).x;
int y = output.getSquare().get(i).y;
results.add(new CandidateLocation(x, y, BigDecimal.ZERO, boardState.countAdjacentUnrevealed(x, y), boardState.countAdjacentConfirmedFlags(x, y)));
// if never a mine then odds of clear is one
} else if (output.bigTally[i].compareTo(BigInteger.ZERO) == 0) {
int x = output.getSquare().get(i).x;
int y = output.getSquare().get(i).y;
results.add(new CandidateLocation(x, y, BigDecimal.ONE, boardState.countAdjacentUnrevealed(x, y), boardState.countAdjacentConfirmedFlags(x, y)));
certainClear = true;
}
}
return result;
}
*/
/*
public List<CandidateLocation> getBestSolutions(BigDecimal freshhold) {
if (crunchResult == null) {
return results;
}
if (!results.isEmpty()) {
return results;
}
List<CandidateLocation> candidates = new ArrayList<>();
boolean ignoreBad = true;
if (crunchResult.getMaxCount() <= 1) {
ignoreBad = false;
solver.display("No candidates provide additional information");
}
// Calculate the probability of a mine being in the square and store in a list
for (int i=0; i < crunchResult.bigTally.length; i++) {
BigDecimal mine = new BigDecimal(crunchResult.bigTally[i]).divide(new BigDecimal(crunchResult.bigGoodCandidates), Solver.DP, RoundingMode.HALF_UP);
BigDecimal notMine = BigDecimal.ONE.subtract(mine);
Location l = crunchResult.getSquare().get(i);
// ignore candidates that yield no info, unless none do or they are certainties
if (crunchResult.getBigCount()[i] > 1 || !ignoreBad || notMine.compareTo(BigDecimal.ZERO) == 0 || notMine.compareTo(BigDecimal.ONE) == 0) {
candidates.add(new CandidateLocation(l.x, l.y, notMine, boardState.countAdjacentUnrevealed(l), boardState.countAdjacentConfirmedFlags(l), crunchResult.getBigCount()[i]));
} else {
solver.display(l.display() + " clear probability " + notMine + " discarded because it reveals no further information");
}
//candidates.add(new CandidateLocation(l.x, l.y, notMine, boardState.countAdjacentUnrevealed(l), boardState.countAdjacentConfirmedFlags(l)));
}
// sort the candidates into descending order by probability
Collections.sort(candidates, CandidateLocation.SORT_BY_PROB_FLAG_FREE);
BigDecimal hwm = candidates.get(0).getProbability();
BigDecimal tolerence;
if (hwm.compareTo(BigDecimal.ONE) == 0) {
tolerence = hwm;
} else {
tolerence = hwm.multiply(freshhold);
}
for (CandidateLocation cl: candidates) {
if (cl.getProbability().compareTo(tolerence) >= 0) {
results.add(cl);
} else {
break;
}
}
boardState.display("Best Guess: " + candidates.size() + " candidates, " + results.size() + " passed tolerence at " + tolerence);
return results;
}
*/
public boolean hasRun() {
return this.hasRun;
}
public boolean hasCertainClear() {
return this.certainClear;
}
public CrunchResult getCrunchResult() {
return this.crunchResult;
}
public BigInteger getSolutionCount() {
if (crunchResult == null) {
return BigInteger.ONE;
}
return crunchResult.bigGoodCandidates;
}
public BigInteger getIterations() {
return this.iterations;
}
public int getTileCount() {
return web.getSquares().size();
}
public BruteForceAnalysisModel getBruteForceAnalysis() {
return bruteForceAnalysis;
}
/**
* Set the probability for the probabilityLocation being satisfied
* @param list
*/
/*
public <T extends ProbabilityLocation> List<T> setProbabilities(List<T> list) {
if (!hasRun) {
return list;
}
List<T> output = new ArrayList<>();
for (ProbabilityLocation pl: list) {
for (int i=0; i < crunchResult.getSquare().size(); i++) {
if (crunchResult.getSquare().get(i).equals(pl)) {
// get the values which are good for this location
int[] adjFlagsRequired = pl.getAdjacentFlagsRequired();
// count the number of solutions which have those values
BigInteger count = BigInteger.ZERO;
for (int j = 0; j < adjFlagsRequired.length; j++) {
count = count.add(crunchResult.bigDistribution[i][adjFlagsRequired[j]]);
}
// work out the % chance of it happening or if zero chance discard the location
if (count.signum() != 0) {
BigDecimal prob = new BigDecimal(count).divide(new BigDecimal(crunchResult.bigGoodCandidates), Solver.DP, RoundingMode.HALF_UP);
pl.setProbability(prob);
boardState.display(pl.display() + " has probability " + prob);
output.add((T) pl);
} else {
boardState.display(pl.display() + " has probability zero and is being discarded");
}
break;
}
}
}
return output;
}
*/
/**
* Returns the probability that this square is not a mine
*/
public BigDecimal getProbability(int x, int y) {
Location l = this.boardState.getLocation(x,y);
if (crunchResult == null) { // this can happen if there are no mines left to find, so everything is a clear
for (Location loc: web.getSquares()) { // if the mouse is hovering over one of the brute forced squares
if (loc.equals(l)) {
return BigDecimal.ONE;
}
}
return BigDecimal.ZERO;
}
for (int i=0; i < crunchResult.getSquare().size(); i++) {
if (crunchResult.getSquare().get(i).equals(l)) {
BigDecimal prob = new BigDecimal(crunchResult.bigTally[i]).divide(new BigDecimal(crunchResult.bigGoodCandidates), Solver.DP, RoundingMode.HALF_UP);
return BigDecimal.ONE.subtract(prob);
}
}
return BigDecimal.ZERO;
}
}

View File

@ -0,0 +1,981 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import minesweeper.gamestate.GameStateModel;
import minesweeper.gamestate.MoveMethod;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Action;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
public class BruteForceAnalysis extends BruteForceAnalysisModel{
// used to hold all the solutions left in the game
private class SolutionTable {
private final byte[][] solutions;
private int size = 0;
private SolutionTable(int maxSize) {
solutions = new byte[maxSize][];
}
private void addSolution(byte[] solution) {
solutions[size] = solution;
size++;
};
private int size() {
return size;
}
private byte[] get(int index) {
return solutions[index];
}
private void sortSolutions(int start, int end, int index) {
Arrays.sort(solutions, start, end, sorters[index]);
}
}
/**
* This sorts solutions by the value of a position
*/
private class SortSolutions implements Comparator<byte[]> {
private final int sortIndex;
public SortSolutions(int index) {
sortIndex = index;
}
@Override
public int compare(byte[] o1, byte[] o2) {
return o1[sortIndex] - o2[sortIndex];
}
}
/**
* A key to uniquely identify a position
*/
private class Position {
private final byte[] position;
private int hash;
private Position() {
position = new byte[locations.size()];
for (int i=0; i < position.length; i++) {
position[i] = 15;
}
}
private Position(Position p, int index, int value) {
// copy and update to reflect the new position
position = Arrays.copyOf(p.position, p.position.length);
position[index] = (byte) (value + 50);
}
@Override
// copied from String hash
public int hashCode() {
int h = hash;
if (h == 0 && position.length > 0) {
for (int i = 0; i < position.length; i++) {
h = 31 * h + position[i];
}
hash = h;
}
return h;
}
@Override
public boolean equals(Object o) {
if (o instanceof Position) {
for (int i=0; i < position.length; i++) {
if (this.position[i] != ((Position) o).position[i]) {
return false;
}
}
return true;
} else {
return false;
}
}
}
/**
* Positions on the board which can still reveal information about the game.
*/
private class LivingLocation implements Comparable<LivingLocation>{
//private int winningLines = 0;
private boolean pruned = false;
private final short index;
private int mineCount = 0; // number of remaining solutions which have a mine in this position
private int maxSolutions = 0; // the maximum number of solutions that can be remaining after clicking here
private int zeroSolutions = 0; // the number of solutions that have a '0' value here
private byte maxValue = -1;
private byte minValue = -1;
private byte count; // number of possible values at this location
private Node[] children;
private LivingLocation(short index) {
this.index = index;
}
/**
* Determine the Nodes which are created if we play this move. Up to 9 positions where this locations reveals a value [0-8].
* @param location
* @return
*/
private void buildChildNodes(Node parent) {
// sort the solutions by possible values
allSolutions.sortSolutions(parent.startLocation, parent.endLocation, this.index);
int index = parent.startLocation;
// skip over the mines
while (index < parent.endLocation && allSolutions.get(index)[this.index] == GameStateModel.MINE) {
index++;
}
Node[] work = new Node[9];
for (int i=this.minValue; i < this.maxValue + 1; i++) {
// if the node is in the cache then use it
Position pos = new Position(parent.position, this.index, i);
Node temp1 = cache.get(pos);
if (temp1 == null) {
Node temp = new Node(pos);
temp.startLocation = index;
// find all solutions for this values at this location
while (index < parent.endLocation && allSolutions.get(index)[this.index] == i) {
index++;
}
temp.endLocation = index;
work[i] = temp;
} else {
//System.out.println("In cache " + temp.position.key + " " + temp1.position.key);
//if (!temp.equals(temp1)) {
// System.out.println("Cache not equal!!");
//}
//temp1.fromCache = true;
work[i] = temp1;
cacheHit++;
cacheWinningLines = cacheWinningLines + temp1.winningLines;
// skip past these details in the array
while (index < parent.endLocation && allSolutions.get(index)[this.index] <= i) {
index++;
}
}
}
if (index != parent.endLocation) {
System.out.println("Didn't read all the elements in the array; index = " + index + " end = " + parent.endLocation);
}
for (int i=this.minValue; i <= this.maxValue; i++) {
if (work[i].getSolutionSize() > 0) {
//if (!work[i].fromCache) {
// work[i].determineLivingLocations(this.livingLocations, living.index);
//}
} else {
work[i] = null; // if no solutions then don't hold on to the details
}
}
this.children = work;
}
@Override
public int compareTo(LivingLocation o) {
// return location most likely to be clear - this has to be first, the logic depends upon it
int test = this.mineCount - o.mineCount;
if (test != 0) {
return test;
}
// then the location most likely to have a zero
test = o.zeroSolutions - this.zeroSolutions;
if (test != 0) {
return test;
}
// then by most number of different possible values
test = o.count - this.count;
if (test != 0) {
return test;
}
// then by the maxSolutions - ascending
return this.maxSolutions - o.maxSolutions;
}
}
/**
* A representation of a possible state of the game
*/
private class Node {
private Position position ; // representation of the position we are analysing / have reached
private int winningLines = 0; // this is the number of winning lines below this position in the tree
private int work = 0; // this is a measure of how much work was needed to calculate WinningLines value
private boolean fromCache = false; // indicates whether this position came from the cache
private int startLocation; // the first solution in the solution array that applies to this position
private int endLocation; // the last + 1 solution in the solution array that applies to this position
private List<LivingLocation> livingLocations; // these are the locations which need to be analysed
private LivingLocation bestLiving; // after analysis this is the location that represents best play
private Node() {
position = new Position();
}
private Node(Position position) {
this.position = position;
}
private List<LivingLocation> getLivingLocations() {
return livingLocations;
}
private int getSolutionSize() {
return endLocation - startLocation;
}
/**
* Get the probability of winning the game from the position this node represents (winningLines / solution size)
* @return
*/
private BigDecimal getProbability() {
return BigDecimal.valueOf(winningLines).divide(BigDecimal.valueOf(getSolutionSize()), Solver.DP, RoundingMode.HALF_UP);
}
/**
* Calculate the number of winning lines if this move is played at this position
* Used at top of the game tree
*/
private int getWinningLines(LivingLocation move) {
//if we can never exceed the cutoff then no point continuing
if (Solver.PRUNE_BF_ANALYSIS && this.getSolutionSize() - move.mineCount <= this.winningLines) {
move.pruned = true;
return (this.getSolutionSize() - move.mineCount);
}
int winningLines = getWinningLines(1, move, this.winningLines);
if (winningLines > this.winningLines) {
this.winningLines = winningLines;
}
return winningLines;
}
/**
* Calculate the number of winning lines if this move is played at this position
* Used when exploring the game tree
*/
private int getWinningLines(int depth, LivingLocation move, int cutoff) {
int result = 0;
int notMines = this.getSolutionSize() - move.mineCount;
// if the max possible winning lines is less than the current cutoff then no point doing the analysis
if (Solver.PRUNE_BF_ANALYSIS && notMines <= cutoff) {
move.pruned = true;
return notMines;
}
// we're going to have to do some work
processCount++;
if (processCount > maxProcessCount) {
return 0;
}
move.buildChildNodes(this);
for (Node child: move.children) {
if (child == null) {
continue; // continue the loop but ignore this entry
}
if (child.fromCache) { // nothing more to do, since we did it before
this.work++;
} else {
child.determineLivingLocations(this.livingLocations, move.index);
this.work++;
if (child.getLivingLocations().isEmpty()) { // no further information ==> all solution indistinguishable ==> 1 winning line
child.winningLines = 1;
} else { // not cached and not terminal node, so we need to do the recursion
for (LivingLocation childMove: child.getLivingLocations()) {
// if the number of safe solutions <= the best winning lines then we can't do any better, so skip the rest
if (child.getSolutionSize() - childMove.mineCount <= child.winningLines) {
break;
}
// now calculate the winning lines for each of these children
int winningLines = child.getWinningLines(depth + 1, childMove, child.winningLines);
if (!childMove.pruned) {
if (child.winningLines < winningLines || (child.bestLiving != null && child.winningLines == winningLines && child.bestLiving.mineCount < childMove.mineCount)) {
child.winningLines = winningLines;
child.bestLiving = childMove;
}
}
// if there are no mines then this is a 100% safe move, so skip any further analysis since it can't be any better
if (childMove.mineCount == 0) {
break;
}
}
// no need to hold onto the living location once we have determined the best of them
child.livingLocations = null;
//if (depth > solver.preferences.BRUTE_FORCE_ANALYSIS_TREE_DEPTH) { // stop holding the tree beyond this depth
// child.bestLiving = null;
//}
// add the child to the cache if it didn't come from there and it is carrying sufficient winning lines
if (child.work > 30) {
child.work = 0;
child.fromCache = true;
cacheSize++;
cache.put(child.position, child);
} else {
this.work = this.work + child.work;
}
}
}
if (depth > solver.preferences.getBruteForceTreeDepth()) { // stop holding the tree beyond this depth
child.bestLiving = null;
}
// store the aggregate winning lines
result = result + child.winningLines;
notMines = notMines - child.getSolutionSize(); // reduce the number of not mines
// if the max possible winning lines is less than the current cutoff then no point doing the analysis
if (Solver.PRUNE_BF_ANALYSIS && result + notMines <= cutoff) {
move.pruned = true;
return (result + notMines);
}
}
return result;
}
/**
* this generates a list of Location that are still alive, (i.e. have more than one possible value) from a list of previously living locations
* Index is the move which has just been played (in terms of the off-set to the position[] array)
*/
private void determineLivingLocations(List<LivingLocation> liveLocs, int index) {
List<LivingLocation> living = new ArrayList<>(liveLocs.size());
for (LivingLocation live: liveLocs) {
if (live.index == index) { // if this is the same move we just played then no need to analyse it - definitely now non-living.
continue;
}
int value;
int valueCount[] = resetValues(0);
int mines = 0;
int maxSolutions = 0;
byte count = 0;
byte minValue = 0;
byte maxValue = 0;
for (int j=startLocation; j < endLocation; j++) {
value = allSolutions.get(j)[live.index];
if (value != GameStateModel.MINE) {
//values[value] = true;
valueCount[value]++;
} else {
mines++;
}
}
// find the new minimum value and maximum value for this location (can't be wider than the previous min and max)
for (byte j=live.minValue; j <= live.maxValue; j++) {
if (valueCount[j] > 0) {
if (count == 0) {
minValue = j;
}
maxValue = j;
count++;
if (maxSolutions < valueCount[j]) {
maxSolutions = valueCount[j];
}
}
}
if (count > 1) {
LivingLocation alive = new LivingLocation(live.index);
alive.mineCount = mines;
alive.count = count;
alive.minValue = minValue;
alive.maxValue = maxValue;
alive.maxSolutions = maxSolutions;
alive.zeroSolutions = valueCount[0];
living.add(alive);
}
}
Collections.sort(living);
this.livingLocations = living;
}
@Override
public int hashCode() {
return position.hashCode();
}
@Override
public boolean equals(Object o) {
if (o instanceof Node) {
return position.equals(((Node) o).position);
} else {
return false;
}
}
}
private class ProcessedMove implements Comparable<ProcessedMove> {
private final Location location;
private final int winningLines;
private final boolean pruned;
private ProcessedMove(Location loc, int winningLines, boolean pruned) {
this.location = loc;
this.winningLines = winningLines;
this.pruned = pruned;
}
@Override
public int compareTo(ProcessedMove o) {
int c = o.winningLines - this.winningLines;
if (c == 0) {
if (!this.pruned && o.pruned) {
c = -1;
} else if (this.pruned && !o.pruned) {
c = 1;
} else {
c = 0;
}
}
return c;
}
}
private static final String INDENT = "................................................................................";
private static final BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);
private long processCount = 0;
private long maxProcessCount;
private long processCountExtension;
private int movesProcessed = 0;
private int movesToProcess = 0;
private final Solver solver;
private final int maxSolutionSize;
//private Node top;
private final List<? extends Location> locations; // the positions being analysed
private final List<? extends Location> startLocations; // the positions which will be considered for the first move
private final List<ProcessedMove> processedMoves = new ArrayList<>(); // moves which have been processed
private final SolutionTable allSolutions;
private final String scope;
private Node currentNode;
private Location expectedMove;
private final SortSolutions[] sorters;
private int cacheHit = 0;
private int cacheSize = 0;
private int cacheWinningLines = 0;
private boolean allDead = false; // this is true if all the locations are dead
private Area deadLocations = Area.EMPTY_AREA;
// some work areas to prevent having to instantiate many 1000's of copies of them
//private final boolean[] values = new boolean[9];
private final int[][] valueCount = new int[2][9];
private Map<Position, Node> cache = new HashMap<>(5000);
public BruteForceAnalysis(Solver solver, List<? extends Location> locations, int size, String scope, List<Location> startLocations) {
this.solver = solver;
this.locations = locations;
this.maxSolutionSize = size;
this.scope = scope;
this.allSolutions = new SolutionTable(size);
//this.top = new Node();
this.sorters = new SortSolutions[locations.size()];
for (int i=0; i < sorters.length; i++) {
this.sorters[i] = new SortSolutions(i);
}
this.startLocations = startLocations;
}
// this can be called by different threads when brute force is running on multiple threads
@Override
protected synchronized void addSolution(byte[] solution) {
if (solution.length != locations.size()) {
throw new RuntimeException("Solution does not have the correct number of locations");
}
if (allSolutions.size() >= maxSolutionSize) {
if (!tooMany) {
solver.logger.log(Level.WARN, "BruteForceAnalysis solution table overflow after %d solutions found (%s)", allSolutions.size(), this.scope);
}
tooMany = true;
return;
}
/*
String text = "";
for (int i=0; i < solution.length; i++) {
text = text + solution[i] + " ";
}
solver.display(text);
*/
allSolutions.addSolution(solution);
}
@Override
protected void process() {
long start = System.currentTimeMillis();
solver.logger.log(Level.INFO, "----- Brute Force Deep Analysis starting ----");
solver.logger.log(Level.INFO, "%d solutions in BruteForceAnalysis", allSolutions.size());
// create the top node
Node top = buildTopNode(allSolutions);
int best = 0;
if (top.getLivingLocations().isEmpty()) {
allDead = true;
best = 1; // only 1 winning line if everything is dead
}
this.movesToProcess = top.getLivingLocations().size();
this.maxProcessCount = solver.preferences.getBruteForceMaxNodes();
if (startLocations == null || startLocations.size() == 0) {
this.processCountExtension = this.maxProcessCount / 2;
} else {
this.processCountExtension = 0;
}
for (LivingLocation move: top.getLivingLocations()) {
// check that the move is in the startLocation list
if (startLocations != null) {
boolean found = false;
for (Location l: startLocations) {
if (locations.get(move.index).equals(l)) {
found = true;
break;
}
}
if (!found) { // if not then skip this move
solver.logger.log(Level.INFO, "%d %s is not a starting location", move.index, locations.get(move.index));
continue;
}
}
int winningLines = top.getWinningLines(move); // calculate the number of winning lines if this move is played
if (!move.pruned) {
if (best < winningLines || (top.bestLiving != null && best == winningLines && top.bestLiving.mineCount < move.mineCount)) {
best = winningLines;
top.bestLiving = move;
}
}
BigDecimal singleProb = BigDecimal.valueOf(allSolutions.size() - move.mineCount).divide(BigDecimal.valueOf(allSolutions.size()), Solver.DP, RoundingMode.HALF_UP);
if (move.pruned) {
solver.logger.log(Level.INFO, "%d %s is living with %d possible values and probability %s, this location was pruned (max winning lines %d)", move.index, locations.get(move.index), move.count, percentage(singleProb), winningLines);
} else {
solver.logger.log(Level.INFO, "%d %s is living with %d possible values and probability %s, winning lines %d", move.index, locations.get(move.index), move.count, percentage(singleProb), winningLines);
}
if (processCount < this.maxProcessCount) {
movesProcessed++;
Location loc = this.locations.get(move.index);
processedMoves.add(new ProcessedMove(loc, winningLines, move.pruned));
// if we've got to half way then allow extra cycles to finish up
if (this.processCountExtension !=0 && this.movesProcessed * 2 > this.movesToProcess) {
this.maxProcessCount = this.maxProcessCount + this.processCountExtension;
this.processCountExtension = 0;
solver.logger.log(Level.INFO, "Extending BFDA cycles to %d after %d of %d moves analysed", this.maxProcessCount, this.movesProcessed, this.movesToProcess);
}
}
}
// sort the processed moves into best move at the top
processedMoves.sort(null); // use the comparable method to sort
top.winningLines = best;
currentNode = top;
if (processCount < this.maxProcessCount) {
this.completed = true;
if (solver.isShowProbabilityTree()) {
solver.newLine("--------- Probability Tree dump start ---------");
showTree(0, 0, top);
solver.newLine("---------- Probability Tree dump end ----------");
}
}
// clear down the cache
cache.clear();
long end = System.currentTimeMillis();
solver.logger.log(Level.INFO, "Total nodes in cache %d, total cache hits %d, total winning lines saved %d", cacheSize, cacheHit, this.cacheWinningLines);
solver.logger.log(Level.INFO, "process took %d milliseconds and explored %d nodes", (end - start), processCount);
solver.logger.log(Level.INFO, "----- Brute Force Deep Analysis finished ----");
}
/**
* Builds a top of tree node based on the solutions provided
*/
private Node buildTopNode(SolutionTable solutionTable) {
List<Location> deadLocations = new ArrayList<>();
Node result = new Node();
result.startLocation = 0;
result.endLocation = solutionTable.size();
List<LivingLocation> living = new ArrayList<>();
for (short i=0; i < locations.size(); i++) {
int value;
int valueCount[] = resetValues(0);
int mines = 0;
int maxSolutions = 0;
byte count = 0;
byte minValue = 0;
byte maxValue = 0;
for (int j=0; j < result.getSolutionSize(); j++) {
if (solutionTable.get(j)[i] != GameStateModel.MINE) {
value = solutionTable.get(j)[i];
//values[value] = true;
valueCount[value]++;
} else {
mines++;
}
}
for (byte j=0; j < valueCount.length; j++) {
if (valueCount[j] > 0) {
if (count == 0) {
minValue = j;
}
maxValue = j;
count++;
if (maxSolutions < valueCount[j]) {
maxSolutions = valueCount[j];
}
}
}
if (count > 1) {
LivingLocation alive = new LivingLocation(i);
alive.mineCount = mines;
alive.count = count;
alive.minValue = minValue;
alive.maxValue = maxValue;
alive.maxSolutions = maxSolutions;
alive.zeroSolutions = valueCount[0];
living.add(alive);
} else {
if (mines == result.getSolutionSize()) {
solver.logger.log(Level.INFO, "Tile %s is a mine", locations.get(i));
} else {
solver.logger.log(Level.INFO, "Tile %s is dead with value %d", locations.get(i), minValue);
deadLocations.add(locations.get(i));
}
}
}
Collections.sort(living);
result.livingLocations = living;
this.deadLocations = new Area(deadLocations);
return result;
}
private int[] resetValues(int thread) {
for (int i=0; i < valueCount[thread].length; i++) {
valueCount[thread][i] = 0;
}
return valueCount[thread];
}
@Override
protected int getSolutionCount() {
return allSolutions.size();
}
@Override
protected long getNodeCount() {
return processCount;
}
@Override
protected Action getNextMove(BoardState boardState) {
LivingLocation bestLiving = getBestLocation(currentNode);
if (bestLiving == null) {
return null;
}
Location loc = this.locations.get(bestLiving.index);
//solver.display("first best move is " + loc.display());
BigDecimal prob = BigDecimal.ONE.subtract(BigDecimal.valueOf(bestLiving.mineCount).divide(BigDecimal.valueOf(currentNode.getSolutionSize()), Solver.DP, RoundingMode.HALF_UP));
while (boardState.isRevealed(loc)) {
int value = boardState.getWitnessValue(loc);
currentNode = bestLiving.children[value];
bestLiving = getBestLocation(currentNode);
if (bestLiving == null) {
return null;
}
prob = BigDecimal.ONE.subtract(BigDecimal.valueOf(bestLiving.mineCount).divide(BigDecimal.valueOf(currentNode.getSolutionSize()), Solver.DP, RoundingMode.HALF_UP));
loc = this.locations.get(bestLiving.index);
}
solver.logger.log(Level.INFO, "Solutions with mines is %d out of %d", bestLiving.mineCount, currentNode.getSolutionSize());
for (int i=0; i < bestLiving.children.length; i++) {
if (bestLiving.children[i] == null) {
//solver.display("Value of " + i + " is not possible");
continue; //ignore this node but continue the loop
}
String probText;
if (bestLiving.children[i].bestLiving == null) {
probText = Action.FORMAT_2DP.format(ONE_HUNDRED.divide(BigDecimal.valueOf(bestLiving.children[i].getSolutionSize()), Solver.DP, RoundingMode.HALF_UP)) + "%";
} else {
probText = Action.FORMAT_2DP.format(bestLiving.children[i].getProbability().multiply(ONE_HUNDRED)) + "%";
}
solver.logger.log(Level.INFO, "Value of %d leaves %d solutions and winning probability %s", i, bestLiving.children[i].getSolutionSize(), probText);
}
String text = " (solve " + scope + " " + Action.FORMAT_2DP.format(currentNode.getProbability().multiply(ONE_HUNDRED)) + "%)";
Action action = new Action(loc, Action.CLEAR, MoveMethod.BRUTE_FORCE_DEEP_ANALYSIS, text, prob);
expectedMove = loc;
return action;
}
private LivingLocation getBestLocation(Node node) {
return node.bestLiving;
}
private void showTree(int depth, int value, Node node) {
String condition;
if (depth == 0) {
condition = node.getSolutionSize() + " solutions remain";
} else {
condition = "When '" + value + "' ==> " + node.getSolutionSize() + " solutions remain";
}
if (node.bestLiving == null) {
String line = INDENT.substring(0, depth*3) + condition + " Solve chance " + Action.FORMAT_2DP.format(node.getProbability().multiply(ONE_HUNDRED)) + "%";
System.out.println(line);
solver.newLine(line);
return;
}
Location loc = this.locations.get(node.bestLiving.index);
BigDecimal prob = BigDecimal.ONE.subtract(BigDecimal.valueOf(node.bestLiving.mineCount).divide(BigDecimal.valueOf(node.getSolutionSize()), Solver.DP, RoundingMode.HALF_UP));
String line = INDENT.substring(0, depth*3) + condition + " play " + loc.toString() + " Survival chance " + Action.FORMAT_2DP.format(prob.multiply(ONE_HUNDRED)) + "%, Solve chance " + Action.FORMAT_2DP.format(node.getProbability().multiply(ONE_HUNDRED)) + "%";
System.out.println(line);
solver.newLine(line);
//for (Node nextNode: node.bestLiving.children) {
for (int val=0; val < node.bestLiving.children.length; val++) {
Node nextNode = node.bestLiving.children[val];
if (nextNode != null) {
showTree(depth + 1, val, nextNode);
}
}
}
@Override
protected Location getExpectedMove() {
return expectedMove;
}
private String percentage(BigDecimal prob) {
return Action.FORMAT_2DP.format(prob.multiply(ONE_HUNDRED));
}
@Override
protected boolean allDead() {
return allDead;
}
@Override
Area getDeadLocations() {
return deadLocations;
}
@Override
protected int getMovesProcessed() {
return movesProcessed;
}
@Override
protected int getMovesToProcess() {
return this.movesToProcess;
}
@Override
protected Location checkForBetterMove(Location location) {
// no moves processed
if (processedMoves.size() == 0) {
return null;
}
ProcessedMove best = processedMoves.get(0);
// the move is already the best
if (location.equals(best.location)) {
solver.logger.log(Level.INFO, "Tile %s (Winning %d) is best according to partial BFDA", location, best.winningLines);
return null;
}
// if the chosen location has been processed and it isn't the best then send the best
for (ProcessedMove pm: processedMoves) {
if (pm.location.equals(location)) {
solver.logger.log(Level.INFO, "Tile %s (Winning %d pruned %b) replaced by %s (winning %d pruned %b)", location, pm.winningLines, pm.pruned, best.location, best.winningLines, best.pruned);
return best.location;
}
}
// the chosen location hasn't been processed
return null;
}
@Override
BigDecimal getSolveChance() {
return this.currentNode.getProbability();
}
@Override
List<? extends Location> getLocations() {
return locations;
}
}

View File

@ -0,0 +1,54 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.util.List;
import minesweeper.structure.Action;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
abstract public class BruteForceAnalysisModel {
protected boolean completed = false;
protected boolean tooMany = false;
abstract protected void addSolution(byte[] solution);
abstract protected void process();
protected boolean isComplete() {
return this.completed;
}
protected boolean tooMany() {
return this.tooMany;
}
protected boolean isShallow() {
return false;
}
abstract protected int getSolutionCount();
abstract protected int getMovesProcessed();
abstract protected int getMovesToProcess();
abstract protected Location checkForBetterMove(Location location);
abstract protected long getNodeCount();
abstract protected Action getNextMove(BoardState boardState);
abstract protected Location getExpectedMove();
abstract protected boolean allDead();
abstract Area getDeadLocations();
abstract BigDecimal getSolveChance();
abstract List<? extends Location> getLocations();
}

View File

@ -0,0 +1,222 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver;
import java.math.BigInteger;
import java.util.List;
import minesweeper.structure.Location;
/**
*
* @author David
*/
public class CrunchResult {
protected List<Location> square;
protected Location witness[];
protected int originalNumMines;
// information about witnesses
protected int[] witnessGood;
protected boolean[] witnessRestFlags;
protected boolean[] witnessRestClear;
protected int currentFlags[];
protected boolean alwaysSatisfied[];
//protected int[] hookMines;
/**
* The weight is used to scale up the values so they represent the whole solution.
*/
private BigInteger weight;
protected BigInteger bigGoodCandidates = BigInteger.ZERO;
/**
* The number of candidate solutions that put a mine in this square.
* if the value is zero a mine is never present, if equals bigGoodCandidates then a mine is always present
*/
protected BigInteger[] bigTally;
/**
* <p>bigDistribution[square][3] = 102, means 102 candidate solutions have 3 mines surrounding this square</p>
*/
protected BigInteger[][] bigDistribution;
/**
* The largest number of candidate solutions within the bigDistribution for this square. This is the maximum number of solutions that can
* remain if we choose to guess here.
*/
//private BigInteger[] bigMax; // the maximum count for each square
//private int[] bigMaxIndex;
/**
* Number of different values this square can have
*/
private int[] bigCount; // number of different values this square can have
private int maxBigCount = 0;
// merge crunch results to give a single view of the situation
protected static CrunchResult bigMerge(CrunchResult... cr) {
CrunchResult result = new CrunchResult();
// if all the results have the same number of original mines then store that
int originalMines = cr[0].originalNumMines;
for (CrunchResult r: cr) {
if (r.originalNumMines != originalMines) {
originalMines = 0;
break;
}
}
result.originalNumMines = originalMines;
result.witness = cr[0].witness;
result.square = cr[0].getSquare();
result.setWeight(BigInteger.ONE);
BigInteger[][] distribution;
BigInteger[] bigMax;
BigInteger[] bigCount;
if (cr[0].bigDistribution == null) {
distribution = null;
} else {
distribution = new BigInteger[result.getSquare().size()][9];
bigMax = new BigInteger[result.getSquare().size()];
bigCount = new BigInteger[result.getSquare().size()];
for (int i = 0; i < result.getSquare().size(); i++) {
for (int j = 0; j < 9; j++) {
distribution[i][j] = BigInteger.ZERO;
}
}
}
BigInteger[] tally = new BigInteger[cr[0].bigTally.length];
BigInteger candidates = BigInteger.ZERO;
for (int i=0; i < tally.length; i++) {
tally[i] = BigInteger.ZERO;
}
for (int j=0; j < cr.length; j++) {
for (int k=0; k < tally.length; k++) {
tally[k] = tally[k].add(cr[j].getWeight().multiply(cr[j].bigTally[k]));
if (distribution != null) {
for (int l=0; l < 9; l++) {
distribution[k][l] = distribution[k][l].add(cr[j].getWeight().multiply(cr[j].bigDistribution[k][l]));
}
}
}
candidates = candidates.add(cr[j].getWeight().multiply(cr[j].bigGoodCandidates));
}
result.bigTally = tally;
result.bigGoodCandidates = candidates;
result.bigDistribution = distribution;
//result.calculateMinMax();
// merge the witness information
result.witnessRestFlags = new boolean[cr[0].witnessRestFlags.length];
result.witnessRestClear = new boolean[cr[0].witnessRestClear.length];
for (int i=0; i < result.witnessRestFlags.length; i++) {
result.witnessRestFlags[i] = true;
result.witnessRestClear[i] = true;
for (int j=0; j < cr.length; j++) {
result.witnessRestFlags[i] = result.witnessRestFlags[i] & cr[j].witnessRestFlags[i];
result.witnessRestClear[i] = result.witnessRestClear[i] & cr[j].witnessRestClear[i];
}
}
// witness good information should be the same for all the merged data
result.witnessGood = cr[0].witnessGood;
return result;
}
/*
protected void calculateMinMax() {
if (this.bigDistribution == null) {
return;
}
this.setBigCount(new int[this.getSquare().size()]);
this.setBigMax(new BigInteger[this.getSquare().size()]);
this.setBigMaxIndex(new int[this.getSquare().size()]);
for (int i=0; i < this.getSquare().size(); i++) {
BigInteger max = BigInteger.ZERO;
int maxIndex = 0;
for (int j=0; j < this.bigDistribution[i].length; j++) {
if (this.bigDistribution[i][j].signum() != 0) {
this.bigCount[i]++;
}
if (this.bigDistribution[i][j].compareTo(max) > 0) {
max = this.bigDistribution[i][j];
maxIndex = j;
}
}
this.getBigMax()[i] = max;
this.getBigMaxIndex()[i] = maxIndex;
}
for (int i=0; i < this.bigCount.length; i++) {
maxBigCount = Math.max(maxBigCount, bigCount[i]);
}
}
*/
public List<Location> getSquare() {
return square;
}
public void setSquare(List<Location> square) {
this.square = square;
}
//public BigInteger[] getBigMax() {
// return bigMax;
//}
//public void setBigMax(BigInteger[] bigMax) {
// this.bigMax = bigMax;
//}
public BigInteger getWeight() {
return weight;
}
public void setWeight(BigInteger weight) {
this.weight = weight;
}
public int[] getBigCount() {
return bigCount;
}
public void setBigCount(int[] bigCount) {
this.bigCount = bigCount;
}
//public int[] getBigMaxIndex() {
// return bigMaxIndex;
//}
//public void setBigMaxIndex(int[] bigMaxIndex) {
// this.bigMaxIndex = bigMaxIndex;
//}
//public int getMaxCount() {
// return this.maxBigCount;
//}
}

View File

@ -0,0 +1,333 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver;
import Asynchronous.Asynchronous;
import minesweeper.gamestate.GameStateModel;
import minesweeper.solver.constructs.WitnessData;
import minesweeper.solver.iterator.Iterator;
import minesweeper.structure.Location;
import java.math.BigInteger;
import java.util.List;
/**
* Performs a brute force search on the provided squares using the iterator
*
*/
public class Cruncher implements Asynchronous<CrunchResult> {
private final BoardState boardState;
private final Iterator iterator;
private final List<Location> square;
private final List<? extends Location> witness;
private final boolean calculateDistribution;
private final BruteForceAnalysisModel bfa;
private final boolean[] workRestNotFlags;
private final boolean[] workRestNotClear;
private CrunchResult result;
public Cruncher(BoardState boardState, List<Location> square, List<? extends Location> witness, Iterator iterator, boolean calculateDistribution, BruteForceAnalysisModel bfa) {
this.iterator = iterator;
this.square = square;
this.witness = witness;
this.calculateDistribution = calculateDistribution;
this.bfa = bfa;
this.boardState = boardState;
workRestNotFlags = new boolean[witness.size()];
workRestNotClear = new boolean[witness.size()];
}
@Override
public void start() {
result = crunch();
//System.out.println("Candidates = " + result.bigGoodCandidates);
result.setWeight(BigInteger.ONE);
}
@Override
public void requestStop() {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public CrunchResult getResult() {
return result;
}
private CrunchResult crunch() {
//display("crunching " + iterator.numberBalls + " Mines in " + square.length + " Squares with " + witness.length + " witnesses");
// 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);
//display("Witness " + i + " location " + d.location.display() + " current flags = " + d.currentFlags + " good witness = " + d.witnessGood + " Satisified = " + d.alwaysSatisfied);
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;
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.hookMines = null;
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);
*/
//boolean[] workRestNotFlags = new boolean[witnessData.length];
//boolean[] workRestNotClear = new boolean[witnessData.length];
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);
//int flags3 = board[witnessData[i].location.x][witnessData[i].location.y];
// 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;
}
}
//if (bfa != null && !bfa.tooMany()) {
bfa.addSolution(solution);
//}
}
return true;
}
protected BruteForceAnalysisModel getBFA() {
return bfa;
}
protected Iterator getIterator() {
return this.iterator;
}
// 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;
}
}

View File

@ -0,0 +1,616 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import minesweeper.gamestate.MoveMethod;
import minesweeper.solver.constructs.CandidateLocation;
import minesweeper.solver.constructs.ChordLocation;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Action;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
public class EfficiencyHelper {
private static final BigDecimal MINE_THRESHOLD = BigDecimal.valueOf(1.0); // probability of mine to consider
private static final int RISK_ADVANTAGE = 2; // <= benefit - cost
private static final BigDecimal ONE_ADVANTAGE_THRESHOLD = BigDecimal.valueOf(0.9); // accept mine probability when benefit - cost = 1
private static final BigDecimal CLEAR_ZERO_VALUE = BigDecimal.valueOf(0.85); // clear a possible zero if chance if >= this value
private static final BigDecimal NFE_BLAST_PENALTY = BigDecimal.valueOf(0.75);
private static final boolean ALLOW_ZERO_NET_GAIN_CHORD = true;
private static final boolean ALLOW_ZERO_NET_GAIN_PRE_CHORD = true;
private BoardState board;
private WitnessWeb wholeEdge;
private List<Action> actions;
private ProbabilityEngineModel pe;
public EfficiencyHelper(BoardState board, WitnessWeb wholeEdge, List<Action> actions, ProbabilityEngineModel pe) {
this.board = board;
this.actions = actions;
this.wholeEdge = wholeEdge;
this.pe = pe;
}
public List<Action> process() {
List<Action> result = new ArrayList<>();
List<ChordLocation> chordLocations = new ArrayList<>();
// look for tiles satisfied by known mines and work out the benefit of placing the mines and then chording
for (Location loc: board.getAllLivingWitnesses()) {
// if the witness is satisfied how many clicks will it take to clear the area by chording
if (board.getWitnessValue(loc) == board.countAdjacentConfirmedFlags(loc)) {
List<Location> mines = new ArrayList<>();
// how many hidden tiles are next to the mine(s) we would have flagged, the more the better
// this favours flags with many neighbours over flags buried against cleared tiles.
Set<Location> hiddenMineNeighbours = new HashSet<>();
for (Location adjMine: board.getAdjacentSquaresIterable(loc)) {
if (!board.isConfirmedMine(adjMine)) {
continue;
}
// if the flag isn't on the board we need to add it
if (!board.isFlagOnBoard(adjMine)) {
mines.add(adjMine);
}
for (Location adjTile: board.getAdjacentSquaresIterable(adjMine)) {
if (board.isUnrevealed(adjTile)) {
hiddenMineNeighbours.add(adjTile);
}
}
}
int cost = board.getWitnessValue(loc) - board.countAdjacentFlagsOnBoard(loc); // flags still to be placed
int benefit = board.countAdjacentUnrevealed(loc);
if (board.getWitnessValue(loc) != 0) { // if the witness isn't a zero then add the cost of chording - zero can only really happen in the analyser
cost++;
}
if (benefit >= cost) {
board.getLogger().log(Level.INFO, "Chord %s has reward %d and tiles adjacent to new flags %d", loc, (benefit - cost), hiddenMineNeighbours.size());
chordLocations.add(new ChordLocation(loc.x, loc.y, benefit, cost, hiddenMineNeighbours.size(), BigDecimal.ONE, mines));
}
}
}
BigDecimal oneAdvantageTest = BigDecimal.ONE.subtract(ONE_ADVANTAGE_THRESHOLD);
// also consider tiles which are possibly mines and their benefit
for (CandidateLocation cl: pe.getProbableMines(MINE_THRESHOLD)) {
for (Location adjTile: board.getAdjacentSquaresIterable(cl)) {
if (board.isRevealed(adjTile) && board.getWitnessValue(adjTile) - board.countAdjacentConfirmedFlags(adjTile) == 1) { // if the adjacent tile needs 1 more tile
int cost = board.getWitnessValue(adjTile) - board.countAdjacentFlagsOnBoard(adjTile) + 1; // placing the flag and chording
int benefit = board.countAdjacentUnrevealed(adjTile) - 1; // the probable mine isn't a benefit
if (benefit >= cost + RISK_ADVANTAGE || benefit - cost == 1 && cl.getProbability().compareTo(oneAdvantageTest) < 0) {
List<Location> mines = new ArrayList<>();
mines.add(cl);
Set<Location> hiddenMineNeighbours = new HashSet<>();
for (Location adjNewFlag: board.getAdjacentSquaresIterable(cl)) {
if (board.isUnrevealed(adjNewFlag)) {
hiddenMineNeighbours.add(adjNewFlag);
}
}
for (Location adjMine: board.getAdjacentSquaresIterable(adjTile)) {
if (!board.isConfirmedMine(adjMine)) {
continue;
}
// if the flag isn't on the board we need to add it
if (!board.isFlagOnBoard(adjMine)) {
mines.add(adjMine);
}
for (Location adjNewFlag: board.getAdjacentSquaresIterable(adjMine)) {
if (board.isUnrevealed(adjNewFlag)) {
hiddenMineNeighbours.add(adjNewFlag);
}
}
}
board.getLogger().log(Level.INFO, "Placing possible mine %s and Chording %s has reward %d and tiles adjacent to new flags %d", cl, adjTile, (benefit - cost), hiddenMineNeighbours.size());
chordLocations.add(new ChordLocation(adjTile.x, adjTile.y, benefit, cost, hiddenMineNeighbours.size(), BigDecimal.ONE.subtract(cl.getProbability()), mines));
}
}
}
}
// sort the most beneficial chords to the top
Collections.sort(chordLocations, ChordLocation.SORT_BY_BENEFIT_DESC);
ChordLocation bestChord = null;
BigDecimal bestNetBenefit = BigDecimal.ZERO;
for (ChordLocation cl: chordLocations) {
if (cl.getNetBenefit().signum() > 0 || EfficiencyHelper.ALLOW_ZERO_NET_GAIN_CHORD && cl.getNetBenefit().signum() == 0 && cl.getCost() > 0) {
bestChord = cl;
bestNetBenefit = cl.getNetBenefit();
}
break;
}
if (bestChord != null) {
board.getLogger().log(Level.INFO, "Chord %s has best reward of %f", bestChord, bestChord.getNetBenefit());
} else {
board.getLogger().log(Level.INFO, "No Chord has net benefit >= 0");
}
/*
for (ChordLocation cl: chordLocations) {
for (Location l: board.getAdjacentSquaresIterable(cl)) {
// flag not yet on board
if (!processed[l.x][l.y] && board.isConfirmedFlag(l) && !board.isFlagOnBoard(l)) {
result.add(new Action(l, Action.FLAG, MoveMethod.TRIVIAL, "Place flag", BigDecimal.ONE, 0));
}
// flag on board in error
if (!processed[l.x][l.y] && !board.isConfirmedFlag(l) && board.isFlagOnBoard(l)) {
result.add(new Action(l, Action.FLAG, MoveMethod.CORRECTION, "Remove flag", BigDecimal.ONE, 0));
}
processed[l.x][l.y] = true;
}
// now add the clear all
result.add(new Action(cl, Action.CLEARALL, MoveMethod.TRIVIAL, "Clear All", BigDecimal.ONE, 1));
break;
}
*/
Action bestAction = null;
BigDecimal highest = BigDecimal.ZERO;
Action bestZero = null;
BigInteger bestZeroSolutions = BigInteger.ZERO;
List<Location> emptyList = Collections.emptyList();
SolutionCounter currSolnCount = board.getSolver().validatePosition(wholeEdge, emptyList, null, Area.EMPTY_AREA);
if (bestNetBenefit.signum() > 0) {
highest = new BigDecimal(currSolnCount.getSolutionCount()).multiply(bestNetBenefit);
}
// look for click then chord if the right number turns up
// or chord then chord if the right number turns up
for (Action act: actions) {
if (act.getAction() == Action.CLEAR) {
// find the best chord adjacent to this clear if there is one
ChordLocation adjChord = null;
for (ChordLocation cl: chordLocations) {
if (cl.getNetBenefit().signum() == 0 && !ALLOW_ZERO_NET_GAIN_PRE_CHORD) {
continue;
}
if (cl.isAdjacent(act)) {
// first adjacent chord, or better adj chord, or cheaper adj chord, or exposes more tiles
if (adjChord == null || adjChord.getNetBenefit().compareTo(cl.getNetBenefit()) < 0 || adjChord.getNetBenefit().compareTo(cl.getNetBenefit()) == 0 && adjChord.getCost() > cl.getCost() ||
adjChord.getNetBenefit().compareTo(cl.getNetBenefit()) == 0 && adjChord.getCost() == cl.getCost() && adjChord.getExposedTileCount() < cl.getExposedTileCount()) {
adjChord = cl;
}
}
}
if (adjChord == null) {
//console.log("(" + act.x + "," + act.y + ") has no adjacent chord with net benefit > 0");
} else {
board.getLogger().log(Level.INFO, "Tile %s has adjacent chord %s with net benefit %f", act, adjChord, adjChord.getNetBenefit());
}
int adjMines = board.countAdjacentConfirmedFlags(act);
int adjFlags = board.countAdjacentFlagsOnBoard(act);
int hidden = board.countAdjacentUnrevealed(act);
int chordCost;
if (adjMines != 0) { // if the value we want isn't zero we'll need to subtract the cost of doing the chording
chordCost = 1;
} else {
chordCost = 0;
}
//BigDecimal chordReward = BigDecimal.valueOf(hidden - adjMines + adjFlags - chordCost); // tiles adjacent - ones which are mines - mines which aren't flagged yet
BigDecimal chordReward = ChordLocation.chordReward(hidden, adjMines - adjFlags + chordCost);
if (chordReward.compareTo(bestNetBenefit) > 0) {
SolutionCounter counter = board.getSolver().validateLocationUsingSolutionCounter(wholeEdge, act, adjMines, Area.EMPTY_AREA);
BigDecimal current = new BigDecimal(counter.getSolutionCount()).multiply(chordReward);
BigDecimal prob = new BigDecimal(counter.getSolutionCount()).divide(new BigDecimal(currSolnCount.getSolutionCount()), 10, RoundingMode.HALF_UP);
// realistic expectation
BigDecimal expBenefit = current.divide(new BigDecimal(currSolnCount.getSolutionCount()), 10, RoundingMode.HALF_UP);
board.getLogger().log(Level.INFO, "considering Clear (" + act.x + "," + act.y + ") with value " + adjMines + " and reward " + chordReward + " ( H=" + hidden + " M=" + adjMines + " F=" + adjFlags + " Chord=" + chordCost
+ " Prob=" + prob + "), expected benefit " + expBenefit);
// if we have found an 100% certain zero then just click it.
if (adjMines == 0) {
if (counter.getSolutionCount().equals(currSolnCount.getSolutionCount())) {
board.getLogger().log(Level.INFO, "Tile %s is a certain zero no need for further analysis", act);
bestZero = act;
bestZeroSolutions = currSolnCount.getSolutionCount();
bestAction = null;
bestChord = null;
break;
} else if (counter.getSolutionCount().compareTo(bestZeroSolutions) > 0) {
bestZero = act;
bestZeroSolutions = counter.getSolutionCount();
}
}
// realistic expectation
BigDecimal clickChordNetBenefit = chordReward.multiply(new BigDecimal(counter.getSolutionCount())); // expected benefit from clicking the tile then chording it
// optimistic expectation
//BigDecimal clickChordNetBenefit = BigDecimal.valueOf(reward).multiply(new BigDecimal(currSolnCount.getSolutionCount())); // expected benefit from clicking the tile then chording it
//if (adjMines == 0) {
// adjChord = null;
// board.getLogger().log(Level.INFO, "Not considering Chord Chord combo because we'd be chording into a zero");
//}
// if it is a chord/chord combo
if (adjChord != null) {
current = chordChordCombo(adjChord, act, counter.getSolutionCount(), currSolnCount.getSolutionCount());
if (current.compareTo(clickChordNetBenefit) < 0) { // if click chord is better then discard the adjacent chord
current = clickChordNetBenefit;
adjChord = null;
}
} else { // or a clear/chord combo
current = clickChordNetBenefit; // expected benefit == p*benefit
}
if (current.compareTo(highest) > 0) {
highest = current;
if (adjChord != null) { // if there is an adjacent chord then use this to clear the tile
bestChord = adjChord;
bestAction = null;
} else {
bestChord = null;
bestAction = act;
}
}
} else {
board.getLogger().log(Level.INFO, "Not considering Tile %s", act);
}
}
}
BigInteger zeroThreshold = new BigDecimal(currSolnCount.getSolutionCount()).multiply(CLEAR_ZERO_VALUE).toBigInteger();
if (bestZero != null && bestZeroSolutions.compareTo(zeroThreshold) >= 0) {
result.add(bestZero);
} else if (bestAction != null) {
result.add(bestAction);
} else if (bestChord != null) {
result.clear();
// add the required flags if they aren't already there
for (Location adjMine: bestChord.getMines()) {
if (!board.isFlagOnBoard(adjMine)) {
result.add(new Action(adjMine, Action.FLAG, MoveMethod.TRIVIAL, "Place flag", BigDecimal.ONE, 0));
}
}
//for (Location adjTile: board.getAdjacentSquaresIterable(bestChord)) {
// if (board.isConfirmedFlag(adjTile) && !board.isFlagOnBoard(adjTile)) {
// result.add(new Action(adjTile, Action.FLAG, MoveMethod.TRIVIAL, "Place flag", BigDecimal.ONE, 0));
// }
//}
// Add the chord action
result.add(new Action(bestChord, Action.CLEARALL, MoveMethod.TRIVIAL, "Clear All", bestChord.getScale(), 1));
}
if (result.isEmpty()) { // return the first action
result.add(actions.get(0));
return result;
} else {
return result;
}
}
// the ChordLocation of the tile to chord, the Tile to be chorded afterwards if the value comes up good, the number of solutions where this occurs
// and the total number of solutions
// this method works out the net benefit of this play
private BigDecimal chordChordCombo(ChordLocation chord1, Location chord2Tile, BigInteger occurs, BigInteger total) {
// now check each tile around the tile to be chorded 2nd and see how many mines to flag and tiles will be cleared
int alreadyCounted = 0;
int needsFlag = 0;
int clearable = 0;
int chordClick = 0;
for (Location adjTile: board.getAdjacentSquaresIterable(chord2Tile)) {
if (board.isConfirmedMine(adjTile)) {
chordClick = 1;
}
// if adjacent to chord1
if (chord1.isAdjacent(adjTile)) {
alreadyCounted++;
} else if (board.isConfirmedMine(adjTile) && !board.isFlagOnBoard(adjTile)) {
needsFlag++;
} else if (board.isUnrevealed(adjTile)) {
clearable++;
}
}
BigDecimal failedBenefit = chord1.getNetBenefit();
BigDecimal secondBenefit = ChordLocation.chordReward(clearable, needsFlag + chordClick);
// realistic expectation
BigDecimal score = failedBenefit.multiply(new BigDecimal(total)).add(secondBenefit.multiply(new BigDecimal(occurs)));
// optimistic expectation
//BigDecimal score = failedBenefit.multiply(new BigDecimal(total)).add( BigDecimal.valueOf(secondBenefit).multiply(new BigDecimal(total)));
BigDecimal expBen = score.divide(new BigDecimal(total), Solver.DP, RoundingMode.HALF_DOWN);
board.getLogger().log(Level.INFO, "Chord %s followed by Chord %s: Chord 1: benefit %f, Chord2: H=%d, to F=%d, Chord=%d, Benefit=%f ==> expected benefit %f"
, chord1, chord2Tile, chord1.getNetBenefit(), clearable, needsFlag, chordClick, secondBenefit, expBen);
return score;
}
/**
* A No-flag efficiency algorithm
*/
public List<Action> processNF() {
List<Action> result = new ArrayList<>();
//Set<Location> notZeros = new HashSet<>();
Map<Location, BigDecimal> zeroProbs = new HashMap<>();
// locations next to a mine can't be zero
for (int i=0; i < board.getGameWidth() - 1; i++) {
for (int j=0; j < board.getGameHeight() - 1; j++) {
if (this.board.isConfirmedMine(i, j)) {
for (Location adjTile: board.getAdjacentSquaresIterable(this.board.getLocation(i, j))) {
if (board.isUnrevealed(adjTile)) {
zeroProbs.put(adjTile, BigDecimal.ZERO); // tiles adjacent to a mine have zero probability of being a '0'
}
}
//notZeros.addAll(board.getAdjacentUnrevealedSquares(this.board.getLocation(i, j)));
}
}
}
// calculate the current solution count
List<Location> emptyList = Collections.emptyList();
SolutionCounter currSolnCount = board.getSolver().validatePosition(wholeEdge, emptyList, null, Area.EMPTY_AREA);
Set<Location> onEdgeSet = new HashSet<>(this.wholeEdge.getSquares());
Set<Location> adjacentEdgeSet = new HashSet<>();
BigDecimal zeroTileScore = null;
Location zeroTile = null;
// do a more costly check for whether zero is possible, for those which haven't already be determined
for (Location tile: this.wholeEdge.getSquares()) {
if (!zeroProbs.containsKey(tile) && !this.board.isConfirmedMine(tile)) {
SolutionCounter counter = board.getSolver().validateLocationUsingSolutionCounter(wholeEdge, tile, 0, Area.EMPTY_AREA);
if (counter.getSolutionCount().signum() == 0) { // no solution where this is a zero
zeroProbs.put(tile, BigDecimal.ZERO);
} else if (counter.getSolutionCount().compareTo(currSolnCount.getSolutionCount()) == 0) {
board.getLogger().log(Level.INFO, "Tile %s is always zero", tile);
result.add(new Action(tile, Action.CLEAR, MoveMethod.TRIVIAL, "Certain zero (1)", BigDecimal.ONE));
break;
} else {
BigDecimal zeroProb = new BigDecimal(counter.getSolutionCount()).divide(new BigDecimal(currSolnCount.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
zeroProbs.put(tile, zeroProb);
BigDecimal safety = this.pe.getProbability(tile);
BigDecimal score = zeroProb.subtract(BigDecimal.ONE.subtract(safety).multiply(NFE_BLAST_PENALTY));
if (zeroTile == null || zeroTileScore.compareTo(score) < 0) {
zeroTile = tile;
zeroTileScore = score;
}
}
}
// collect hidden tiles adjacent to the boundary and not on the boundary
for (Location adjTile: this.board.getAdjacentSquaresIterable(tile)) {
if (this.board.isUnrevealed(adjTile) && !onEdgeSet.contains(adjTile)) {
adjacentEdgeSet.add(adjTile);
}
}
}
if (!result.isEmpty()) {
return result;
}
// do a more costly check for whether zero is possible for actions not already considered, for those which haven't already be determined
for (Action tile: this.actions) {
if (tile.getAction() == Action.CLEAR && !zeroProbs.containsKey(tile)) {
SolutionCounter counter = board.getSolver().validateLocationUsingSolutionCounter(wholeEdge, tile, 0, Area.EMPTY_AREA);
if (counter.getSolutionCount().signum() == 0) { // no solution where this is a zero
zeroProbs.put(tile, BigDecimal.ZERO);
} else if (counter.getSolutionCount().compareTo(currSolnCount.getSolutionCount()) == 0) {
board.getLogger().log(Level.INFO, "Tile %s is always zero", tile);
result.add(tile);
break;
} else {
BigDecimal zeroProb = new BigDecimal(counter.getSolutionCount()).divide(new BigDecimal(currSolnCount.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
zeroProbs.put(tile, zeroProb);
BigDecimal safety = this.pe.getProbability(tile);
BigDecimal score = zeroProb.subtract(BigDecimal.ONE.subtract(safety).multiply(NFE_BLAST_PENALTY));
if (zeroTile == null || zeroTileScore.compareTo(score) < 0) {
zeroTile = tile;
zeroTileScore = score;
}
}
}
// collect hidden tiles adjacent to the boundary and not on the boundary
for (Location adjTile: this.board.getAdjacentSquaresIterable(tile)) {
if (this.board.isUnrevealed(adjTile) && !onEdgeSet.contains(adjTile)) {
adjacentEdgeSet.add(adjTile);
}
}
}
if (!result.isEmpty()) {
return result;
}
BigDecimal offEdgeSafety = this.pe.getOffEdgeProb();
// see if tiles adjacent to the boundary can be zero
for (Location tile: adjacentEdgeSet) {
SolutionCounter counter = board.getSolver().validateLocationUsingSolutionCounter(wholeEdge, tile, 0, Area.EMPTY_AREA);
if (counter.getSolutionCount().signum() == 0) { // no solution where this is a zero
zeroProbs.put(tile, BigDecimal.ZERO);
} else if (counter.getSolutionCount().compareTo(currSolnCount.getSolutionCount()) == 0) {
board.getLogger().log(Level.INFO, "Tile %s is always zero", tile);
result.add(new Action(tile, Action.CLEAR, MoveMethod.TRIVIAL, "Certain zero (2)", BigDecimal.ONE));
break;
} else {
BigDecimal zeroProb = new BigDecimal(counter.getSolutionCount()).divide(new BigDecimal(currSolnCount.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
zeroProbs.put(tile, zeroProb);
BigDecimal score = zeroProb.subtract(BigDecimal.ONE.subtract(offEdgeSafety).multiply(NFE_BLAST_PENALTY));
if (zeroTile == null || zeroTileScore.compareTo(score) < 0) {
zeroTile = tile;
zeroTileScore = score;
}
}
}
if (!result.isEmpty()) {
return result;
}
BigDecimal maxAllNotZeroProbability = BigDecimal.ZERO;
Action bestAllNotZeroAction = null;
// see if any safe tiles are also never next to a zero
for (Action act: actions) {
if (act.getAction() == Action.CLEAR) {
// find the best chord adjacent to this clear if there is one
//boolean valid = true;
BigDecimal allNotZeroProbability = BigDecimal.ONE;
// if all the adjacent tiles can't be zero then we are safe to clear this tile without wasting a 3BV
for (Location adjTile: this.board.getAdjacentSquaresIterable(act)) {
if (this.board.isUnrevealed(adjTile)) {
if (zeroProbs.containsKey(adjTile)) {
allNotZeroProbability = allNotZeroProbability.multiply(BigDecimal.ONE.subtract(zeroProbs.get(adjTile)));
} else {
board.getLogger().log(Level.WARN, "Tile %s doesn't have a probability for being a zero", adjTile);
}
}
}
if (bestAllNotZeroAction == null || maxAllNotZeroProbability.compareTo(allNotZeroProbability) < 0) {
bestAllNotZeroAction = act;
maxAllNotZeroProbability = allNotZeroProbability;
}
if (allNotZeroProbability.compareTo(BigDecimal.ONE) == 0) {
board.getLogger().log(Level.INFO, "Tile %s is 3BV safe because it can't be next to a zero", act);
result.add(act);
}
}
}
if (!result.isEmpty()) {
return result;
}
if (zeroTile != null) {
BigDecimal prob = this.pe.getProbability(zeroTile);
if (bestAllNotZeroAction != null) {
if (maxAllNotZeroProbability.compareTo(zeroTileScore ) > 0 && zeroTileScore.compareTo(BigDecimal.ZERO) < 0) {
result.add(bestAllNotZeroAction);
} else {
result.add(new Action(zeroTile, Action.CLEAR, MoveMethod.TRIVIAL, "best zero", prob));
}
} else {
result.add(new Action(zeroTile, Action.CLEAR, MoveMethod.TRIVIAL, "best zero", prob));
}
} else {
if (bestAllNotZeroAction != null) {
result.add(bestAllNotZeroAction);
}
}
// otherwise use the best tile looking for a zero
//Action action = new Action(zeroTile, Action.CLEAR, MoveMethod.TRIVIAL, "best zero", this.pe.getProbability(zeroTile));
//result.add(action);
//board.getLogger().log(Level.INFO, "Action %s", action);
return result;
}
}

View File

@ -0,0 +1,690 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import minesweeper.solver.constructs.Box;
import minesweeper.solver.constructs.Square;
import minesweeper.solver.constructs.Witness;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
public class FiftyFiftyHelper {
private boolean[][] PATTERNS = new boolean[][] {{true, true, true, true}, // four mines
{true, true, true, false}, {true, false, true, true}, {false, true, true, true}, {true, true, false, true}, // 3 mines
{true, false, true, false}, {false, true, false, true}, {true, true, false, false}, {false, false, true, true}, // 2 mines
{false, true, false, false}, {false, false, false, true}, {true, false, false, false}, {false, false, true, false} // 1 mine
};
private final static BigDecimal HALF = new BigDecimal("0.5");
private class Link {
private Location tile1;
private boolean closed1 = true;
private Location tile2;
private boolean closed2 = true;
private boolean processed = false;
// list of locations which could prevent us being an unavoidable 50/50
private List<Location> trouble = new ArrayList<>();
}
private Set<Location> deferGuessing = new HashSet<>();
private BoardState board;
private WitnessWeb wholeEdge;
private Area deadLocations;
private BigDecimal bestNonPseudo2Tile5050Probability = BigDecimal.ZERO;
private Location bestNonPseudo2Tile5050Tile1; // this represents the best tile which isn't a 2-tile pseudo 50/50 because it does support 2 mines
private Location bestNonPseudo2Tile5050Tile2;
private BigDecimal bestNonPseudo2Tilelts = BigDecimal.ZERO;
private int bestNonPseudo2TileAdjacent = 0;
public FiftyFiftyHelper(BoardState board, WitnessWeb wholeEdge, Area deadLocations) {
this.board = board;
this.wholeEdge = wholeEdge;
this.deadLocations = deadLocations;
}
public Location findUnavoidable5050(List<Location> extraMines) {
List<Link> links = new ArrayList<>();
// also look for unavoidable guesses
for (Witness witness: wholeEdge.getPrunedWitnesses()) {
if (witness.getMines() == 1 && witness.getSquares().size() == 2) {
// create a new link
Link link = new Link();
link.tile1 = witness.getSquares().get(0);
link.tile2 = witness.getSquares().get(1);
board.getLogger().log(Level.INFO, "Witness %s is a possible unavoidable guess witness for %s and %s", witness, link.tile1, link.tile2);
boolean unavoidable = true;
// if every monitoring tile also monitors all the other tiles then it can't provide any information
for (Square tile: witness.getSquares()) {
for (Location adjTile: board.getAdjacentSquaresIterable(tile)) {
// are we one of the tiles other tiles, if so then no need to check
boolean toCheck = true;
for (Square otherTile: witness.getSquares()) {
if (otherTile.equals(adjTile)) {
toCheck = false;
break;
}
}
// if we are monitoring and not a mine then see if we are also monitoring all the other mines
if (toCheck && !board.isConfirmedMine(adjTile) && !extraMines.contains(adjTile)) {
for (Square otherTile: witness.getSquares()) {
if (!otherTile.equals(adjTile) && !adjTile.isAdjacent(otherTile)) {
//board.display("Tile " + adjTile.display() + " is not monitoring " + otherTile.display());
board.getLogger().log(Level.DEBUG, "Tile %S can receive exclusive information from %s", tile, adjTile);
link.trouble.add(adjTile);
if (tile.equals(link.tile1)) {
link.closed1 = false;
} else {
link.closed2 = false;
}
unavoidable = false;
//break check;
}
}
}
}
}
if (unavoidable) {
Location guess = board.getSolver().getLowest(witness.getSquares(), deadLocations);
board.getLogger().log(Level.INFO, "Tile %s is an unavoidable guess", guess);
return guess;
}
links.add(link);
}
}
List<Location> area5050 = new ArrayList<>(); // used to hold the expanding candidate 50/50
// try and connect 2 or links together to form an unavoidable 50/50. Closed at both ends.
for (Link link: links) {
if (!link.processed && (link.closed1 ^ link.closed2)) { // this is the XOR operator, so 1 and only 1 of these is closed
Location openTile;
int extensions = 0;
if (!link.closed1) {
openTile = link.tile1;
} else {
openTile = link.tile2;
}
area5050.clear();
area5050.add(link.tile1);
area5050.add(link.tile2);
link.processed = true;
boolean noMatch = false;
while (openTile != null && !noMatch) {
noMatch = true;
for (Link extension: links) {
if (!extension.processed) {
if (extension.tile1.equals(openTile)) {
extensions++;
extension.processed = true;
noMatch = false;
// accumulate the trouble tiles as we progress;
link.trouble.addAll(extension.trouble);
area5050.add(extension.tile2); // tile2 is the new tile
if (extension.closed2) {
if (extensions % 2 == 0 && noTrouble(link, area5050)) {
board.getLogger().log(Level.INFO, "Tile %s is an unavoidable guess, with %d extensions", openTile, extensions);
return board.getSolver().getLowest(area5050, deadLocations);
} else {
board.getLogger().log(Level.INFO, "Tile %s is a closed extension with %d parts", openTile, (extensions + 1));
deferGuessing.addAll(area5050);
openTile = null;
}
} else { // found an open extension, now look for an extension for this
openTile = extension.tile2;
}
break;
}
if (extension.tile2.equals(openTile)) {
extensions++;
extension.processed = true;
noMatch = false;
// accumulate the trouble tiles as we progress;
link.trouble.addAll(extension.trouble);
area5050.add(extension.tile1); // tile 1 is the new tile
if (extension.closed1) {
if (extensions % 2 == 0 && noTrouble(link, area5050)) {
board.getLogger().log(Level.INFO, "Tile %s is an unavoidable guess, with %d extensions", openTile, extensions);
return board.getSolver().getLowest(area5050, deadLocations);
} else {
board.getLogger().log(Level.INFO, "Tile %s is a closed extension with %d parts", openTile, (extensions + 1));
deferGuessing.addAll(area5050);
openTile = null;
}
} else { // found an open extension, now look for an extension for this
openTile = extension.tile1;
}
break;
}
}
}
}
}
}
/* This makes results worse, preumaby because some non-50/50s are getting through
// try and find a circular unavoidable 50/50. Not closed.
for (Link link: links) {
if (!link.processed && !link.closed1 && !link.closed2) { // not processed and open at both ends
Location openTile;
Location startTile;
int extensions = 0;
startTile = link.tile1;
openTile = link.tile2;
area5050.clear();
area5050.add(link.tile1);
area5050.add(link.tile2);
link.processed = true;
boolean noMatch = false;
while (openTile != null && !noMatch) {
noMatch = true;
for (Link extension: links) {
if (!extension.processed && !extension.closed1 && !extension.closed2) { // a circular 50/50 must have links open at both ends
if (extension.tile1.equals(openTile)) {
extension.processed = true;
noMatch = false;
// accumulate the trouble tiles as we progress;
link.trouble.addAll(extension.trouble);
area5050.add(extension.tile2); // tile2 is the new tile
if (extension.tile2.equals(startTile)) {
if (extensions % 2 == 0 ) { // && noTrouble(link, area5050)
board.getLogger().log(Level.WARN, "Tile %s is an unavoidable circular 50/50 guess, with %d extensions", openTile, extensions);
return board.getSolver().getLowest(area5050);
} else {
board.getLogger().log(Level.INFO, "Tile %s is a circular extension with %d parts", openTile, (extensions + 1));
deferGuessing.addAll(area5050);
openTile = null;
}
} else { // not closed the loop, so keep going
extensions++;
openTile = extension.tile2;
}
break;
}
if (extension.tile2.equals(openTile)) {
extension.processed = true;
noMatch = false;
// accumulate the trouble tiles as we progress;
link.trouble.addAll(extension.trouble);
area5050.add(extension.tile1); // tile 1 is the new tile
if (extension.tile1.equals(startTile)) {
if (extensions % 2 == 0 ) { // && noTrouble(link, area5050)
board.getLogger().log(Level.WARN, "Tile %s is an unavoidable circular 50/50 guess, with %d extensions", openTile, extensions);
return board.getSolver().getLowest(area5050);
} else {
board.getLogger().log(Level.INFO, "Tile %s is a circular extension with %d parts", openTile, (extensions + 1));
deferGuessing.addAll(area5050);
openTile = null;
}
} else { // not closed the loop, so keep going
extensions++;
openTile = extension.tile1;
}
break;
}
}
}
}
}
}
*/
board.getLogger().log(Level.INFO, "%d locations set to defered guessing", deferGuessing.size());
return null;
}
private boolean noTrouble(Link link, List<Location> area) {
// each trouble location must be adjacent to 2 tiles in the extended 50/50
top: for (Location tile: link.trouble) {
for (Location tile5050: area) {
if (tile.equals(tile5050)) {
continue top; //if a trouble tile is part of the 50/50 it isn't trouble
}
}
int adjCount = 0;
for (Location tile5050: area) {
if (tile.isAdjacent(tile5050)) {
adjCount++;
}
}
if (adjCount % 2 != 0) {
board.getLogger().log(Level.DEBUG, "Trouble Tile %s isn't adjacent to an even number of tiles in the extended candidate 50/50, adjacent %d of %d", tile, adjCount, area.size());
return false;
}
}
return true;
}
public boolean isDeferGuessing(Location l) {
return deferGuessing.contains(l);
}
/**
* Looks for pseudo-50/50s (which may be real 50/50s since we don't check any further)
*/
public Location process(ProbabilityEngineModel pe) {
board.getLogger().log(Level.INFO, "Starting search for 50/50s");
int minesLeft = board.getMines() - board.getConfirmedMineCount();
// horizontal 2x1
for (int i=0; i < board.getGameWidth() - 1; i++) {
for (int j=0; j < board.getGameHeight(); j++) {
// need 2 hidden tiles
if (!board.isUnrevealed(i, j) || !board.isUnrevealed(i + 1, j)) {
continue;
}
if (isPotentialInfo(i-1, j-1) || isPotentialInfo(i-1, j) || isPotentialInfo(i-1, j+1)
|| isPotentialInfo(i+2, j-1) || isPotentialInfo(i+2, j) || isPotentialInfo(i+2, j+1)) {
continue; // this skips the rest of the logic below this in the for-loop
}
Location tile1 = this.board.getLocation(i, j);
Location tile2 = this.board.getLocation(i + 1, j);
//board.getLogger().log(Level.INFO, tile1 + " and " + tile2 + " is candidate 50/50");
if (minesLeft > 1) {
// see if the 2 tiles can support 2 mines
List<Location> mines = new ArrayList<>();
mines.add(tile1);
mines.add(tile2);
SolutionCounter counter = board.getSolver().validatePosition(wholeEdge, mines, null, Area.EMPTY_AREA);
if (counter.getSolutionCount().signum() == 0) {
board.getLogger().log(Level.INFO, "%s and %s can't have 2 mines, guess immediately", tile1, tile2);
return tile1;
} else {
BigDecimal notPseudo = new BigDecimal(counter.getSolutionCount()).divide(new BigDecimal(pe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
BigDecimal pseudo = BigDecimal.ONE.subtract(notPseudo);
board.getLogger().log(Level.INFO, "%s and %s are not both mines %f of the time", tile1, tile2, pseudo);
if (pseudo.compareTo(bestNonPseudo2Tile5050Probability) > 0
|| pseudo.compareTo(bestNonPseudo2Tile5050Probability) == 0 && board.countAdjacentUnrevealed(tile1) < bestNonPseudo2TileAdjacent) {
bestNonPseudo2Tile5050Probability = pseudo;
bestNonPseudo2Tile5050Tile1 = tile1;
bestNonPseudo2Tile5050Tile2 = tile2;
bestNonPseudo2TileAdjacent = board.countAdjacentUnrevealed(bestNonPseudo2Tile5050Tile1);
}
}
} else {
board.getLogger().log(Level.INFO, "%s and %s can't have 2 mines since not enough mines left in the game, guess immediately", tile1, tile2);
return tile1;
}
}
}
// vertical 1x2
for (int i=0; i < board.getGameWidth(); i++) {
for (int j=0; j < board.getGameHeight() - 1; j++) {
// need 2 hidden tiles
if (!board.isUnrevealed(i, j) || !board.isUnrevealed(i, j + 1)) {
continue;
}
if (isPotentialInfo(i - 1, j - 1) || isPotentialInfo(i, j - 1) || isPotentialInfo(i + 1, j - 1)
|| isPotentialInfo(i - 1, j + 2) || isPotentialInfo(i, j + 2) || isPotentialInfo(i + 1, j + 2)) {
continue; // this skips the rest of the logic below this in the for-loop
}
Location tile1 = this.board.getLocation(i, j);
Location tile2 = this.board.getLocation(i, j + 1);
//board.getLogger().log(Level.INFO, tile1 + " and " + tile2 + " is candidate 50/50");
if (minesLeft > 1) {
// see if the 2 tiles can support 2 mines
List<Location> mines = new ArrayList<>();
mines.add(tile1);
mines.add(tile2);
SolutionCounter counter = board.getSolver().validatePosition(wholeEdge, mines, null, Area.EMPTY_AREA);
if (counter.getSolutionCount().signum() == 0) {
board.getLogger().log(Level.INFO, "%s and %s can't have 2 mines, guess immediately", tile1, tile2);
return tile1;
} else {
BigDecimal notPseudo = new BigDecimal(counter.getSolutionCount()).divide(new BigDecimal(pe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
BigDecimal pseudo = BigDecimal.ONE.subtract(notPseudo);
board.getLogger().log(Level.INFO, "%s and %s are not both mines %f of the time", tile1, tile2, pseudo);
if (pseudo.compareTo(bestNonPseudo2Tile5050Probability) > 0
|| pseudo.compareTo(bestNonPseudo2Tile5050Probability) == 0 && board.countAdjacentUnrevealed(tile1) < bestNonPseudo2TileAdjacent) {
bestNonPseudo2Tile5050Probability = pseudo;
bestNonPseudo2Tile5050Tile1 = tile1;
bestNonPseudo2Tile5050Tile2 = tile2;
bestNonPseudo2TileAdjacent = board.countAdjacentUnrevealed(tile1);
}
}
} else {
board.getLogger().log(Level.INFO, "%s and %s can't have 2 mines since not enought mines left in the game, guess immediately", tile1, tile2);
return tile1;
}
}
}
if (bestNonPseudo2Tile5050Probability.signum() > 0) {
board.getLogger().log(Level.INFO, "%s is the best 2 tile non-Pseudo5050 at %f", bestNonPseudo2Tile5050Tile2, bestNonPseudo2Tile5050Probability);
List<Location> mines = new ArrayList<>();
List<Location> noMines = new ArrayList<>();
mines.add(bestNonPseudo2Tile5050Tile1);
noMines.add(bestNonPseudo2Tile5050Tile2);
SolutionCounter counter1 = board.getSolver().validatePosition(wholeEdge, mines, noMines, Area.EMPTY_AREA);
mines.clear();
noMines.clear();
mines.add(bestNonPseudo2Tile5050Tile2);
noMines.add(bestNonPseudo2Tile5050Tile1);
SolutionCounter counter2 = board.getSolver().validatePosition(wholeEdge, mines, noMines, Area.EMPTY_AREA);
BigDecimal prob5050 = new BigDecimal(counter1.getSolutionCount().add(counter2.getSolutionCount())).divide(new BigDecimal(pe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
bestNonPseudo2Tilelts = BigDecimal.ONE.subtract(prob5050.multiply(HALF));
board.getLogger().log(Level.INFO, "%s and %s form a 50/50 %f of the time giving long term safety of %f", bestNonPseudo2Tile5050Tile1, bestNonPseudo2Tile5050Tile1, prob5050, this.bestNonPseudo2Tilelts);
}
/*
// box 2x2
Location[] tiles = new Location[4];
List<Location> mines = new ArrayList<>();
List<Location> noMines = new ArrayList<>();
for (int i=0; i < board.getGameWidth() - 1; i++) {
for (int j=0; j < board.getGameHeight() - 1; j++) {
// need 4 hidden tiles
if (!board.isUnrevealed(i, j) || !board.isUnrevealed(i, j + 1) || !board.isUnrevealed(i + 1, j) || !board.isUnrevealed(i + 1, j + 1)) {
continue;
}
// need the corners to be flags or off the board
if (isPotentialInfo(i - 1, j - 1) || isPotentialInfo(i + 2, j - 1) || isPotentialInfo(i - 1, j + 2) || isPotentialInfo(i + 2, j + 2)) {
continue; // this skips the rest of the logic below this in the for-loop
}
tiles[0] = new Location(i, j);
tiles[1] = new Location(i + 1, j);
tiles[2] = new Location(i, j + 1);
tiles[3] = new Location(i + 1, j + 1);
board.getLogger().log(Level.INFO, "%s %s %s %s is candidate box pseudo-50/50", tiles[0], tiles[1], tiles[2], tiles[3]);
// keep track of which tiles are risky - once all 4 are then not a pseudo-50/50
int riskyTiles = 0;
boolean[] risky = new boolean[4];
// check each tile is in the web and that at least one is living
boolean okay = true;
boolean allDead = true;
for (int l = 0; l < 4; l++) {
if (!this.deadLocations.contains(tiles[l])) {
allDead = false;
} else {
riskyTiles++;
risky[l] = true; // since we'll never select a dead tile, consider them risky
}
if (!this.wholeEdge.isOnWeb(tiles[l])) {
board.getLogger().log(Level.DEBUG, "%s has no witnesses, so nothing to check", tiles[l]);
okay = false;
break;
}
}
if (!okay || allDead) {
continue;
}
int start;
if (minesLeft > 3) {
start = 0;
} else if (minesLeft == 3) {
start = 1;
} else if (minesLeft == 2) {
start = 5;
} else {
start = 9;
}
for (int k = start; k < PATTERNS.length; k++) {
mines.clear();
noMines.clear();
boolean run = false;
// allocate each position as a mine or noMine
for (int l = 0; l < 4; l++) {
if (PATTERNS[k][l]) {
mines.add(tiles[l]);
if (!risky[l]) {
run = true;
}
} else {
noMines.add(tiles[l]);
}
}
// only run if this pattern can discover something we don't already know
if (!run) {
board.getLogger().log(Level.DEBUG, "Pattern %d skipped", k);
continue;
}
// see if the position is valid
SolutionCounter counter = board.getSolver().validatePosition(wholeEdge, mines, noMines, Area.EMPTY_AREA);
// if it is then mark each mine tile as risky
if (counter.getSolutionCount().signum() != 0) {
board.getLogger().log(Level.DEBUG, "Pattern %d is valid", k);
for (int l = 0; l < 4; l++) {
if (PATTERNS[k][l]) {
if (!risky[l]) {
risky[l] = true;
riskyTiles++;
}
}
}
if (riskyTiles == 4) {
break;
}
} else {
board.getLogger().log(Level.DEBUG, "Pattern %d is not valid", k);
}
}
// if not all 4 tiles are risky then send back one which isn't
if (riskyTiles != 4) {
for (int l = 0; l < 4; l++) {
// if not risky and not dead then select it
if (!risky[l] && !deadLocations.contains(tiles[l])) {
board.getLogger().log(Level.INFO, "%s %s %s %s is pseudo 50/50 - " + tiles[l].toString() + " is not risky", tiles[0], tiles[1], tiles[2], tiles[3]);
return tiles[l];
}
}
}
}
}
*/
// can't create a 2x2 50/50 if only 1 tile left
if (minesLeft < 2) {
return null;
}
// box 2x2
Location[] tiles = new Location[4];
List<Location> mines = new ArrayList<>();
List<Location> noMines = new ArrayList<>();
for (int i=0; i < board.getGameWidth() - 1; i++) {
for (int j=0; j < board.getGameHeight() - 1; j++) {
// need 4 hidden tiles
if (!board.isUnrevealed(i, j) || !board.isUnrevealed(i, j + 1) || !board.isUnrevealed(i + 1, j) || !board.isUnrevealed(i + 1, j + 1)) {
continue;
}
// need the corners to be flags or off the board
if (isPotentialInfo(i - 1, j - 1) || isPotentialInfo(i + 2, j - 1) || isPotentialInfo(i - 1, j + 2) || isPotentialInfo(i + 2, j + 2)) {
continue; // this skips the rest of the logic below this in the for-loop
}
tiles[0] = this.board.getLocation(i, j);
tiles[1] = this.board.getLocation(i + 1, j);
tiles[2] = this.board.getLocation(i, j + 1);
tiles[3] = this.board.getLocation(i + 1, j + 1);
board.getLogger().log(Level.INFO, "%s %s %s %s is candidate box pseudo-50/50", tiles[0], tiles[1], tiles[2], tiles[3]);
mines.clear();
noMines.clear();
mines.add(tiles[0]);
mines.add(tiles[3]);
noMines.add(tiles[1]);
noMines.add(tiles[2]);
// see if the position is valid
SolutionCounter counter = board.getSolver().validatePosition(wholeEdge, mines, noMines, Area.EMPTY_AREA);
for (Location t: tiles) {
if (!this.deadLocations.contains(t)) {
Box b = pe.getBox(t);
if (b != null) {
board.getLogger().log(Level.INFO, "Tile %s has tally %d, new board has solutions %d", t, b.getTally(), counter.getSolutionCount());
if (b.getTally().compareTo(counter.getSolutionCount()) == 0) {
board.getLogger().log(Level.INFO, "%s %s %s %s is pseudo 50/50 - %s is not risky", tiles[0], tiles[1], tiles[2], tiles[3], t);
return t;
}
} else {
board.getLogger().log(Level.INFO, "Tile %s is not on the boundary", t);
}
}
}
}
}
return null;
}
public BigDecimal getBestNonPseudo2Tile5050Probability() {
return this.bestNonPseudo2Tile5050Probability;
}
public Location getBestNonPseudo2Tile() {
return this.bestNonPseudo2Tile5050Tile1;
}
public BigDecimal getLongTermSafety() {
return bestNonPseudo2Tilelts;
}
// returns whether the tile is still valid even if it has no witnesses
/*
private boolean isExempt(Location l) {
// if not test mode then no exemption
if (!board.getSolver().preferences.isTestMode()) {
return false;
}
// if the tile is in a corner then it is exempt
if ((l.x == 0 || l.x == board.getGameWidth() - 1) && (l.y == 0 || l.y == board.getGameHeight() - 1)) {
return true;
}
return false;
}
*/
// returns whether there information to be had at this location; i.e. on the board and either unrevealed or revealed
private boolean isPotentialInfo(int x, int y) {
if (x < 0 || x >= board.getGameWidth() || y < 0 || y >= board.getGameHeight()) {
return false;
}
if (board.isConfirmedMine(x, y)) {
return false;
} else {
return true;
}
}
}

View File

@ -0,0 +1,19 @@
package minesweeper.solver;
import java.util.Collection;
import java.util.List;
import minesweeper.solver.constructs.EvaluatedLocation;
import minesweeper.structure.Action;
import minesweeper.structure.Location;
public interface LocationEvaluator {
abstract public Action[] bestMove();
abstract public List<EvaluatedLocation> getEvaluatedLocations();
abstract public void evaluateLocations();
abstract public void showResults();
abstract public void evaluateOffEdgeCandidates(List<Location> allUnrevealedSquares);
abstract public void addLocations(Collection<? extends Location> tiles);
}

View File

@ -0,0 +1,672 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import minesweeper.solver.constructs.Box;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
public class LongTermRiskHelper {
private final static BigDecimal APPROX_THRESHOLD = new BigDecimal("0.01");
private class Result {
private final BigInteger influence;
private final List<Location> enablers; // these are mines which needed to form the outer part of the 50/50
private Result(BigInteger influence, List<Location> enablers) {
this.influence = influence;
this.enablers = enablers;
}
}
private final BoardState board;
private final WitnessWeb wholeEdge;
private final ProbabilityEngineModel currentPe;
// the number of solutions that come from 50/50s for each tile
// this is only an approximation based on the most common types of 50/50
private BigInteger[][] influence5050s;
private BigInteger[][] influenceEnablers;
private Location pseudo;
final List<Location> mines = new ArrayList<>();
final List<Location> notMines = new ArrayList<>();
public LongTermRiskHelper(BoardState board, WitnessWeb wholeEdge, ProbabilityEngineModel pe) {
this.board = board;
this.wholeEdge = wholeEdge;
this.currentPe = pe;
influence5050s = new BigInteger[board.getGameWidth()][board.getGameHeight()];
influenceEnablers = new BigInteger[board.getGameWidth()][board.getGameHeight()];
}
/**
* Scan whole board looking for tiles heavily influenced by 50/50s
*/
public Location findInfluence() {
checkFor2Tile5050();
checkForBox5050();
if (pseudo != null) {
board.getLogger().log(Level.INFO, "Tile %s is a 50/50, or safe", pseudo);
}
return pseudo;
}
/**
* Get the 50/50 influence for a particular tile
*/
public BigInteger findInfluence(Location tile) {
final int minesLeft = board.getMines() - board.getConfirmedMineCount();
BigInteger influence = BigInteger.ZERO;
Location tile1, tile2, tile3;
// 2-tile 50/50
tile1 = board.getLocation(tile.x - 1, tile.y);
BigInteger influence1 = BigInteger.ZERO;
influence1 = maxNotNull(BigInteger.ZERO, getHorizontal(tile, 4, 4, minesLeft));
influence1 = maxNotNull(influence1, getHorizontal(tile1, 4, 4, minesLeft));
influence = influence.add(influence1);
tile2 = board.getLocation(tile.x, tile.y - 1);
BigInteger influence2 = BigInteger.ZERO;
influence2 = maxNotNull(influence2, getVertical(tile, 4, 4, minesLeft));
influence2 = maxNotNull(influence2, getVertical(tile2, 4, 4, minesLeft));
influence = influence.add(influence2);
// 4-tile 50/50
tile3 = board.getLocation(tile.x - 1, tile.y - 1);
BigInteger influence4 = BigInteger.ZERO;
influence4 = maxNotNull(influence4, getBoxInfluence(tile, 5, 5, minesLeft));
influence4 = maxNotNull(influence4, getBoxInfluence(tile1, 5, 5, minesLeft));
influence4 = maxNotNull(influence4, getBoxInfluence(tile2, 5, 5, minesLeft));
influence4 = maxNotNull(influence4, getBoxInfluence(tile3, 5, 5, minesLeft));
influence = influence.add(influence4);
board.getLogger().log(Level.INFO, "Tile %s base influence tally %d", tile, influence);
// enablers also get influence as playing there also removes the 50/50 risk, so consider that as well as the 50/50
if (influenceEnablers[tile.x][tile.y] != null) {
influence = influence.add(influenceEnablers[tile.x][tile.y]);
}
BigInteger maxInfluence;
Box box = currentPe.getBox(tile);
if (box == null) {
maxInfluence = currentPe.getOffEdgeTally();
} else {
maxInfluence = box.getTally();
}
// 50/50 influence P(50/50)/2 can't be larger than P(mine) or P(safe)
BigInteger other = currentPe.getSolutionCount().subtract(maxInfluence);
maxInfluence = maxInfluence.min(other);
influence = influence.min(maxInfluence);
return influence;
}
private void checkFor2Tile5050() {
final int minMissingMines = 2;
final int maxMissingMines = 2;
board.getLogger().log(Level.INFO, "Checking for 2-tile 50/50 influence");
final int minesLeft = board.getMines() - board.getConfirmedMineCount();
// horizontal 2x1
for (int i=0; i < board.getGameWidth() - 1; i++) {
for (int j=0; j < board.getGameHeight(); j++) {
Location tile1 = board.getLocation(i, j);
Location tile2 = board.getLocation(i + 1, j);
Result result = getHorizontal(tile1, minMissingMines, maxMissingMines, minesLeft);
if (result != null) {
BigInteger influenceTally = addNotNull(BigInteger.ZERO, result);
BigDecimal influence = new BigDecimal(influenceTally).divide(new BigDecimal(currentPe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
board.getLogger().log(Level.INFO, "%s and %s have horiontal 2-tile 50/50 influence %f", tile1, tile2, influence);
addInfluence(influenceTally, result.enablers, tile1, tile2);
if (pseudo != null) { // if we've found a pseudo then we can stop here
return;
}
}
}
}
// vertical 2x1
for (int i=0; i < board.getGameWidth(); i++) {
for (int j=0; j < board.getGameHeight() - 1; j++) {
Location tile1 = board.getLocation(i, j);
Location tile2 = board.getLocation(i, j + 1);
Result result = getVertical(tile1, minMissingMines, maxMissingMines, minesLeft);
if (result != null) {
BigInteger influenceTally = addNotNull(BigInteger.ZERO, result);
BigDecimal influence = new BigDecimal(influenceTally).divide(new BigDecimal(currentPe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
board.getLogger().log(Level.INFO, "%s and %s have vertical 2-tile 50/50 influence %f", tile1, tile2, influence);
addInfluence(influenceTally, result.enablers, tile1, tile2);
if (pseudo != null) { // if we've found a pseudo then we can stop here
return;
}
}
}
}
}
private Result getHorizontal(final Location subject, final int minMissingMines, final int maxMissingMines, final int minesLeft) {
if (subject == null) {
return null;
}
int i = subject.x;
int j = subject.y;
if (i < 0 || i + 1 >= board.getGameWidth()) { // need 1 extra space to the right
return null;
}
// need 2 hidden tiles
if (!isHidden(i, j) || !isHidden(i + 1, j)) {
return null;
}
List<Location> missingMines = getMissingMines(board.getLocation(i-1, j-1), board.getLocation(i-1, j), board.getLocation(i-1, j+1),
board.getLocation(i+2, j-1), board.getLocation(i+2, j), board.getLocation(i+2, j+1));
// only consider possible 50/50s with less than 3 missing mines or requires more mines then are left in the game (plus 1 to allow for the extra mine in the 50/50)
if (missingMines == null || missingMines.size() + 1 > maxMissingMines || missingMines.size() + 1 > minesLeft) {
return null;
}
Location tile1 = subject;
Location tile2 = board.getLocation(i + 1, j);
BigDecimal approxChance = calculateApproxChanceOf5050(missingMines, tile1);
board.getLogger().log(Level.INFO, "Evaluating candidate 50/50 - %s %s - approx chance %f", tile1, tile2, approxChance);
// if the estimate chance is too low then don't consider it
if (missingMines.size() + 1 > minMissingMines && approxChance.compareTo(APPROX_THRESHOLD) < 0) {
return null;
}
mines.clear();
notMines.clear();
// add the missing Mines and the mine required to form the 50/50
mines.addAll(missingMines);
mines.add(tile1);
notMines.add(tile2);
SolutionCounter counter = board.getSolver().validatePosition(wholeEdge, mines, notMines, Area.EMPTY_AREA);
return new Result(counter.getSolutionCount(), missingMines);
}
private Result getVertical(final Location subject, final int minMissingMines, final int maxMissingMines, final int minesLeft) {
if (subject == null) {
return null;
}
int i = subject.x;
int j = subject.y;
if (j < 0 || j + 1 >= board.getGameHeight()) { // need 1 extra space below
return null;
}
// need 2 hidden tiles
if (!isHidden(i, j) || !isHidden(i, j + 1)) {
return null;
}
List<Location> missingMines = getMissingMines(board.getLocation(i-1, j-1), board.getLocation(i, j - 1), board.getLocation(i + 1, j - 1),
board.getLocation(i - 1, j + 2), board.getLocation(i, j + 2), board.getLocation(i + 1, j + 2));
// only consider possible 50/50s with less than 3 missing mines or requires more mines then are left in the game (plus 1 to allow for the extra mine in the 50/50)
if (missingMines == null || missingMines.size() + 1 > maxMissingMines || missingMines.size() + 1 > minesLeft) {
return null;
}
Location tile1 = board.getLocation(i, j);
Location tile2 = board.getLocation(i, j + 1);
BigDecimal approxChance = calculateApproxChanceOf5050(missingMines, tile1);
board.getLogger().log(Level.INFO, "Evaluating candidate 50/50 - %s %s - approx chance %f", tile1, tile2, approxChance);
// if the estimate chance is too low then don't consider it
if (missingMines.size() + 1 > minMissingMines && approxChance.compareTo(APPROX_THRESHOLD) < 0) {
return null;
}
mines.clear();
notMines.clear();
// add the missing Mines and the mine required to form the 50/50
mines.addAll(missingMines);
mines.add(tile1);
notMines.add(tile2);
SolutionCounter counter = board.getSolver().validatePosition(wholeEdge, mines, notMines, Area.EMPTY_AREA);
return new Result(counter.getSolutionCount(), missingMines);
}
private void checkForBox5050() {
final int minMissingMines = 2;
final int maxMissingMines = 2;
int minesLeft = board.getMines() - board.getConfirmedMineCount();
board.getLogger().log(Level.INFO, "Checking for 2-tile 50/50 influence: Mines left %d", minesLeft);
// box 2x2
for (int i=0; i < board.getGameWidth() - 1; i++) {
for (int j=0; j < board.getGameHeight() - 1; j++) {
Location tile1 = board.getLocation(i, j);
Location tile2 = board.getLocation(i, j + 1);
Location tile3 = board.getLocation(i + 1, j);
Location tile4 = board.getLocation(i + 1, j + 1);
Result result = getBoxInfluence(tile1, minMissingMines, maxMissingMines, minesLeft);
if (result != null) {
BigInteger influenceTally = addNotNull(BigInteger.ZERO, result);
BigDecimal influence = new BigDecimal(influenceTally).divide(new BigDecimal(currentPe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
board.getLogger().log(Level.INFO, "%s %s %s %s have box 4-tile 50/50 influence %f", tile1, tile2, tile3, tile4, influence);
addInfluence(influenceTally, result.enablers, tile1, tile2, tile3, tile4);
if (pseudo != null) { // if we've found a pseudo then we can stop here
return;
}
}
}
}
}
private Result getBoxInfluence(final Location subject, final int minMissingMines, final int maxMissingMines, final int minesLeft) {
if (subject == null) {
return null;
}
int i = subject.x;
int j = subject.y;
if (j < 0 || j + 1 >= board.getGameHeight() || i < 0 || i + 1 >= board.getGameWidth()) { // need 1 extra space to the right and below
return null;
}
// need 4 hidden tiles
if (!isHidden(i, j) || !isHidden(i, j + 1) || !isHidden(i + 1, j) || !isHidden(i + 1, j + 1)) {
return null;
}
List<Location> missingMines = getMissingMines(board.getLocation(i - 1, j - 1), board.getLocation(i + 2, j - 1), board.getLocation(i - 1, j + 2), board.getLocation(i + 2, j + 2));
// only consider possible 50/50s with less than 3 missing mines or requires more mines then are left in the game (plus 1 to allow for the extra mine in the 50/50)
if (missingMines == null || missingMines.size() + 2 > maxMissingMines || missingMines.size() + 2 > minesLeft) {
return null;
}
Location tile1 = board.getLocation(i, j);
Location tile2 = board.getLocation(i, j + 1);
Location tile3 = board.getLocation(i + 1, j);
Location tile4 = board.getLocation(i + 1, j + 1);
BigDecimal approxChance = calculateApproxChanceOf5050(missingMines, tile1, tile4);
board.getLogger().log(Level.INFO, "Evaluating candidate 50/50 - %s %s %s %s - approx chance %f", tile1, tile2, tile3, tile4, approxChance);
// if the estimate chance is too low then don't consider it
if (missingMines.size() + 2 > minMissingMines && approxChance.compareTo(APPROX_THRESHOLD) < 0) {
return null;
}
mines.clear();
notMines.clear();
// add the missing Mines and the mine required to form the 50/50
mines.addAll(missingMines);
mines.add(tile1);
mines.add(tile4);
notMines.add(tile2);
notMines.add(tile3);
SolutionCounter counter = board.getSolver().validatePosition(wholeEdge, mines, notMines, Area.EMPTY_AREA);
board.getLogger().log(Level.INFO, "Candidate 50/50 - %s %s %s %s influence %d", tile1, tile2, tile3, tile4, counter.getSolutionCount());
return new Result(counter.getSolutionCount(), missingMines);
}
private BigDecimal calculateApproxChanceOf5050(List<Location> missingMines, Location... other) {
BigDecimal result = BigDecimal.ONE;
for (Location tile: missingMines) {
result = result.multiply(BigDecimal.ONE.subtract(this.currentPe.getProbability(tile)));
}
for (Location tile: other) {
result = result.multiply(BigDecimal.ONE.subtract(this.currentPe.getProbability(tile)));
}
return result;
}
private BigInteger addNotNull(BigInteger influence, Result result) {
if (result == null) {
return influence;
} else {
return function(influence, result.influence);
}
}
private BigInteger maxNotNull(BigInteger influence, Result result) {
if (result == null) {
return influence;
} else {
return influence.max(result.influence);
}
}
private void addInfluence(BigInteger influence, List<Location> enablers, Location... tiles) {
List<Location> pseudos = new ArrayList<>();
// the tiles which enable a 50/50 but aren't in it also get an influence
if (enablers != null) {
//BigInteger influence2 = influence.multiply(BigInteger.valueOf(2)).divide(BigInteger.valueOf(3));
for (Location loc: enablers) {
// store the influence
if (influenceEnablers[loc.x][loc.y] == null) {
influenceEnablers[loc.x][loc.y] = influence;
} else {
influenceEnablers[loc.x][loc.y] = function(influenceEnablers[loc.x][loc.y],influence);
}
}
}
for (Location loc: tiles) {
Box b = currentPe.getBox(loc);
BigInteger mineTally;
if (b == null) {
mineTally = currentPe.getOffEdgeTally();
} else {
mineTally = b.getTally();
}
// If the mine influence covers the whole of the mine tally then it is a pseudo-5050
//if (influence.compareTo(mineTally) == 0 && pseudo == null) {
// if (!currentPe.getDeadLocations().contains(loc)) { // don't accept dead tiles
// //board.getLogger().log(Level.INFO, "Tile %s is a 50/50, or safe", loc);
// pseudo = loc;
// }
//}
if (influence.compareTo(mineTally) == 0 && pseudo == null) {
if (!currentPe.getDeadLocations().contains(loc)) { // don't accept dead tiles
//board.getLogger().log(Level.INFO, "Tile %s is a 50/50, or safe", loc);
pseudos.add(loc);
}
}
// store the influence
if (influence5050s[loc.x][loc.y] == null) {
influence5050s[loc.x][loc.y] = influence;
} else {
//influences[loc.x][loc.y] = influences[loc.x][loc.y].max(influence);
influence5050s[loc.x][loc.y] = function(influence5050s[loc.x][loc.y],influence);
}
}
if (pseudos.size() == 3) {
pickPseudo(pseudos);
} else if (!pseudos.isEmpty()) {
pseudo = pseudos.get(0);
}
}
private void pickPseudo(List<Location> locations) {
int maxX = 0;
int maxY = 0;
for (Location loc: locations) {
maxX = Math.max(maxX, loc.x);
maxY = Math.max(maxY, loc.y);
}
int maxX1 = maxX - 1;
int maxY1 = maxY - 1;
int found = 0;
// see if this diagonal exists in the pseudo candidates
for (Location loc: locations) {
if (loc.x == maxX && loc.y == maxY || loc.x == maxX1 && loc.y == maxY1) {
found++;
}
}
// if the 2 diagonals exist then choose the pseudo from those, other wise choose the pseudo from the other diagonal
if (found == 2) {
pseudo = board.getLocation(maxX, maxY);
} else {
pseudo = board.getLocation(maxX - 1, maxY);
}
}
/**
* Get how many solutions have common 50/50s at this location
*/
/*
public BigInteger get5050Influence(Location loc) {
BigInteger result = BigInteger.ZERO;
if (influence5050s[loc.x][loc.y] != null) {
result = result.add(influence5050s[loc.x][loc.y]);
}
if (influenceEnablers[loc.x][loc.y] != null) {
result = result.add(influenceEnablers[loc.x][loc.y]);
}
return result;
}
*/
/**
* Return all the locations with 50/50 influence
*/
public List<Location> getInfluencedTiles(BigDecimal threshold) {
BigInteger cuttoffTally = threshold.multiply(new BigDecimal(currentPe.getSolutionCount())).toBigInteger();
List<Location> result = new ArrayList<>();
for (int i=0; i < board.getGameWidth(); i++) {
for (int j=0; j < board.getGameHeight(); j++) {
BigInteger influence = BigInteger.ZERO;
if (influence5050s[i][j] != null) {
influence = influence.add(influence5050s[i][j]);
}
if (influenceEnablers[i][j] != null) {
influence = influence.add(influenceEnablers[i][j]);
}
if (influence.signum() !=0 ) { // if we are influenced by 50/50s
Location loc = board.getLocation(i,j);
if (!currentPe.getDeadLocations().contains(loc)) { // and not dead
Box b = currentPe.getBox(loc);
BigInteger mineTally;
if (b == null) {
mineTally = currentPe.getOffEdgeTally();
} else {
mineTally = b.getTally();
}
BigInteger safetyTally = currentPe.getSolutionCount().subtract(mineTally).add(influence);
if (safetyTally.compareTo(cuttoffTally) > 0) {
board.getLogger().log(Level.INFO, "Tile %s has influence %d cutoff %d", loc, safetyTally, cuttoffTally);
result.add(loc);
}
}
}
}
}
return result;
}
// should we add or use max? Make a single place to change.
private BigInteger function(BigInteger a, BigInteger b) {
return a.add(b);
}
// given a list of tiles return those which are on the board but not a mine
// if any of the tiles are revealed then return null
private List<Location> getMissingMines(Location... tiles) {
List<Location> result = new ArrayList<>();
for (Location loc: tiles) {
// if out of range don't return the location
if (loc == null) {
continue;
}
// if the tile is revealed then we can't form a 50/50 here
if (board.isRevealed(loc)) {
return null;
}
// if the location is already a mine then don't return the location
if (board.isConfirmedMine(loc) || isMineInPe(loc.x, loc.y)) {
continue;
}
result.add(loc);
}
return result;
}
// returns whether there information to be had at this location; i.e. on the board and either unrevealed or revealed
private boolean isPotentialInfo(int x, int y) {
if (x < 0 || x >= board.getGameWidth() || y < 0 || y >= board.getGameHeight()) {
return false;
}
if (board.isConfirmedMine(x, y) || isMineInPe(x, y)) {
return false;
} else {
return true;
}
}
// not a certain mine or revealed
private boolean isHidden(int x, int y) {
if (board.isConfirmedMine(x, y)) {
return false;
}
if (board.isRevealed(x, y)) {
return false;
}
//if (isMineInPe(x, y)) {
// return false;
//}
return true;
}
private boolean isMineInPe(int x, int y) {
for (Location loc: this.currentPe.getMines()) {
if (loc.x == x && loc.y == y) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,569 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
public class LongTermRiskHelperOld {
private boolean[][] PATTERNS = new boolean[][] {{true, false, false, true}, {false, true, true, false}}; // 2 ways to make a 50/50 in a box
private class Risk5050 {
private Location poi;
private List<Location> area;
private List<Location> livingArea = new ArrayList<>();
private Risk5050(Location poi, Location... locs) {
this.poi = poi;
this.area = Arrays.asList(locs);
for (Location loc: locs) {
if (!deadLocations.contains(loc)) {
livingArea.add(loc);
}
}
}
}
private final static BigDecimal HALF = new BigDecimal("0.5");
private final BoardState board;
private final WitnessWeb wholeEdge;
private final ProbabilityEngineModel currentPe;
private final Area deadLocations;
private final List<Location> fifty;
private BigDecimal currentLongTermSafety;
private Risk5050 worstBox5050;
private BigDecimal box5050Safety = BigDecimal.ONE;
private BigDecimal twoTileSafety;
private List<Risk5050> risk5050s = new ArrayList<>();
public LongTermRiskHelperOld(BoardState board, WitnessWeb wholeEdge, ProbabilityEngineModel pe) {
this.board = board;
this.wholeEdge = wholeEdge;
this.currentPe = pe;
this.deadLocations = pe.getDeadLocations();
this.fifty = currentPe.getFiftyPercenters();
// sort into location order
fifty.sort(null);
}
public void findRisks() {
checkFor2Tile5050();
//checkForBox5050();
this.currentLongTermSafety = this.box5050Safety.multiply(this.twoTileSafety);
}
public void checkFor2Tile5050() {
BigDecimal longTermSafety = BigDecimal.ONE;
for (int i=0; i < fifty.size(); i++) {
Location tile1 = fifty.get(i);
Location tile2 = null;
Location info = null;
Risk5050 risk = null;
for (int j=i+1; j < fifty.size(); j++) {
tile2 = fifty.get(j);
// tile2 is below tile1
if (tile1.x == tile2.x && tile1.y == tile2.y - 1) {
info = checkVerticalInfo(tile1, tile2);
if (info == null) { // try extending it
Location tile3 = getFifty(tile2.x, tile2.y + 2);
Location tile4 = getFifty(tile2.x, tile2.y + 3);
if (tile3 != null && tile4 != null) {
info = checkVerticalInfo(tile1, tile4);
if (info != null) {
risk = new Risk5050(info, tile1, tile2, tile3, tile4);
}
}
} else {
risk = new Risk5050(info, tile1, tile2);
}
break;
}
// tile 2 is right of tile1
if (tile1.x == tile2.x - 1 && tile1.y == tile2.y) {
info = checkHorizontalInfo(tile1, tile2);
if (info == null) { // try extending it
Location tile3 = getFifty(tile2.x + 2, tile2.y);
Location tile4 = getFifty(tile2.x + 3, tile2.y);
if (tile3 != null && tile4 != null) {
info = checkHorizontalInfo(tile1, tile4);
if (info != null) {
risk = new Risk5050(info, tile1, tile2, tile3, tile4);
}
}
} else {
risk = new Risk5050(info, tile1, tile2);
}
break;
}
}
// if the 2 fifties form a pair with only 1 remaining source of information
if (risk != null) {
risk5050s.add(risk); // store the positions of interest
BigDecimal safety = BigDecimal.ONE.subtract(BigDecimal.ONE.subtract(currentPe.getProbability(info)).multiply(HALF));
board.getLogger().log(Level.INFO, "Seed %d - %s %s has 1 remaining source of information - tile %s %f", board.getSolver().getGame().getSeed(), tile1, tile2, info, safety);
longTermSafety = longTermSafety.multiply(safety);
}
}
if (longTermSafety.compareTo(BigDecimal.ONE) != 0) {
board.getLogger().log(Level.INFO, "Seed %d - Total long term safety %f", board.getSolver().getGame().getSeed(), longTermSafety);
}
this.twoTileSafety = longTermSafety;
}
private boolean isFifty(int x, int y) {
return (getFifty(x, y) != null);
}
private Location getFifty(int x, int y) {
for (Location loc: fifty) {
if (loc.x == x && loc.y == y) {
return loc;
}
}
return null;
}
public List<Location> get5050Breakers() {
List<Location> breakers = new ArrayList<>();
if (board.getSolver().preferences.considerLongTermSafety()) {
for (Risk5050 risk: risk5050s) {
breakers.addAll(risk.livingArea);
breakers.add(risk.poi);
}
}
return breakers;
}
public BigDecimal getLongTermSafety() {
return this.currentLongTermSafety;
}
public BigDecimal getLongTermSafety(Location candidate, ProbabilityEngineModel pe) {
BigDecimal longTermSafety = null;
if (board.getSolver().preferences.considerLongTermSafety()) {
// if there is a possible box 50/50 then see if we are breaking it, otherwise use that as the start safety
if (worstBox5050 != null) {
if (worstBox5050.poi.equals(candidate) || pe.getProbability(worstBox5050.poi).compareTo(BigDecimal.ONE) == 0) {
//board.getLogger().log(Level.INFO, "%s has broken 50/50", candidate);
longTermSafety = BigDecimal.ONE;
} else {
for (Location loc: worstBox5050.area) {
if (loc.equals(candidate)) {
//board.getLogger().log(Level.INFO, "%s has broken 50/50", candidate);
longTermSafety = BigDecimal.ONE;
break;
}
}
}
if (longTermSafety == null) {
longTermSafety = this.box5050Safety;
}
} else {
longTermSafety = BigDecimal.ONE;
}
for (Risk5050 risk: this.risk5050s) {
BigDecimal safety = null;
// is the candidate part of the 50/50 - if so it is being broken
for (Location loc: risk.area) {
if (loc.equals(candidate)) {
safety = BigDecimal.ONE;
break;
}
}
if (safety == null) {
if (risk.poi.equals(candidate)) {
safety = BigDecimal.ONE;
} else {
safety = BigDecimal.ONE.subtract(BigDecimal.ONE.subtract(pe.getProbability(risk.poi)).multiply(HALF));
}
}
longTermSafety = longTermSafety.multiply(safety);
}
} else {
longTermSafety = BigDecimal.ONE;
}
return longTermSafety;
}
// returns the location of the 1 tile which can still provide information, or null
private Location checkVerticalInfo(Location tile1, Location tile2) {
Location info = null;
final int top = tile1.y - 1;
final int bottom = tile2.y + 1;
final int left = tile1.x - 1;
if (isPotentialInfo(left, top)) {
if (board.isRevealed(left, top)) {
return null;
} else {
info = new Location(left, top);
}
}
if (isPotentialInfo(left + 1, top)) {
if (board.isRevealed(left + 1, top)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left + 1, top);
}
if (isPotentialInfo(left + 2, top)) {
if (board.isRevealed(left + 2, top)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left + 2, top);
}
if (isPotentialInfo(left, bottom)) {
if (board.isRevealed(left, bottom)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left, bottom);
}
if (isPotentialInfo(left + 1, bottom)) {
if (board.isRevealed(left + 1, bottom)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left + 1, bottom);
}
if (isPotentialInfo(left + 2, bottom)) {
if (board.isRevealed(left + 2, bottom)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left + 2, bottom);
}
return info;
}
// returns the location of the 1 tile which can still provide information, or null
private Location checkHorizontalInfo(Location tile1, Location tile2) {
Location info = null;
final int top = tile1.y - 1;
final int left = tile1.x - 1;
final int right = tile2.x + 1;
if (isPotentialInfo(left, top)) {
if (board.isRevealed(left, top)) {
return null;
} else {
info = new Location(left, top);
}
}
if (isPotentialInfo(left, top + 1)) {
if (board.isRevealed(left, top + 1)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left, top + 1);
}
if (isPotentialInfo(left, top + 2)) {
if (board.isRevealed(left, top + 2)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left, top + 2);
}
if (isPotentialInfo(right, top)) {
if (board.isRevealed(right, top)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(right, top);
}
if (isPotentialInfo(right, top + 1)) {
if (board.isRevealed(right, top + 1)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(right, top + 1);
}
if (isPotentialInfo(right, top + 2)) {
if (board.isRevealed(right, top + 2)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(right, top + 2);
}
return info;
}
private void checkForBox5050() {
// box 2x2
Location[] tiles = new Location[4];
BigDecimal maxProbability = BigDecimal.ZERO;
Risk5050 worst5050 = null;
List<Location> mines = new ArrayList<>();
List<Location> noMines = new ArrayList<>();
for (int i=0; i < board.getGameWidth() - 1; i++) {
for (int j=0; j < board.getGameHeight() - 1; j++) {
// need 4 hidden tiles
if (!board.isUnrevealed(i, j) || !board.isUnrevealed(i, j + 1) || !board.isUnrevealed(i + 1, j) || !board.isUnrevealed(i + 1, j + 1)) {
continue;
}
tiles[0] = new Location(i, j);
Location info = checkBoxInfo(tiles[0]);
// need the corners to be flags or off the board
if (info == null) {
continue; // this skips the rest of the logic below this in the for-loop
}
tiles[1] = new Location(i + 1, j);
tiles[2] = new Location(i, j + 1);
tiles[3] = new Location(i + 1, j + 1);
BigInteger solutions = BigInteger.ZERO;
for (int k = 0; k < PATTERNS.length; k++) {
mines.clear();
noMines.clear();
mines.add(info); // the missing mine
// allocate each position as a mine or noMine
for (int l = 0; l < 4; l++) {
if (PATTERNS[k][l]) {
mines.add(tiles[l]);
} else {
noMines.add(tiles[l]);
}
}
// see if the position is valid
SolutionCounter counter = board.getSolver().validatePosition(wholeEdge, mines, noMines, Area.EMPTY_AREA);
// if it is then mark each mine tile as risky
if (counter.getSolutionCount().signum() != 0) {
board.getLogger().log(Level.DEBUG, "Pattern %d is valid with %d solutions", k, counter.getSolutionCount());
solutions = solutions.add(counter.getSolutionCount());
} else {
board.getLogger().log(Level.DEBUG, "Pattern %d is not valid", k);
}
}
BigDecimal probability = new BigDecimal(solutions).divide(new BigDecimal(this.currentPe.getSolutionCount()), 6, RoundingMode.HALF_UP);
board.getLogger().log(Level.INFO, "%s %s %s %s is box 50/50 %f of the time", tiles[0], tiles[1], tiles[2], tiles[3], probability);
if (probability.compareTo(maxProbability) > 0) {
maxProbability = probability;
worst5050 = new Risk5050(info, tiles[0], tiles[1], tiles[2], tiles[3]);
board.getLogger().log(Level.INFO, "%s %s %s %s is box 50/50 is new worst 50/50", tiles[0], tiles[1], tiles[2], tiles[3]);
}
}
}
this.worstBox5050 = worst5050;
this.box5050Safety = BigDecimal.ONE.subtract(maxProbability.multiply(HALF));
}
// returns the location of the 1 tile which can still provide information for a 2x2 box, or null
private Location checkBoxInfo(Location tileTopLeft) {
Location info = null;
final int top = tileTopLeft.y - 1;
final int left = tileTopLeft.x - 1;
if (isPotentialInfo(left, top)) {
if (board.isRevealed(left, top)) {
return null;
} else {
info = new Location(left, top);
}
}
if (isPotentialInfo(left, top - 3)) {
if (board.isRevealed(left, top - 3)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left, top - 3);
}
if (isPotentialInfo(left + 3, top)) {
if (board.isRevealed(left + 3, top)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left + 3, top);
}
if (isPotentialInfo(left + 3, top + 3)) {
if (board.isRevealed(left + 3, top + 3)) { // info is certain
return null;
} else {
if (info != null) { // more than 1 tile giving possible info
return null;
}
}
info = new Location(left + 3, top + 3);
}
return info;
}
// returns whether there information to be had at this location; i.e. on the board and either unrevealed or revealed
private boolean isPotentialInfo(int x, int y) {
if (x < 0 || x >= board.getGameWidth() || y < 0 || y >= board.getGameHeight()) {
return false;
}
if (board.isConfirmedMine(x, y) || isMineInPe(x, y)) {
return false;
} else {
return true;
}
}
private boolean isMineInPe(int x, int y) {
for (Location loc: this.currentPe.getMines()) {
if (loc.x == x && loc.y == y) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,42 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import minesweeper.solver.constructs.Box;
import minesweeper.solver.constructs.CandidateLocation;
import minesweeper.solver.constructs.LinkedLocation;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
abstract public class ProbabilityEngineModel {
abstract public void process();
abstract protected long getDuration();
abstract protected long getIndependentGroups();
abstract Box getBox(Location l);
abstract public BigDecimal getProbability(Location l);
abstract protected List<CandidateLocation> getBestCandidates(BigDecimal freshhold, boolean excludeDead);
abstract protected List<CandidateLocation> getProbableMines(BigDecimal freshhold);
abstract protected List<Location> getFiftyPercenters();
abstract protected BigInteger getSolutionCount();
abstract protected BigDecimal getBestOnEdgeProb();
abstract protected BigDecimal getOffEdgeProb();
abstract protected BigInteger getOffEdgeTally();
abstract protected boolean foundCertainty();
abstract protected Area getDeadLocations();
abstract boolean allDead();
abstract protected int getDeadValueDelta(Location l);
abstract protected List<Location> getMines();
abstract protected List<LinkedLocation> getLinkedLocations();
abstract protected LinkedLocation getLinkedLocation(Location tile);
abstract protected List<BruteForce> getIsolatedEdges();
abstract protected boolean isBestGuessOffEdge();
abstract protected int getLivingClearCount();
abstract protected List<Box> getEmptyBoxes();
abstract protected BigDecimal getBestNotDeadSafety();
}

View File

@ -0,0 +1,535 @@
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.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import minesweeper.gamestate.MoveMethod;
import minesweeper.solver.constructs.Box;
import minesweeper.solver.constructs.CandidateLocation;
import minesweeper.solver.constructs.EvaluatedLocation;
import minesweeper.solver.constructs.LinkedLocation;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Action;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
public class ProgressEvaluator implements LocationEvaluator {
//private final static Comparator<EvaluatedLocation> SORT_ORDER = EvaluatedLocation.SORT_BY_PROGRESS_PROBABILITY; // this works well
private final static Comparator<EvaluatedLocation> SORT_ORDER = EvaluatedLocation.SORT_BY_WEIGHT; // trying this
//private final static Comparator<EvaluatedLocation> SORT_ORDER = EvaluatedLocation.SORT_BY_FIXED_CLEARS_PROGRESS; // trying this
private final static int[][] OFFSETS = {{2, 0}, {-2, 0}, {0, 2}, {0, -2}};
private final static int[][] OFFSETS_ALL = {{2,-2}, {2,-1}, {2,0}, {2,1}, {2,2}, {-2,-2}, {-2,-1}, {-2,0}, {-2,1}, {-2,2}, {-1,2}, {0,2}, {1,2}, {-1,-2}, {0,-2}, {1,-2}};
private final BoardState boardState;
private final WitnessWeb wholeEdge;
private final ProbabilityEngineModel pe;
private final Solver solver;
private final Set<Location> tileOfInterest = new HashSet<>();
private List<EvaluatedLocation> evaluated = new ArrayList<>();
private EvaluatedLocation best;
private boolean certainProgress = false;
public ProgressEvaluator(Solver solver, BoardState boardState, WitnessWeb wholeEdge, ProbabilityEngineModel pe) {
this.boardState = boardState;
this.wholeEdge = wholeEdge;
this.pe = pe;
this.solver = solver;
}
/**
* Look for off edge positions which are good for breaking open new areas
*/
public void evaluateOffEdgeCandidates(List<Location> allUnrevealedSquares) {
//int minesLeft = boardState.getMines() - boardState.getConfirmedFlagCount();
// || allUnrevealedSquares.size() - minesLeft < 6
// if there are only a small number of tiles off the edge then consider them all
if (allUnrevealedSquares.size() - wholeEdge.getSquares().size() < 30) {
for (Location tile: allUnrevealedSquares) {
if (!wholeEdge.isOnWeb(tile)) {
tileOfInterest.add(new CandidateLocation(tile.x, tile.y, pe.getOffEdgeProb(), 0, 0));
}
}
//evaluateLocations(tileOfInterest);
return;
}
int[][] offsets;
if (boardState.isHighDensity()) {
offsets = OFFSETS_ALL;
} else {
offsets = OFFSETS;
}
// look for potential super locations
for (Location tile: wholeEdge.getOriginalWitnesses()) {
//boardState.display(tile.display() + " is an original witness");
for (int[] offset: offsets) {
int x1 = tile.x + offset[0];
int y1 = tile.y + offset[1];
if ( x1 >= 0 && x1 < boardState.getGameWidth() && y1 >= 0 && y1 < boardState.getGameHeight()) {
CandidateLocation loc = new CandidateLocation(x1, y1, pe.getOffEdgeProb(), 0, 0);
if (boardState.isUnrevealed(loc) && !wholeEdge.isOnWeb(loc)) { // if the location is un-revealed and not on the edge
//boardState.display(loc.display() + " is of interest");
tileOfInterest.add(loc);
}
}
}
}
// look for potential off edge squares with not many neighbours and calculate their probability of having no more flags around them
for (Location tile: allUnrevealedSquares) {
int adjMines = boardState.countAdjacentConfirmedFlags(tile);
int adjUnrevealed = boardState.countAdjacentUnrevealed(tile);
if ( adjUnrevealed > 1 && adjUnrevealed < 4 && !wholeEdge.isOnWeb(tile) && !tileOfInterest.contains(tile)) {
tileOfInterest.add(new CandidateLocation(tile.x, tile.y, pe.getOffEdgeProb(), 0, 0));
}
}
//evaluateLocations(tileOfInterest);
}
@Override
public void addLocations(Collection<? extends Location> tiles) {
tileOfInterest.addAll(tiles);
}
/**
* Evaluate a set of tiles to see the expected number of clears it will provide
*/
public void evaluateLocations() {
for (Location tile: tileOfInterest) {
evaluateLocation(tile);
}
}
/**
* Evaluate a tile to see the expected number of clears it will provide
*/
public void evaluateLocation(Location tile) {
//if (best != null & !this.solver.preferences.isExperimentalScoring()) {
// if (tile.getProbability().multiply(Solver.PROGRESS_MULTIPLIER).compareTo(best.getWeighting()) <= 0) {
// boardState.getLogger().log(Level.INFO, "%s is ignored because it can not do better than the best", tile);
// return;
// }
//}
//EvaluatedLocation evalTile = doEvaluateTile(tile);
EvaluatedLocation evalTile = doFullEvaluateTile(tile);
if (evalTile != null) {
if (best == null || evalTile.getWeighting().compareTo(best.getWeighting()) > 0) {
best = evalTile;
}
evaluated.add(evalTile);
}
}
/**
* Evaluate this tile and return its EvaluatedLocation
*/
/*
private EvaluatedLocation doEvaluateTile(Location tile) {
//long nanoStart = System.nanoTime();
//boardState.display(tile.display() + " is of interest as a superset");
EvaluatedLocation result = null;
List<Location> superset = boardState.getAdjacentUnrevealedSquares(tile);
int minesGot = boardState.countAdjacentConfirmedFlags(tile);
//boardState.display("----");
int minMines = minesGot;
int hits = 0;
for (Location loc: boardState.getAdjacentSquaresIterable(tile, 2)) {
if (boardState.isRevealed(loc) && boardState.countAdjacentUnrevealed(loc) != 0) { // if the location is revealed then see if we are a super set of it
boolean supersetOkay = true;
//boolean subSetIncludesMe = false; // does the subset contain the Tile we are considering
for (Location adj: boardState.getAdjacentSquaresIterable(loc)) {
if (boardState.isUnrevealed(adj)) {
boolean found = false;
if (adj.equals(tile)) { // if the subset contains me that's okay
found = true;
//subSetIncludesMe = true;
} else { // otherwise check the superset
for (Location test: superset) {
if (adj.equals(test)) {
found = true;
break;
}
}
}
if (!found) {
supersetOkay = false;
break;
}
}
}
if (supersetOkay) {
int minesNeeded = boardState.getWitnessValue(loc) - boardState.countAdjacentConfirmedFlags(loc);
int value = minesNeeded + minesGot;
//boardState.display(tile.display() + " is a superset of " + loc.display() + " value " + value);
hits++;
if (minMines < value) {
minMines = value;
}
}
}
}
// if we aren't a superset square then just see what the chances that this square is already fully satisfied.
if (hits == 0) {
boardState.display(tile.display() + " is not a superset");
hits = 1;
} else {
boardState.display(tile.display() + " is a superset " + hits + " times");
}
int maxMines = Math.min(minMines + hits - 1, minesGot + superset.size());
BigDecimal probThisTile = pe.getProbability(tile);
LinkedLocation linkedLocation = pe.getLinkedLocation(tile);
int linkedTiles;
if (linkedLocation != null) {
linkedTiles = linkedLocation.getLinksCount();
} else {
linkedTiles = 0;
}
// work out the expected number of clears if we clear here to start with (i.e. ourself + any linked clears)
//BigDecimal expectedClears = BigDecimal.valueOf(1 + linkedTiles).multiply(probThisTile);
//BigDecimal expectedClears = BigDecimal.ZERO;
BigDecimal expectedClears = probThisTile;
//boardState.display(tile.display() + " has " + linkedTiles + " linked tiles");
BigDecimal progressProb = BigDecimal.ZERO;
boolean found = false;
for (int i = minMines; i < maxMines + 1; i++) {
//int clears = solver.validateLocationUsingLocalCheck(tile, i);
//if (clears > 0) {
SolutionCounter counter = solver.validateLocationUsingSolutionCounter(wholeEdge, tile, i, pe.getDeadLocations());
BigInteger sol = counter.getSolutionCount();
int clears = counter.getClearCount();
if (sol.signum() != 0 && clears > linkedTiles) {
//if (sol.signum() != 0) {
found = true;
BigDecimal prob = new BigDecimal(sol).divide(new BigDecimal(pe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
boardState.display(tile.display() + " with value " + i + " has " + clears + " clears with probability " + prob.toPlainString());
// expected clears is the sum of the number of mines cleared * the probability of clearing them
expectedClears = expectedClears.add(BigDecimal.valueOf(clears - linkedTiles).multiply(prob));
progressProb = progressProb.add(prob);
} else {
if (sol.signum() == 0) {
boardState.display(tile.display() + " with value " + i + " with probability zero");
if (!found && i == maxMines && maxMines != 8) { // if we haven't found a possible match yet keep going
maxMines++;
}
} else {
found = true;
boardState.display(tile.display() + " with value " + i + " only has linked clears");
}
}
//} else {
// boardState.display(tile.display() + " with value " + i + " fails local check");
//}
}
//if (linkedTiles > 0) {
// progressProb = probThisTile;
//}
//if (expectedClears.compareTo(BigDecimal.ZERO) > 0) {
result = new EvaluatedLocation(tile.x, tile.y, probThisTile, progressProb, expectedClears, linkedTiles, null, BigDecimal.ZERO, this.solver.preferences.isExperimentalScoring());
if (linkedLocation != null) {
boardState.display("Considering with " + linkedLocation.getLinkedLocations().size() + " linked locations");
top: for (Location link: linkedLocation.getLinkedLocations()) {
boardState.display("Linked with " + link.display());
for (EvaluatedLocation e: evaluated) {
if (e.equals(link)) {
boardState.display("Found link in evaluated" + link.display());
e.merge(result);
result = null;
break top;
}
}
}
}
//}
//long nanoEnd = System.nanoTime();
//boardState.display("Duration = " + (nanoEnd - nanoStart) + " nano-seconds");
return result;
}
*/
/**
* Evaluate this tile and return its EvaluatedLocation
*/
private EvaluatedLocation doFullEvaluateTile(Location tile) {
long nanoStart = System.nanoTime();
//boardState.display(tile.display() + " is of interest as a superset");
EvaluatedLocation result = null;
List<Location> superset = boardState.getAdjacentUnrevealedSquares(tile);
int minesGot = boardState.countAdjacentConfirmedFlags(tile);
//boardState.display("----");
int minMines = minesGot;
int maxMines = minesGot + superset.size();
BigDecimal probThisTile = pe.getProbability(tile);
// work out the expected number of clears if we clear here to start with (i.e. ourself + any linked clears)
//BigDecimal expectedClears = BigDecimal.valueOf(1 + linkedTiles).multiply(probThisTile);
//BigDecimal expectedClears = BigDecimal.ZERO;
//TODO is this correct?
BigDecimal expectedClears = probThisTile;
BigDecimal maxValueProgress = BigDecimal.ZERO;
BigDecimal progressProb = BigDecimal.ZERO;
Area deadLocations = pe.getDeadLocations();
BigDecimal probThisTileLeft = probThisTile;
List<Box> commonClears = null;
for (int i = minMines; i <= maxMines; i++) {
// calculate the weight
BigDecimal bonus = BigDecimal.ONE.add(progressProb.add(probThisTileLeft).multiply(Solver.PROGRESS_VALUE));
BigDecimal weight = probThisTile.multiply(bonus);
// if the remaining safe component for the tile can now never reach the best if if 100% safe for all future values then abandon analysis
if (best != null && weight.compareTo(best.getWeighting()) < 0) {
result = new EvaluatedLocation(tile.x, tile.y, probThisTile, weight, expectedClears, 0, commonClears, maxValueProgress);
result.setPruned();
return result;
}
SolutionCounter counter = solver.validateLocationUsingSolutionCounter(wholeEdge, tile, i, deadLocations);
BigInteger sol = counter.getSolutionCount();
int clears = counter.getLivingClearCount();
// keep track of the maximum probability across all valid values
if (sol.signum() != 0) {
if (commonClears == null) {
commonClears = counter.getEmptyBoxes();
} else {
commonClears = mergeEmptyBoxes(commonClears, counter.getEmptyBoxes());
}
BigDecimal prob = new BigDecimal(sol).divide(new BigDecimal(pe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
maxValueProgress = maxValueProgress.max(prob); // mini-max
//maxValueProgress = maxValueProgress.add(prob.multiply(prob)); // sum of prob^2 = expected solution space left
boardState.getLogger().log(Level.INFO, "%s with value %d has %d living clears with probability %f", tile, i, clears, prob);
// expected clears is the sum of the number of mines cleared * the probability of clearing them
expectedClears = expectedClears.add(BigDecimal.valueOf(clears).multiply(prob));
if (clears != 0) {
progressProb = progressProb.add(prob);
}
// reduce the remaining safe probability
probThisTileLeft = probThisTileLeft.subtract(prob);
} else {
boardState.getLogger().log(Level.INFO, "Tile %s with value %d is not valid", tile, i);
}
}
if (!commonClears.isEmpty()) {
solver.logger.log(Level.DEBUG, "%s has certain progress if survive", tile);
certainProgress = true;
}
// calculate the weight
BigDecimal bonus = BigDecimal.ONE.add(progressProb.multiply(Solver.PROGRESS_VALUE));
BigDecimal weighting = probThisTile.multiply(bonus);
result = new EvaluatedLocation(tile.x, tile.y, probThisTile, weighting, expectedClears, 0, commonClears, maxValueProgress);
long nanoEnd = System.nanoTime();
solver.logger.log(Level.DEBUG, "Duration %d nano-seconds", (nanoEnd - nanoStart));
return result;
}
public void showResults() {
evaluated.sort(SORT_ORDER);
solver.logger.log(Level.INFO, "--- evaluated locations ---");
for (EvaluatedLocation el: evaluated) {
solver.logger.log(Level.INFO, "%s", el);
}
}
private List<Box> mergeEmptyBoxes(List<Box> boxes1, List<Box> boxes2) {
if (boxes1.size() == 0) {
return boxes1;
}
if (boxes2.size() == 0) {
return boxes2;
}
List<Box> result = new ArrayList<>();
for (Box b1: boxes1) {
for (Box b2: boxes2) {
if (b1.equals(b2)) {
result.add(b1);
break;
}
}
}
return result;
}
// find a move which 1) is safer than the move given and 2) when move is safe ==> the alternative is safe
private EvaluatedLocation findAlternativeMove(EvaluatedLocation move) {
if (move.getEmptyBoxes() == null) {
return null;
}
// if one of the common boxes contains a tile which has already been processed then the current tile is redundant
for (EvaluatedLocation eval: evaluated) {
if (eval.getProbability().subtract(move.getProbability()).compareTo(BigDecimal.valueOf(0.001d)) > 0) { // the alternative move is at least a bit safer than the current move
for (Box b: move.getEmptyBoxes()) { // see if the move is in the list of empty boxes
for (Location l: b.getSquares()) {
if (l.equals(eval)) {
return eval;
}
}
}
}
}
return null;
}
@Override
public Action[] bestMove() {
if (evaluated.isEmpty()) {
return new Action[0];
}
// for high density board guess safety and then minimax probability of tile value
if (boardState.isHighDensity() && !certainProgress) {
solver.logger.log(Level.INFO, "High density evaluation being used");
evaluated.sort(EvaluatedLocation.SORT_BY_SAFETY_MINIMAX);
} else {
// other wise weigh safety and progress
evaluated.sort(SORT_ORDER);
}
EvaluatedLocation evalLoc = evaluated.get(0);
// see if this guess has a strictly better guess
if (solver.preferences.isDoDomination()) {
EvaluatedLocation alternative = findAlternativeMove(evalLoc);
if (alternative != null) {
solver.logger.log(Level.INFO, "Replacing %s ...", evalLoc);
solver.logger.log(Level.INFO, "... with %s", alternative);
evalLoc = alternative;
}
}
Action action = new Action(evalLoc, Action.CLEAR, MoveMethod.PROBABILITY_ENGINE, "", evalLoc.getProbability());
// let the boardState decide what to do with this action
boardState.setAction(action);
Action[] result = boardState.getActions().toArray(new Action[0]);
//display("Best Guess: " + action.asString());
return result;
}
@Override
public List<EvaluatedLocation> getEvaluatedLocations() {
return evaluated;
}
}

View File

@ -0,0 +1,741 @@
package minesweeper.solver;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import Asynchronous.Asynchronous;
import Monitor.AsynchMonitor;
import minesweeper.gamestate.GameStateModel;
import minesweeper.gamestate.GameStateModelViewer;
import minesweeper.gamestate.GameStateReader;
import minesweeper.solver.constructs.Box;
import minesweeper.solver.constructs.Square;
import minesweeper.solver.constructs.Witness;
import minesweeper.solver.settings.SettingsFactory;
import minesweeper.solver.settings.SettingsFactory.Setting;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.solver.settings.SolverSettings;
import minesweeper.structure.Action;
import minesweeper.structure.Location;
/**
* This class determines the possible distribution of mines along the edge and the number of ways that can be done
*
*/
public class RolloutGenerator {
private int[][] SMALL_COMBINATIONS = new int[][] {{1}, {1,1}, {1,2,1}, {1,3,3,1}, {1,4,6,4,1}, {1,5,10,10,5,1}, {1,6,15,20,15,6,1}, {1,7,21,35,35,21,7,1}, {1,8,28,56,70,56,28,8,1}};
// used to hold a viable solution
private class ProbabilityLine implements Comparable<ProbabilityLine> {
private int mineCount = 0;
private BigInteger solutionCount = BigInteger.ZERO;
private int[] allocatedMines = new int[boxCount]; // this is the number of mines originally allocate to a box
private int weight; // all lines normalized to sum to 1 million
private ProbabilityLine() {
this(BigInteger.ZERO);
}
private ProbabilityLine(BigInteger solutionCount) {
this.solutionCount = solutionCount;
}
@Override
// sort by the number of mines in the solution
public int compareTo(ProbabilityLine o) {
return this.mineCount - o.mineCount;
}
}
// used to hold what we need to analyse next
private class NextWitness {
private Witness witness;
private List<Box> newBoxes = new ArrayList<>();
private List<Box> oldBoxes = new ArrayList<>();
private NextWitness(Witness w) {
this.witness = w;
for (Box b: w.getBoxes()) {
if (b.isProcessed()) {
oldBoxes.add(b);
} else {
newBoxes.add(b);
}
}
}
}
private long duration;
private List<ProbabilityLine> workingProbs = new ArrayList<>(); // as we work through an independent set of witnesses probabilities are held here
//when set to true indicates that the box has been part of this analysis
private boolean[] mask;
final private BoardState boardState;
final private WitnessWeb web;
final private int boxCount;
final private List<Witness> witnesses;
final private List<Box> boxes;
final private int minesLeft; // number of mines undiscovered in the game
final private int tilesOfEdge; // number of squares undiscovered in the game and off the web
final private List<Location> offWebTiles;
final private List<Location> revealedTiles = new ArrayList<>();
final private List<Location> placedMines = new ArrayList<>();
private int recursions = 0;
// these are the limits that can be on the edge
final private int minTotalMines;
final private int maxTotalMines;
private int totalWeight = 0;
private boolean valid = true;
public RolloutGenerator(BoardState boardState, WitnessWeb web, int squaresLeft, int minesLeft) {
this.boardState = boardState;
this.web = web;
this.minesLeft = minesLeft;
this.tilesOfEdge = squaresLeft - web.getSquares().size();
this.minTotalMines = Math.max(0, minesLeft - this.tilesOfEdge); //we can't use so few mines that we can't fit the remainder elsewhere on the board
this.maxTotalMines = minesLeft; // we can't use more mines than are left in the game
boardState.getLogger().log(Level.DEBUG, "Total mines %d to %d", minTotalMines, maxTotalMines);
web.generateBoxes();
this.witnesses = web.getPrunedWitnesses();
this.boxes = web.getBoxes();
this.boxCount = boxes.size();
for (Witness w: witnesses) {
w.setProcessed(false);
}
for (Box b: boxes) {
b.setProcessed(false);
}
// all tile ...
Set<Location> offWeb = new HashSet<>(boardState.getAllUnrevealedSquares());
// ... minus those on the edge
for (Location tile: web.getSquares()) {
offWeb.remove(tile);
}
offWebTiles = new ArrayList<>(offWeb);
boardState.getLogger().log(Level.INFO, "Total tiles off web %d", offWebTiles.size());
int width = boardState.getGameWidth();
int height = boardState.getGameHeight();
for (int x=0; x < width; x++) {
for (int y=0; y < height; y++) {
if (boardState.isRevealed(x, y)) {
revealedTiles.add(boardState.getLocation(x,y));
}
if (boardState.isConfirmedMine(x,y)) {
placedMines.add(boardState.getLocation(x,y));
}
}
}
boardState.getLogger().log(Level.INFO, "Total tiles revealed %d", revealedTiles.size());
boardState.getLogger().log(Level.INFO, "Total mines placed %d", placedMines.size());
}
/**
* Run the Rollout generator
*/
public void process() {
if (!web.isWebValid()) { // if the web is invalid then nothing we can do
boardState.getLogger().log(Level.INFO, "Web is invalid - exiting the Rollout generator processing");
valid = false;
return;
}
long startTime = System.currentTimeMillis();
// add an empty probability line to get us started
workingProbs.add(new ProbabilityLine(BigInteger.ONE));
// create an empty mask - indicating no boxes have been processed
mask = new boolean[boxCount];
NextWitness witness = findFirstWitness();
while (witness != null) {
// mark the new boxes as processed - which they will be soon
for (Box b: witness.newBoxes) {
mask[b.getUID()] = true;
}
//System.out.println("Processing " + witness.witness.getLocation().display());
workingProbs = mergeProbabilities(witness);
witness = findNextWitness(witness);
}
// have we got a valid position
if (!workingProbs.isEmpty()) {
calculateBoxProbabilities();
} else {
valid = false;
}
duration = System.currentTimeMillis() - startTime;
}
// here we calculate the total number of candidate solutions left in the game
private void calculateBoxProbabilities() {
boardState.getLogger().log(Level.INFO, "showing %d probability Lines...", workingProbs.size());
// total game tally
BigInteger totalTally = BigInteger.ZERO;
// outside a box tally
BigInteger outsideTally = BigInteger.ZERO;
BigInteger hcf = null;
// calculate how many solutions are in each line / Highest common divisor
for (ProbabilityLine pl: workingProbs) {
if (pl.mineCount >= minTotalMines) { // if the mine count for this solution is less than the minimum it can't be valid
BigInteger mult = Solver.combination(minesLeft - pl.mineCount, tilesOfEdge); //# of ways the rest of the board can be formed
pl.solutionCount = pl.solutionCount.multiply(mult);
totalTally = totalTally.add(pl.solutionCount);
if (hcf == null) {
hcf = pl.solutionCount;
} else {
hcf = hcf.gcd(pl.solutionCount);
}
}
}
BigInteger million = BigInteger.valueOf(1000000);
// display the lines with a weight as a part of 1,000,000
for (ProbabilityLine pl: workingProbs) {
if (pl.mineCount >= minTotalMines) { // if the mine count for this solution is less than the minimum it can't be valid
pl.weight = pl.solutionCount.multiply(million).divide(totalTally).intValue();
totalWeight = totalWeight + pl.weight;
String display = "Mines=" + pl.mineCount + " Weight=" + pl.weight;
for (int i=0; i < pl.allocatedMines.length; i++) {
display = display + " " + boxes.get(i).getSquares().size() + "(" + pl.allocatedMines[i] + ") ";
}
boardState.getLogger().log(Level.INFO, display);
}
}
}
private List<ProbabilityLine> mergeProbabilities(NextWitness nw) {
List<ProbabilityLine> newProbs = new ArrayList<>();
for (ProbabilityLine pl: workingProbs) {
int missingMines = nw.witness.getMines() - countPlacedMines(pl, nw);
if (missingMines < 0) {
// too many mines placed around this witness previously, so this probability can't be valid
} else if (missingMines == 0) {
newProbs.add(pl); // witness already exactly satisfied, so nothing to do
} else if (nw.newBoxes.isEmpty()) {
// nowhere to put the new mines, so this probability can't be valid
} else {
newProbs.addAll(distributeMissingMines(pl, nw, missingMines, 0));
}
}
//solver.display("Processed witness " + nw.witness.display());
// flag the last set of details as processed
nw.witness.setProcessed(true);
for (Box b: nw.newBoxes) {
b.setProcessed(true);
}
return newProbs;
}
// this is used to recursively place the missing Mines into the available boxes for the probability line
private List<ProbabilityLine> distributeMissingMines(ProbabilityLine pl, NextWitness nw, int missingMines, int index) {
recursions++;
if (recursions % 10000 == 0) {
boardState.getLogger().log(Level.WARN, "Probability Engine recursion exceeding %d iterations", recursions);
}
List<ProbabilityLine> result = new ArrayList<>();
// if there is only one box left to put the missing mines we have reach this end of this branch of recursion
if (nw.newBoxes.size() - index == 1) {
// if there are too many for this box then the probability can't be valid
if (nw.newBoxes.get(index).getMaxMines() < missingMines) {
return result;
}
// if there are too few for this box then the probability can't be valid
if (nw.newBoxes.get(index).getMinMines() > missingMines) {
return result;
}
// if there are too many for this game then the probability can't be valid
if (pl.mineCount + missingMines > maxTotalMines) {
return result;
}
// otherwise place the mines in the probability line
result.add(extendProbabilityLine(pl, nw.newBoxes.get(index), missingMines));
return result;
}
// this is the recursion
int maxToPlace = Math.min(nw.newBoxes.get(index).getMaxMines(), missingMines);
for (int i=nw.newBoxes.get(index).getMinMines(); i <= maxToPlace; i++) {
ProbabilityLine npl = extendProbabilityLine(pl, nw.newBoxes.get(index), i);
result.addAll(distributeMissingMines(npl, nw, missingMines - i, index + 1));
}
return result;
}
// create a new probability line by taking the old and adding the mines to the new Box
private ProbabilityLine extendProbabilityLine(ProbabilityLine pl, Box newBox, int mines) {
int combination = SMALL_COMBINATIONS[newBox.getSquares().size()][mines];
BigInteger newSolutionCount = pl.solutionCount.multiply(BigInteger.valueOf(combination));
ProbabilityLine result = new ProbabilityLine(newSolutionCount);
result.mineCount = pl.mineCount + mines;
result.allocatedMines = pl.allocatedMines.clone();
result.allocatedMines[newBox.getUID()] = mines;
return result;
}
// counts the number of mines already placed
private int countPlacedMines(ProbabilityLine pl, NextWitness nw) {
int result = 0;
for (Box b: nw.oldBoxes) {
result = result + pl.allocatedMines[b.getUID()];
}
return result;
}
// return any witness which hasn't been processed
private NextWitness findFirstWitness() {
for (Witness w: witnesses) {
if (!w.isProcessed()) {
return new NextWitness(w);
}
}
// if we are here all witness have been processed
return null;
}
// look for the next witness to process
private NextWitness findNextWitness(NextWitness prevWitness) {
int bestTodo = 99999;
Witness bestWitness = null;
// and find a witness which is on the boundary of what has already been processed
for (Box b: boxes) {
if (b.isProcessed()) {
for (Witness w: b.getWitnesses()) {
if (!w.isProcessed()) {
int todo = 0;
for (Box b1: w.getBoxes()) {
if (!b1.isProcessed()) {
todo++;
}
}
if (todo == 0) {
return new NextWitness(w);
} else if (todo < bestTodo) {
bestTodo = todo;
bestWitness = w;
}
}
}
}
}
if (bestWitness != null) {
return new NextWitness(bestWitness);
}
// if we are down here then there is no witness which is on the boundary, so we have processed a complete set of independent witnesses
// get an unprocessed witness
NextWitness nw = findFirstWitness();
// return the next witness to process
return nw;
}
/**
* The duration to do the processing in milliseconds
* @return
*/
protected long getDuration() {
return this.duration;
}
public boolean isValid() {
return valid;
}
public int getWidth() {
return boardState.getGameWidth();
}
public int getHeight() {
return boardState.getGameHeight();
}
public synchronized GameStateModelViewer generateGame(long seed) {
return generateGame(seed, null);
}
public synchronized GameStateModelViewer generateGame(long seed, Location safeTile) {
GameStateModelViewer result;
int width = boardState.getGameWidth();
int height = boardState.getGameHeight();
int mineCount = this.minesLeft;
Random rng = new Random(seed);
int edge = (int) (rng.nextDouble()*totalWeight);
//boardState.display("Random number is " + edge);
int soFar = 0;
ProbabilityLine line = null;
for (ProbabilityLine pl: workingProbs) {
soFar = soFar + pl.weight;
if (soFar > edge) {
line = pl;
break;
}
}
mineCount = mineCount - line.mineCount;
List<Location> mines = new ArrayList<>(placedMines); // start with the mines we have already placed
for (int i=0; i < line.allocatedMines.length; i++) {
if (line.allocatedMines[i] == 0) { // if no mines here nothing to do
} else if (line.allocatedMines[i] == boxes.get(i).getSquares().size()) { // if the box is full of mines then all tile in the box are mines
for (Square tile: boxes.get(i).getSquares()) {
mines.add(tile);
}
} else { // shuffle the tiles in the box and take the first ones as the mines
// in order to make this repeatable with the same seed, we can't shuffle the underlying data. So create a copy.
List<Location> boxTiles = new ArrayList<>(boxes.get(i).getSquares());
Collections.shuffle(boxTiles, rng);
int toGet = line.allocatedMines[i];
for (int j=0; j < toGet; j++) {
if (safeTile == null || !boxTiles.get(j).equals(safeTile)) { // don't place a mine in the safe tile
mines.add(boxTiles.get(j));
} else {
toGet++; // if this mine is no good then we need to look for an extra one
}
}
}
}
// in order to make this repeatable with the same seed, we can't shuffle the underlying data. So create a copy.
List<Location> owt = new ArrayList<>(offWebTiles);
Collections.shuffle(owt, rng);
int toGet = mineCount;
for (int j=0; j < toGet; j++) {
if (safeTile == null || !owt.get(j).equals(safeTile)) { // don't place a mine in the safe tile
mines.add(owt.get(j));
} else {
toGet++; // if this mine is no good then we need to look for an extra one
}
}
if (mines.size() != this.minesLeft) {
System.out.println("Logic error: Mines generated " + mines.size() + " does not equal mines left " + this.minesLeft);
}
result = GameStateReader.loadMines(width, height, mines, revealedTiles);
/*
// show the board
for (int y=0; y < height; y++) {
for (int x=0; x < width; x++) {
int tile = result.privilegedQuery(new Location(x,y) , true);
if (tile == GameStateModel.MINE) {
System.out.print("M");
} else if (tile == GameStateModel.HIDDEN) {
System.out.print(".");
} else {
System.out.print(tile);
}
}
System.out.println();
}
*/
return result;
}
public class Adversarial<T> implements Comparable<Adversarial<T>> {
public final T original;
public int wins;
public int played;
private Adversarial(T original) {
this.original = original;
}
@Override
public int compareTo(Adversarial<T> o) {
return o.wins - this.wins;
}
}
public class RolloutWork implements Asynchronous<Boolean> {
private final Adversarial<? extends Location> player;
private final int plays;
public RolloutWork(Adversarial<? extends Location> player, int plays) {
this.player = player;
this.plays = plays;
}
@Override
public void start() {
int wins = playGames(player.original, plays);
player.wins = player.wins + wins;
player.played = player.played + plays;
}
@Override
public void requestStop() {
// TODO Auto-generated method stub
}
@Override
public Boolean getResult() {
return true;
}
}
public <T extends Location> List<Adversarial<T>> adversarial(List<T> candidates) {
List<Adversarial<T>> players = new ArrayList<>();
for (T candidate: candidates) {
players.add(new Adversarial<T>(candidate));
}
int check = players.size();
final int plays = 200;
while (check > 1) {
RolloutWork[] workers = new RolloutWork[check];
for (int i=0; i < check; i++) {
Adversarial<T> player = players.get(i);
workers[i] = new RolloutWork(player, plays);
}
AsynchMonitor monitor = new AsynchMonitor(workers);
monitor.setMaxThreads(6);
try {
monitor.startAndWait();
} catch (Exception e) {
e.printStackTrace();
}
Collections.sort(players);
if (check > 4) {
check = check * 3 / 4;
} else {
check--;
}
}
for (Adversarial<T> player: players) {
boardState.getLogger().log(Level.INFO, "%s had %d wins out of %d", player.original, player.wins, player.played);
}
return players;
}
private int playGames(Location startLocation, int count) {
int wins = 0;
Random seeder = new Random();
SolverSettings preferences = SettingsFactory.GetSettings(Setting.TINY_ANALYSIS).setTieBreak(false);
int steps = 0;
int maxSteps = count;
while (steps < maxSteps) {
steps++;
GameStateModel gs = generateGame(seeder.nextLong());
Solver solver = new Solver(gs, preferences, false);
gs.doAction(new Action(startLocation, Action.CLEAR));
int state = gs.getGameState();
boolean win;
if (state == GameStateModel.LOST || state == GameStateModel.WON) { // if we have won or lost on the first move nothing more to do
win = (state == GameStateModel.WON);
} else { // otherwise use the solver to play the game
win = playGame(gs, solver);
}
if (win) {
wins++;
}
}
return wins;
}
private boolean playGame(GameStateModel gs, Solver solver) {
int state;
play: while (true) {
Action[] moves;
try {
solver.start();
moves = solver.getResult();
} catch (Exception e) {
System.out.println("Game " + gs.showGameKey() + " has thrown an exception! ");
e.printStackTrace();
return false;
}
if (moves.length == 0) {
System.err.println("No moves returned by the solver for game " + gs.showGameKey());
return false;
}
// play all the moves until all done, or the game is won or lost
for (int i=0; i < moves.length; i++) {
gs.doAction(moves[i]);
state = gs.getGameState();
if (state == GameStateModel.LOST || state == GameStateModel.WON) {
break play;
}
}
}
if (state == GameStateModel.LOST) {
return false;
} else {
return true;
}
}
@Override
public String toString() {
return this.getWidth() + "x" + this.getHeight() + "/" + (this.minesLeft + this.placedMines.size());
}
}

View File

@ -0,0 +1,635 @@
package minesweeper.solver;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import minesweeper.gamestate.MoveMethod;
import minesweeper.solver.Solver.RunPeResult;
import minesweeper.solver.constructs.Box;
import minesweeper.solver.constructs.CandidateLocation;
import minesweeper.solver.constructs.EvaluatedLocation;
import minesweeper.solver.utility.Logger;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Action;
import minesweeper.structure.Area;
import minesweeper.structure.Location;
public class SecondarySafetyEvaluator implements LocationEvaluator {
//private final static BigDecimal PROGRESS_CONTRIBUTION = new BigDecimal("0.1"); // was 0.1
private final static BigDecimal EQUALITY_THRESHOLD = new BigDecimal("0.0001");
private final static BigDecimal FIFTYFIFTY_SCALE = new BigDecimal("0.9");
private final static Comparator<EvaluatedLocation> SORT_ORDER = EvaluatedLocation.SORT_BY_WEIGHT;
private final static int[][] OFFSETS = {{2, 0}, {-2, 0}, {0, 2}, {0, -2}};
private final static int[][] OFFSETS_ALL = {{2,-2}, {2,-1}, {2,0}, {2,1}, {2,2}, {-2,-2}, {-2,-1}, {-2,0}, {-2,1}, {-2,2}, {-1,2}, {0,2}, {1,2}, {-1,-2}, {0,-2}, {1,-2}};
private final BoardState boardState;
private final WitnessWeb wholeEdge;
private final ProbabilityEngineModel pe;
private final Solver solver;
private final BruteForceAnalysisModel incompleteBFA;
//private final FiftyFiftyHelper fiftyFiftyHelper;
private final Set<Location> tileOfInterestOff = new HashSet<>();
private final Set<Location> tileOfInterestOn = new LinkedHashSet<>();
//private final List<Location> tileOfInterestOn = new ArrayList<>();
//private final LongTermRiskHelperOld ltrHelperOld;
private final SpaceCounter spaceCounter;
private final LongTermRiskHelper ltrHelper;
private final BigDecimal progressContribution;
private final static BigDecimal essrContribution = new BigDecimal("0.02");
private List<EvaluatedLocation> evaluated = new ArrayList<>();
private EvaluatedLocation best;
private boolean certainProgress = false;
public SecondarySafetyEvaluator(Solver solver, BoardState boardState, WitnessWeb wholeEdge, ProbabilityEngineModel pe, BruteForceAnalysisModel incompleteBFA, LongTermRiskHelper ltr) {
this.boardState = boardState;
this.wholeEdge = wholeEdge;
this.pe = pe;
this.solver = solver;
this.incompleteBFA = incompleteBFA;
this.progressContribution = solver.preferences.getProgressContribution();
//this.fiftyFiftyHelper = fiftyFiftyHelper;
// look for long term risks and then use this to compare what impact the short term risks have on them
//this.ltrHelperOld = new LongTermRiskHelperOld(boardState, wholeEdge, pe);
//this.ltrHelperOld.findRisks();
// find major 50/50 influence on the board - wip
this.ltrHelper = ltr;
this.spaceCounter = new SpaceCounter(boardState, 8);
}
/**
* Look for off edge positions which are good for breaking open new areas
*/
public void evaluateOffEdgeCandidates(List<Location> allUnrevealedSquares) {
//int minesLeft = boardState.getMines() - boardState.getConfirmedFlagCount();
// || allUnrevealedSquares.size() - minesLeft < 6
// if there are only a small number of tiles off the edge then consider them all
if (allUnrevealedSquares.size() - wholeEdge.getSquares().size() < 30) {
for (Location tile: allUnrevealedSquares) {
if (!wholeEdge.isOnWeb(tile)) {
tileOfInterestOff.add(new CandidateLocation(tile.x, tile.y, pe.getOffEdgeProb(), 0, 0));
}
}
//evaluateLocations(tileOfInterest);
return;
}
int[][] offsets;
if (boardState.isHighDensity()) {
offsets = OFFSETS_ALL;
} else {
offsets = OFFSETS;
}
// look for potential super locations
for (Location tile: wholeEdge.getOriginalWitnesses()) {
//boardState.display(tile.display() + " is an original witness");
for (int[] offset: offsets) {
int x1 = tile.x + offset[0];
int y1 = tile.y + offset[1];
if ( x1 >= 0 && x1 < boardState.getGameWidth() && y1 >= 0 && y1 < boardState.getGameHeight()) {
CandidateLocation loc = new CandidateLocation(x1, y1, pe.getOffEdgeProb(), boardState.countAdjacentUnrevealed(x1, y1), boardState.countAdjacentConfirmedFlags(x1, y1));
if (boardState.isUnrevealed(loc) && !wholeEdge.isOnWeb(loc)) { // if the location is un-revealed and not on the edge
//boardState.display(loc.display() + " is of interest");
tileOfInterestOff.add(loc);
}
}
}
}
// look for potential off edge squares with not many neighbours and calculate their probability of having no more flags around them
for (Location tile: allUnrevealedSquares) {
int adjMines = boardState.countAdjacentConfirmedFlags(tile);
int adjUnrevealed = boardState.countAdjacentUnrevealed(tile);
if ( adjUnrevealed > 1 && adjUnrevealed < 4 && !wholeEdge.isOnWeb(tile) && !tileOfInterestOff.contains(tile)) {
tileOfInterestOff.add(new CandidateLocation(tile.x, tile.y, pe.getOffEdgeProb(), boardState.countAdjacentUnrevealed(tile), boardState.countAdjacentConfirmedFlags(tile)));
}
}
}
@Override
public void addLocations(Collection<? extends Location> tiles) {
tileOfInterestOn.addAll(tiles);
}
/**
* Evaluate a set of tiles to see the expected number of clears it will provide
*/
public void evaluateLocations() {
BigDecimal threshold = pe.getBestNotDeadSafety().multiply(Solver.PROB_ENGINE_HARD_TOLERENCE);
for (Location loc: ltrHelper.getInfluencedTiles(threshold)) {
if (!tileOfInterestOff.contains(loc)) { // if we aren't in the other set then add it to this one
tileOfInterestOn.add(loc);
}
}
/*
for (Location loc: ltrHelperOld.get5050Breakers()) {
if (!tileOfInterestOff.contains(loc)) { // if we aren't in the other set then add it to this one
tileOfInterestOn.add(loc);
}
}
*/
List<Location> defered = new ArrayList<>();
List<Location> notDefered = new ArrayList<>();
for (Location tile: tileOfInterestOn) {
if (this.spaceCounter.meetsThreshold(tile)) {
notDefered.add(tile);
} else {
defered.add(tile);
}
}
for (Location tile: tileOfInterestOff) {
if (this.spaceCounter.meetsThreshold(tile)) {
notDefered.add(tile);
} else {
defered.add(tile);
}
}
if (!notDefered.isEmpty()) {
for (Location tile: notDefered) {
evaluateLocation(tile);
}
} else {
for (Location tile: defered) {
evaluateLocation(tile);
}
}
}
/**
* Evaluate a tile to see the expected number of clears it will provide
*/
private void evaluateLocation(Location tile) {
EvaluatedLocation evalTile = doFullEvaluateTile(tile);
if (evalTile != null) {
if (best == null || evalTile.getWeighting().compareTo(best.getWeighting()) > 0) {
best = evalTile;
}
evaluated.add(evalTile);
}
}
private EvaluatedLocation doFullEvaluateTile(Location tile) {
// find how many common tiles
SolutionCounter counter1 = solver.validatePosition(wholeEdge, Collections.emptyList(), Arrays.asList(tile), Area.EMPTY_AREA);
int linkedTilesCount = 0;
boolean dominated = false;
boolean linked = false;
for (Box box: counter1.getEmptyBoxes()) {
if (box.contains(tile)) { // if the box contains the tile to be processed then ignore it
} else {
if (box.getSquares().size() > 1) {
dominated = true;
linkedTilesCount = linkedTilesCount + box.getSquares().size();
} else {
linked = true;
linkedTilesCount++;
}
}
}
solver.logger.log(Level.INFO, "%s has %d linked tiles and dominated=%b", tile, linkedTilesCount, dominated);
EvaluatedLocation result;
if (dominated) {
BigDecimal probThisTile = pe.getProbability(tile); // this is both the safety, secondary safety and progress probability.
BigDecimal bonus = BigDecimal.ONE.add(probThisTile.multiply(this.progressContribution));
BigDecimal weight = probThisTile.multiply(bonus);
BigDecimal expectedClears = BigDecimal.valueOf(counter1.getLivingClearCount()); // this isn't true, but better than nothing?
result = new EvaluatedLocation(tile.x, tile.y, probThisTile , weight, expectedClears, 0, counter1.getEmptyBoxes(), probThisTile);
} else {
result = doFullEvaluateTile(tile, linkedTilesCount);
}
//result = doFullEvaluateTile(tile, 0);
return result;
}
/**
* Evaluate this tile and return its EvaluatedLocation
*/
private EvaluatedLocation doFullEvaluateTile(Location tile, int linkedTilesCount) {
long nanoStart = System.nanoTime();
EvaluatedLocation result = null;
List<Location> superset = boardState.getAdjacentUnrevealedSquares(tile);
int minesGot = boardState.countAdjacentConfirmedFlags(tile);
int minMines = minesGot;
int maxMines = minesGot + superset.size();
Box tileBox = pe.getBox(tile);
BigInteger safetyTally;
int tilesOnEdge;
BigDecimal safetyThisTile;
if (tileBox == null) {
safetyThisTile = pe.getOffEdgeProb();
tilesOnEdge = 1;
safetyTally = pe.getSolutionCount().subtract(pe.getOffEdgeTally()); //number of solutions this tile is safe
} else {
safetyThisTile = tileBox.getSafety();
tilesOnEdge = tileBox.getEdgeLength();
safetyTally = pe.getSolutionCount().subtract(tileBox.getTally()); //number of solutions this tile is safe
}
BigDecimal fiftyFiftyInfluence;
if (this.solver.preferences.considerLongTermSafety()) {
BigDecimal tally = new BigDecimal(ltrHelper.findInfluence(tile)).multiply(FIFTYFIFTY_SCALE);
fiftyFiftyInfluence = new BigDecimal(safetyTally).add(tally).divide(new BigDecimal(safetyTally), Solver.DP, RoundingMode.HALF_UP);
} else {
fiftyFiftyInfluence = BigDecimal.ONE;
}
// work out the expected number of clears if we clear here to start with (i.e. ourself + any linked clears)
BigDecimal expectedClears = BigDecimal.ZERO;
BigDecimal maxValueProgress = BigDecimal.ZERO;
BigDecimal secondarySafety = BigDecimal.ZERO;
BigDecimal progressProb = BigDecimal.ZERO;
BigDecimal ess = BigDecimal.ONE.subtract(safetyThisTile); // expect solution space = p(mine) + sum[ P(n)*p(n) ]
BigDecimal safetyThisTileLeft = safetyThisTile;
int validValues = 0;
List<Box> commonClears = null;
for (int i = minMines; i <= maxMines; i++) {
// calculate the weight
BigDecimal bonus = BigDecimal.ONE.add(progressProb.add(safetyThisTileLeft).multiply(this.progressContribution));
//BigDecimal weight = secondarySafety.add(safetyThisTileLeft).multiply(bonus).multiply(fiftyFiftyInfluence);
BigDecimal weight = secondarySafety.add(safetyThisTileLeft.multiply(fiftyFiftyInfluence)).multiply(bonus);
// if the remaining safe component for the tile can now never reach the best if 100% safe for all future values then abandon analysis
if (best != null && weight.compareTo(best.getWeighting()) < 0) {
result = new EvaluatedLocation(tile.x, tile.y, safetyThisTile, weight, expectedClears, 0, commonClears, maxValueProgress);
result.setPruned();
return result;
}
RunPeResult peResult = solver.runProbabilityEngine(wholeEdge, tile, i);
ProbabilityEngineModel counter = peResult.pe;
BigInteger sol = counter.getSolutionCount();
int clears = counter.getLivingClearCount();
// keep track of the maximum probability across all valid values
if (sol.signum() != 0) {
validValues++;
if (commonClears == null) {
commonClears = counter.getEmptyBoxes();
} else {
commonClears = mergeEmptyBoxes(commonClears, counter.getEmptyBoxes());
}
BigDecimal prob = new BigDecimal(sol).divide(new BigDecimal(pe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
maxValueProgress = maxValueProgress.max(prob); // mini-max
ess = ess.add(prob.multiply(prob)); // sum of p^2
// expected clears is the sum of the number of mines cleared * the probability of clearing them
expectedClears = expectedClears.add(BigDecimal.valueOf(clears).multiply(prob));
BigDecimal nextMoveSafety = counter.getBestNotDeadSafety();
//BigDecimal lts = this.ltrHelperOld.getLongTermSafety(tile, counter);
//BigDecimal lts = BigDecimal.ONE;
solver.logger.log(Level.INFO, "%s with value %d has %d living clears with probability %f, secondary safety %f, 50/50 influence %f and %d tiles on edge", tile, i, clears, prob, nextMoveSafety, fiftyFiftyInfluence, tilesOnEdge);
secondarySafety = secondarySafety.add(prob.multiply(nextMoveSafety).multiply(fiftyFiftyInfluence));
if (clears > linkedTilesCount) {
//BigDecimal modProb;
//if (clears == linkedTilesCount + 1) {
// modProb = prob.multiply(BigDecimal.valueOf(0.8));
//} else {
// modProb = prob;
//}
//modProb = prob.multiply(BigDecimal.valueOf(clears - linkedTilesCount));
progressProb = progressProb.add(prob);
}
// reduce the remaining safe probability
safetyThisTileLeft = safetyThisTileLeft.subtract(prob);
} else {
solver.logger.log(Level.DEBUG, "%s with value %d is not valid", tile, i);
}
}
if (commonClears != null && !commonClears.isEmpty()) {
solver.logger.log(Level.DEBUG, "%s has certain progress if survive", tile);
certainProgress = true;
}
// expected solution space reduction
BigDecimal essr = BigDecimal.ONE.subtract(ess);
// calculate the weight
BigDecimal bonus = BigDecimal.ONE.add(progressProb.multiply(this.progressContribution));
//bonus = bonus.add(essr.multiply(this.essrContribution));
BigDecimal weight = secondarySafety.multiply(bonus);
result = new EvaluatedLocation(tile.x, tile.y, safetyThisTile, weight, expectedClears, 0, commonClears, maxValueProgress);
// if the tile is dead then relegate it to a deferred guess
if (validValues == 1) {
solver.logger.log(Level.INFO, "%s is discovered to be dead during secondary safety analysis", tile);
result.setDeferGuessing(true);
}
if (!this.spaceCounter.meetsThreshold(result)) {
result.setDeferGuessing(true);
}
long nanoEnd = System.nanoTime();
solver.logger.log(Level.DEBUG, "Duration %d nano-seconds", (nanoEnd - nanoStart));
return result;
}
/**
* recursively calculate a tile's safety to the required depth
*/
private BigDecimal calculateSafety(Location tile, WitnessWeb currWeb, ProbabilityEngineModel currPe, int depth) {
int minMines = boardState.countAdjacentConfirmedFlags(tile);
int maxMines = minMines + boardState.countAdjacentUnrevealed(tile);
// work out the expected number of clears if we clear here to start with (i.e. ourself + any linked clears)
BigDecimal secondarySafety = BigDecimal.ZERO;
for (int value = minMines; value <= maxMines; value++) {
// make the move
boardState.setWitnessValue(tile, value);
// create a new list of witnesses
List<Location> witnesses = new ArrayList<>(currWeb.getPrunedWitnesses().size() + 1);
witnesses.addAll(currWeb.getPrunedWitnesses());
witnesses.add(tile);
Area witnessed = boardState.getUnrevealedArea(witnesses);
WitnessWeb newWeb = 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 = boardState.getMines() - boardState.getConfirmedMineCount();
ProbabilityEngineModel counter = new ProbabilityEngineFast(boardState, newWeb, unrevealed, minesLeft);
counter.process();
BigInteger sol = counter.getSolutionCount();
int clears = counter.getLivingClearCount();
// keep track of the maximum probability across all valid values
if (sol.signum() != 0) {
BigDecimal prob = new BigDecimal(sol).divide(new BigDecimal(currPe.getSolutionCount()), Solver.DP, RoundingMode.HALF_UP);
List<CandidateLocation> bestCandidates = counter.getBestCandidates(BigDecimal.ONE, true);
BigDecimal safety;
if (bestCandidates.size() == 0 ) {
safety = counter.getOffEdgeProb();
} else {
if (depth == 1) {
safety = bestCandidates.get(0).getProbability();
} else {
safety = calculateSafety(bestCandidates.get(0), newWeb, counter, depth - 1);
}
}
solver.logger.log(Level.INFO, "%s with value %d has %d living clears with probability %f and secondary safety %f", tile, value, clears, prob, safety);
secondarySafety = secondarySafety.add(prob.multiply(safety));
} else {
solver.logger.log(Level.DEBUG, "%s with value %d is not valid", tile, value);
}
// undo the move
boardState.clearWitness(tile);
}
return secondarySafety;
}
public void showResults() {
evaluated.sort(SORT_ORDER);
solver.logger.log(Level.INFO, "--- evaluated locations ---");
for (EvaluatedLocation el: evaluated) {
solver.logger.log(Level.INFO, "%s", el);
}
}
private List<Box> mergeEmptyBoxes(List<Box> boxes1, List<Box> boxes2) {
if (boxes1.size() == 0) {
return boxes1;
}
if (boxes2.size() == 0) {
return boxes2;
}
List<Box> result = new ArrayList<>();
for (Box b1: boxes1) {
for (Box b2: boxes2) {
if (b1.equals(b2)) {
result.add(b1);
break;
}
}
}
return result;
}
// find a move which 1) is safer than the move given and 2) when move is safe ==> the alternative is safe
private EvaluatedLocation findAlternativeMove(EvaluatedLocation move) {
if (move.getEmptyBoxes() == null) {
return null;
}
// if one of the common boxes contains a tile which has already been processed then the current tile is redundant
for (EvaluatedLocation eval: evaluated) {
if (eval.getProbability().subtract(move.getProbability()).compareTo(EQUALITY_THRESHOLD) > 0) { // the alternative move is at least a bit safer than the current move
for (Box b: move.getEmptyBoxes()) { // see if the move is in the list of empty boxes
for (Location l: b.getSquares()) {
if (l.equals(eval)) {
return eval;
}
}
}
}
}
return null;
}
@Override
public Action[] bestMove() {
if (evaluated.isEmpty()) {
return new Action[0];
}
// for high density board guess safety and then minimax probability of tile value
if (boardState.isHighDensity() && !certainProgress) {
solver.logger.log(Level.INFO, "High density evaluation being used");
evaluated.sort(EvaluatedLocation.SORT_BY_SAFETY_MINIMAX);
} else {
// other wise weigh safety and progress
evaluated.sort(SORT_ORDER);
}
EvaluatedLocation evalLoc = evaluated.get(0);
// see if this guess has a strictly better guess
if (solver.preferences.isDoDomination()) {
EvaluatedLocation alternative = findAlternativeMove(evalLoc);
if (alternative != null) {
solver.logger.log(Level.INFO, "Replacing %s ...", evalLoc);
solver.logger.log(Level.INFO, "... with %s", alternative);
evalLoc = alternative;
}
}
// check whether the chosen move is dominated by a partially complete BFDA
if (incompleteBFA != null) {
Location better = incompleteBFA.checkForBetterMove(evalLoc);
if (better != null) {
EvaluatedLocation bfdaBetter = null;
for (EvaluatedLocation evl: evaluated) {
if (evl.equals(better)) {
bfdaBetter = evl;
break;
}
}
if (bfdaBetter == null) {
solver.logger.log(Level.INFO, "Unable to find %s in the Evaluated list", better);
} else {
evalLoc = bfdaBetter;
solver.logger.log(Level.INFO, "Tile %s", evalLoc);
}
}
}
Action action = new Action(evalLoc, Action.CLEAR, MoveMethod.PROBABILITY_ENGINE, "", evalLoc.getProbability());
// let the boardState decide what to do with this action
boardState.setAction(action);
Action[] result = boardState.getActions().toArray(new Action[0]);
//display("Best Guess: " + action.asString());
return result;
}
@Override
public List<EvaluatedLocation> getEvaluatedLocations() {
return evaluated;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,126 @@
package minesweeper.solver;
import java.util.ArrayList;
import java.util.List;
import minesweeper.structure.Location;
public class SpaceCounter {
protected final static int[] DX = {0, 1, 1, 1, 0, -1, -1, -1};
protected final static int[] DY = {-1, -1, 0, 1, 1, 1, 0, -1};
private final BoardState board;
private final int threshold;
private final byte[][] data;
private final int width;
private final int height;
public SpaceCounter(BoardState board, int threshold) {
this.board = board;
this.threshold = threshold; // an area is considered large if greater than or equal to the threshold
this.height = this.board.getGameHeight();
this.width = this.board.getGameWidth();
this.data = new byte[this.width][this.height];
}
public boolean meetsThreshold(Location loc) {
boolean large = false;
if (data[loc.x][loc.y] != 0) {
large = (data[loc.x][loc.y] == 1);
} else {
int index = 0;
List<Location> tiles = new ArrayList<>();
tiles.add(loc);
data[loc.x][loc.y] = -1;
top: while (tiles.size() != index) {
Location m = tiles.get(index);
for (int j=0; j < DX.length; j++) {
final int x1 = m.x + DX[j];
final int y1 = m.y + DY[j];
if (x1 >= 0 && x1 < this.width && y1 >= 0 && y1 < this.height) {
if (this.board.isUnrevealed(x1, y1)) {
if (data[x1][y1] == 0) { // unprocessed tile
data[x1][y1] = -1;
tiles.add(this.board.getLocation(x1, y1));
} else if (data[x1][y1] == 1) { // if we meet a tile which is already in a large area, we are in a large area
large = true;
break top;
} else { // something has gone wrong since we can't encounter a small area
//this.board.getLogger().log(Level.ERROR, "Space counter encountered an area below threshold");
}
} else if (this.board.isRevealed(x1, y1)) { // if he board is revealed then see if we can hop over the tile into more open space
for (int k=0; k < DX.length; k++) {
final int x2 = x1 + DX[k];
final int y2 = y1 + DY[k];
if (x2 >= 0 && x2 < this.width && y2 >= 0 && y2 < this.height) {
if (this.board.isUnrevealed(x2, y2)) {
if (data[x2][y2] == 0) { // unprocessed tile
data[x2][y2] = -1;
tiles.add(this.board.getLocation(x2, y2));
} else if (data[x2][y2] == 1) { // if we meet a tile which is already in a large area, we are in a large area
large = true;
break top;
}
}
}
}
}
}
}
if (tiles.size() >= this.threshold) {
large = true;
break;
}
index++;
}
// record the area if it exceeds the threshold
if (large) {
for (Location l: tiles) {
data[l.x][l.y] = 1;
}
}
}
if (large) {
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,373 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import minesweeper.solver.constructs.Box;
import minesweeper.solver.constructs.Square;
import minesweeper.solver.constructs.Witness;
import minesweeper.solver.utility.Logger;
import minesweeper.solver.utility.Logger.Level;
import minesweeper.structure.Location;
/**
* A witness web is a construct which holds the connectivity information of the game.
*/
public class WitnessWeb {
final private List<Witness> prunedWitnesses = new ArrayList<>(); // the de-duplicated witnesses
final private List<? extends Location> originalWitnesses; // the witnesses passed into this web
final private List<Square> squares = new ArrayList<>();
final private List<Box> boxes = new ArrayList<>();
final private List<Witness> independentWitness = new ArrayList<>();
private final Logger logger;
private int independentMines;
private BigInteger independentIterations = BigInteger.ONE;
private int remainingSquares;
private BoardState boardState;
private int pruned = 0;
private int webNum = 0;
private boolean validWeb = true; // if the web contains contradictions it is invalid
private List<CrunchResult> solutions = new ArrayList<>();
public WitnessWeb(BoardState boardState, List<? extends Location> allWit, Collection<Location> allSqu) {
this(boardState, allWit, allSqu, boardState.getLogger());
}
public WitnessWeb(BoardState boardState, List<? extends Location> allWit, Collection<Location> allSqu, Logger logger) {
//long nanoStart = System.nanoTime();
this.logger = logger;
this.boardState = boardState;
this.originalWitnesses = allWit;
// create squares for all the Square locations provided
for (Location squ: allSqu) {
squares.add(new Square(squ));
}
// create witnesses for all the Witness locations provided
// and attach adjacent Squares
List<Square> adjSqu;
for (Location wit: originalWitnesses) {
// calculate how many mines are left to find
//int mines = gs.query(wit) - solver.countConfirmedFlags(wit);
int mines = boardState.getWitnessValue(wit) - boardState.countAdjacentConfirmedFlags(wit);
adjSqu = new ArrayList<>();
for (Square squ: squares) {
if (squ.isAdjacent(wit)) {
adjSqu.add(squ);
}
}
if (mines > adjSqu.size() || mines < 0) {
validWeb = false;
return;
}
addWitness(new Witness(wit, mines, adjSqu));
}
// this sorts the witnesses by the number of iterations around them
Collections.sort(prunedWitnesses, Witness.SORT_BY_ITERATIONS_DESC);
// now attach non-pruned witnesses to adjacent Squares
for (Witness wit: prunedWitnesses) {
//System.out.println("Witness " + wit.getLocation().display() + " has " + wit.getSquares().size() + " squares");
for (Square squ: squares) {
if (squ.isAdjacent(wit)) {
squ.addWitness(wit);
}
}
//solver.display(wit.getLocation().display() + " has " + wit.getMines() + " Mines to find " + wit.getSquares().size() + " adjacent squares");
}
//long nanoEnd = System.nanoTime();
//boardState.display("Created witness web in " + (nanoEnd - nanoStart) + " nano-seconds");
}
private void setWeb(Square squ, int num) {
if (squ.getWebNum() != 0 && squ.getWebNum() != num) {
System.err.println("Square already assigned to a different web!!!!");
}
// if the square is already part of this web then no more to do here
if (squ.getWebNum() == num) {
return;
}
// claim this square
squ.setWebNum(num);
// claim all the Witnesses around this square and
// recursively claim all the other squares around those witnesses
for (Witness w: squ.getWitnesses()) {
w.setWebNum(num);
for (Square s: w.getSquares()) {
setWeb(s, num);
}
}
}
private void addWitness(Witness wit) {
// if the witness is a duplicate then don't store it
for (Witness w: prunedWitnesses) {
if (w.equivalent(wit)) {
if (boardState.getWitnessValue(w) - boardState.countAdjacentConfirmedFlags(w) != boardState.getWitnessValue(wit) - boardState.countAdjacentConfirmedFlags(wit)) {
logger.log(Level.WARN, "%s and %s share unrevealed squares but have different mine totals!", w, wit);
validWeb = false;
}
pruned++;
return;
}
}
prunedWitnesses.add(wit);
}
/**
* Generate boxes of tiles which all share the same witnesses. These are used in the probability engine and must have the same probability.
*/
public void generateBoxes() {
int boxCount = 0;
// put each square in a box
for (Square squ: squares) {
boolean found = false;
// see if the square fits an existing box
for (Box b: boxes) {
if (b.fitsBox(squ)) {
b.addSquare(squ);
found = true;
break;
}
}
// if not create a new box for it
if (!found) {
boxes.add(new Box(squ, boxCount));
boxCount++;
}
}
int minesLeft = boardState.getMines() - boardState.getConfirmedMineCount();
for (Box b: boxes) {
b.calculate(minesLeft);
//b.display();
}
}
/**
* Generate independent witnesses which can be used by the brute force iteration processing
*/
public void generateIndependentWitnesses() {
remainingSquares = this.squares.size();
// find a set of witnesses which don't share any squares (there can be many of these, but we just want one to use with the brute force iterator)
for (Witness w: prunedWitnesses) {
if (w.getMines() == 0) {
continue;
}
boolean okay = true;
for (Witness iw: independentWitness) {
if (w.overlap(iw)) {
okay = false;
break;
}
}
if (okay) {
remainingSquares = remainingSquares - w.getSquares().size();
independentIterations = independentIterations.multiply(Solver.combination(w.getMines(), w.getSquares().size()));
independentMines = independentMines + w.getMines();
independentWitness.add(w);
}
}
}
/**
* Returns the number of mines around the independent witnesses. The number of mines in any solution can't be less than this.
* @return
*/
public int getMinesPlaced() {
return independentMines;
}
/**
* The locations on the edge
*/
public List<Square> getSquares() {
return squares;
}
/**
* The deduplicated witnesses in this web
*/
public List<Witness> getPrunedWitnesses() {
return prunedWitnesses;
}
/**
* The witnesses passed into this web
*/
public List<? extends Location> getOriginalWitnesses() {
return originalWitnesses;
}
public List<Witness> getIndependentWitnesses() {
return independentWitness;
}
public List<Box> getBoxes() {
return this.boxes;
}
// how many iterations will be required to process this web with the provided number of mines
public BigInteger getIterations(int mines) {
// if too many or too few mines then no work needs to be done
if (mines < independentMines || mines > independentMines + remainingSquares) {
return BigInteger.ZERO;
}
BigInteger result = independentIterations.multiply(Solver.combination(mines - independentMines, remainingSquares));
return result;
}
/**
* The number of ways the non-independent squares and mines can be arranged
* @param mines
* @return
*/
public BigInteger getNonIndependentIterations(int mines) {
// if too many or too few mines then no work needs to be done
if (mines < independentMines || mines > independentMines + remainingSquares) {
return BigInteger.ZERO;
}
BigInteger result = Solver.combination(mines - independentMines, remainingSquares);
return result;
}
//public int getSharedSquares() {
// return remainingSquares;
//}
public int getIndependentMines() {
return independentMines;
}
/*
public List<WitnessWeb> getSubWebs() {
List<WitnessWeb> result = new ArrayList<>();
if (webNum == 1) {
result.add(this);
} else {
for (int i=0; i < webNum; i++) {
result.add(createSubWeb(i+1));
}
}
return result;
}
// create a WitnessWeb from a sub web of this one
private WitnessWeb createSubWeb(int n) {
if (n < 1 || n > webNum ) {
System.err.println("requesting sub-web " + n + " of ( 1 to " + webNum + ")");
}
List<Location> wit = new ArrayList<Location>();
List<Location> squ = new ArrayList<Location>();
for (Witness w: prunedWitnesses) {
if (w.getWebNum() == n) {
wit.add(w);
}
}
for (Square s: squares) {
if (s.getWebNum() == n) {
squ.add(s);
}
}
WitnessWeb result = new WitnessWeb(boardState, wit, squ);
return result;
}
*/
// if the location passed is a square in the web then return true;
public boolean isOnWeb(Location l) {
for (Square s: squares) {
if (s.equals(l)) {
return true;
}
}
return false;
}
public void addSolution(CrunchResult e) {
solutions.add(e);
}
public List<CrunchResult> getSolutions() {
return solutions;
}
public boolean isWebValid() {
return validWeb;
}
}

View File

@ -0,0 +1,430 @@
package minesweeper.solver.bulk;
import java.math.BigDecimal;
import java.util.Random;
import minesweeper.gamestate.GameStateModel;
import minesweeper.solver.bulk.BulkRequest.BulkAction;
import minesweeper.solver.settings.PlayStyle;
import minesweeper.solver.settings.SolverSettings;
abstract public class BulkController implements Runnable {
/*
public enum PlayStyle {
FLAGGED(false, false),
NO_FLAG(true, false),
EFFICIENCY(true, true);
public final boolean flagless;
public final boolean useChords;
private PlayStyle(boolean flagless, boolean useChords) {
this.flagless = flagless;
this.useChords = useChords;
}
}
*/
private final int gamesToPlay;
private final int workers;
private final SolverSettings solverSettings;
private final int bufferSize;
private final BulkRequest[] buffer;
private final BulkWorker[] bulkWorkers;
private final static int REPORT_INTERVAL = 200;
private final static int DEFAULT_BUFFER_PER_WORKER = 1000;
private volatile int waitingSlot = -1; // this is the next slot we are waiting to be returned
private volatile int nextSlot = 0; // this is the next slot to be dispatched
private volatile int nextSequence = 1;
private volatile int waitingSequence = 0;
private volatile int reportInterval = REPORT_INTERVAL;
private final Random seeder;
private volatile boolean finished = false;
private volatile BulkEvent event;
private volatile BulkEvent finalEvent;
private Thread mainThread;
private long startTime;
private long endTime;
private BulkListener eventListener;
private GamePostListener postGameListener;
private GamePreListener preGameListener;
private volatile int failedToStart = 0;
private volatile int wins = 0;
private volatile int guesses = 0;
private volatile int noGuessWins = 0;
private volatile BigDecimal totalGamesValue = BigDecimal.ZERO;
private volatile int totalActions = 0;
private volatile long total3BV = 0;
private volatile long total3BVSolved = 0;
private volatile BigDecimal fairness = BigDecimal.ZERO;
private volatile int currentWinStreak = 0;
private volatile int bestWinStreak = 0;
private volatile int currentMastery = 0;
private volatile int bestMastery = 0;
private volatile boolean[] mastery = new boolean[100];
private PlayStyle playStyle = PlayStyle.NO_FLAG;
public BulkController(Random seeder, int gamesToPlay, SolverSettings solverSettings, int workers) {
this(seeder, gamesToPlay, solverSettings, workers, DEFAULT_BUFFER_PER_WORKER);
}
public BulkController(Random seeder, int gamesToPlay, SolverSettings solverSettings, int workers, int bufferPerWorker) {
this.seeder = seeder;
this.gamesToPlay = gamesToPlay;
this.workers = workers;
this.bulkWorkers = new BulkWorker[this.workers];
this.solverSettings = solverSettings;
this.bufferSize = bufferPerWorker * this.workers;
this.buffer = new BulkRequest[bufferSize];
}
public void registerEventListener(BulkListener listener) {
this.eventListener = listener;
}
public void registerPostGameListener(GamePostListener listener) {
this.postGameListener = listener;
}
public void registerPreGameListener(GamePreListener listener) {
this.preGameListener = listener;
}
/**
* Set the play style
*/
public void setPlayStyle(PlayStyle playStyle) {
this.playStyle = playStyle;
}
public PlayStyle getPlayStyle() {
return this.playStyle;
}
public void setReportInterval(int reportInterval) {
this.reportInterval = reportInterval;
}
/**
* Start the number of workers and wait for them to complete. If you don't want your main thread paused then run this on a separate thread.
*/
@Override
public void run() {
this.startTime = System.currentTimeMillis();
// remember the current thread so we can wake it when completed
mainThread = Thread.currentThread();
for (int i=0; i < workers; i++) {
bulkWorkers[i] = new BulkWorker(this, solverSettings);
new Thread(bulkWorkers[i], "worker-" + (i+1)).start();
}
while (!finished) {
try {
Thread.sleep(10000);
//System.out.println("Main thread waiting for bulk run to complete...");
} catch (InterruptedException e) {
//System.out.println("Main thread wait has been interrupted");
// process the event and then set it to null
if (event != null) {
if (eventListener != null) {
eventListener.intervalAction(event);
}
event = null;
}
}
}
this.endTime = System.currentTimeMillis();
//System.out.println("Finished after " + getDuration() + " milliseconds");
}
/**
* Request each of the workers to stop and then stop the run
*/
public void stop() {
for (BulkWorker worker: bulkWorkers) {
worker.stop();
}
// create a final event
finished = true;
this.event = createEvent();
this.finalEvent = this.event;
// set the process to finished and wake the main thread
mainThread.interrupt();
}
/**
* When the process is finished you can get the final results from here
*/
public BulkEvent getResults() {
return this.finalEvent;
}
/**
* Returns true when the bulk run is completed or been stopped
*/
public boolean isFinished() {
return finished;
}
/**
* returns how log the bulk run took in milliseconds, or how long it has been running depending if it has finished ot not
* @return
*/
public long getDuration() {
if (startTime == 0) { // not started
return 0;
} else if (finished && endTime != 0) { // finished
return endTime - startTime;
} else {
return System.currentTimeMillis() - startTime; // in flight
}
}
private void processSlots() {
boolean doEvent = false;
BulkEvent bulkEvent = null;
// process all the games which have been processed and are waiting in the buffer
while (buffer[waitingSlot] != null) {
BulkRequest request = buffer[waitingSlot];
int masteryIndex = request.sequence % 100;
if (request.gs.getGameState() == GameStateModel.WON) {
wins++;
totalGamesValue = totalGamesValue.add(request.gameValue);
//System.out.println(request.gs.getSeed() + " has 3BV " + request.gs.getTotal3BV() + " and actions " + request.gs.getActionCount());
if (request.guesses == 0) {
noGuessWins++;
}
currentWinStreak++;
if (currentWinStreak > bestWinStreak) {
bestWinStreak = currentWinStreak;
}
// if we lost 100 games ago then mastery is 1 more
if (!mastery[masteryIndex]) {
mastery[masteryIndex] = true;
currentMastery++;
if (currentMastery > bestMastery) {
bestMastery = currentMastery;
}
}
double efficiency = 100 * ((double) request.gs.getTotal3BV() / (double) request.gs.getActionCount());
} else {
if (!request.startedOkay) {
failedToStart++;
}
currentWinStreak = 0;
// if we won 100 games ago, then mastery is now 1 less
if (mastery[masteryIndex]) {
mastery[masteryIndex] = false;
currentMastery--;
}
}
// accumulate the total actions taken
totalActions = totalActions + request.gs.getActionCount();
// accumulate 3BV in the game and how much was solved
total3BV = total3BV + request.gs.getTotal3BV();
total3BVSolved = total3BVSolved + request.gs.getCleared3BV();
// accumulate total guesses made
guesses = guesses + request.guesses;
fairness = fairness.add(request.fairness);
// clear the buffer and move on to the next slot
buffer[waitingSlot] = null;
waitingSlot++;
waitingSequence++;
// recycle the buffer when we get beyond the top
if (this.waitingSlot >= bufferSize) {
this.waitingSlot = this.waitingSlot - bufferSize;
}
// if we have run and processed all the games then wake the main thread
if (waitingSequence == gamesToPlay) {
//System.out.println("All games played, waking the main thread");
finished = true;
this.finalEvent = createEvent();
bulkEvent = this.finalEvent;
doEvent = true;
//mainThread.interrupt();
// provide an update every now and again, do that on the main thread
} else if (this.reportInterval != 0 && waitingSequence % this.reportInterval == 0) {
bulkEvent = createEvent();
doEvent = true;
}
if (postGameListener != null) {
postGameListener.postAction(request);
}
}
// if we have an event to do then interrupt the main thread which will post it
if (doEvent) {
if (this.event == null) {
this.event = bulkEvent;
mainThread.interrupt();
} else {
System.out.println("Event suppressed because earlier event is still in progress");
}
}
}
private BulkEvent createEvent() {
BulkEvent event = new BulkEvent();
event.setGamesToPlay(gamesToPlay);
event.setGamesPlayed(waitingSequence);
event.setGamesWon(wins);
event.setTotalGamesValue(totalGamesValue);
event.setTotalGuesses(guesses);
event.setNoGuessWins(noGuessWins);
if (guesses != 0) {
event.setFairness(fairness.doubleValue() / guesses);
} else {
event.setFairness(0);
}
event.setMastery(bestMastery);
event.setWinStreak(bestWinStreak);
event.setTotalActions(totalActions);
event.setFailedToStart(failedToStart);
event.setTotal3BV(total3BV);
event.setTotal3BVSolved(total3BVSolved);
long duration = getDuration();
long timeLeft;
if (waitingSequence != 0) {
timeLeft = ((duration * gamesToPlay) / waitingSequence) - duration;
} else {
timeLeft = 0;
}
event.setTimeSoFar(duration);
event.setEstimatedTimeLeft(timeLeft);
event.setFinished(finished);
return event;
}
/**
* Returns the last request and gets the next
*/
protected synchronized BulkRequest getNextRequest(BulkRequest request) {
if (request != null) {
buffer[request.slot] = request;
// if this is the slot we are waiting on then process the games which are in the buffer - this is all synchronised so nothing else arrives will it happens
if (request.slot == waitingSlot) {
processSlots();
}
}
// if we have played all the games or we have been stopped then tell the workers to stop
if (nextSequence > gamesToPlay || finished) {
return BulkRequest.STOP;
}
// if the next sequence is a long way ahead of the waiting sequence then wait until we catch up. Tell the worker to wait.
if (nextSequence > waitingSequence + bufferSize - 2) {
System.out.println("Buffer is full after " + nextSequence + " games dispatched");
return BulkRequest.WAIT;
}
// otherwise dispatch the next game to be played
//GameSettings gameSettings = GameSettings.EXPERT;
//GameType gameType = GameType.STANDARD;
//SolverSettings settings = SettingsFactory.GetSettings(Setting.SMALL_ANALYSIS).setExperimentalScoring(true);
BulkRequest next = new BulkRequest();
next.action = BulkAction.RUN;
next.sequence = this.nextSequence;
next.slot = this.nextSlot;
next.gs = getGameState(Math.abs(seeder.nextLong() & 0xFFFFFFFFFFFFFl));
if (this.preGameListener != null) {
preGameListener.preAction(next.gs);
}
if (next.gs.getGameState() == GameStateModel.LOST) {
next.startedOkay = false;
}
// roll onto the next sequence
this.nextSequence++;
this.nextSlot++;
// if this is the first request then initialise the waiting slot
if (waitingSlot == -1) {
waitingSlot = 0;
}
// recycle the buffer when we get beyond the top
if (this.nextSlot >= bufferSize) {
this.nextSlot = this.nextSlot - bufferSize;
}
return next;
}
abstract protected GameStateModel getGameState(long seed);
}

View File

@ -0,0 +1,131 @@
package minesweeper.solver.bulk;
import java.math.BigDecimal;
public class BulkEvent {
private int gamesToPlay;
private int gamesPlayed;
private int gamesWon;
private int noGuessWins;
private int totalActions;
private BigDecimal totalGamesValue;
private long total3BV;
private long total3BVSolved;
private int totalGuesses;
private double fairness;
private int winStreak;
private int mastery;
private int failedToStart; // this is when the game didn't survive the start sequence
private long timeSoFar;
private long estimatedTimeLeft;
private boolean isFinished = false;
public int getFailedToStart() {
return failedToStart;
}
public int getGamesToPlay() {
return gamesToPlay;
}
public int getGamesPlayed() {
return gamesPlayed;
}
public int getGamesWon() {
return gamesWon;
}
public int getNoGuessWins() {
return noGuessWins;
}
public int getTotalGuesses() {
return totalGuesses;
}
public double getFairness() {
return fairness;
}
public int getWinStreak() {
return winStreak;
}
public int getMastery() {
return mastery;
}
public int getTotalActions() {
return totalActions;
}
public long getTotal3BV() {
return total3BV;
}
public long getTotal3BVSolved() {
return total3BVSolved;
}
protected void setFailedToStart(int failedToStart) {
this.failedToStart = failedToStart;
}
protected void setGamesToPlay(int gamesToPlay) {
this.gamesToPlay = gamesToPlay;
}
protected void setGamesPlayed(int gamesPlayed) {
this.gamesPlayed = gamesPlayed;
}
protected void setGamesWon(int gamesWon) {
this.gamesWon = gamesWon;
}
protected void setNoGuessWins(int noGuessWins) {
this.noGuessWins = noGuessWins;
}
protected void setTotalGuesses(int totalGuesses) {
this.totalGuesses = totalGuesses;
}
protected void setFairness(double fairness) {
this.fairness = fairness;
}
protected void setWinStreak(int winStreak) {
this.winStreak = winStreak;
}
protected void setMastery(int mastery) {
this.mastery = mastery;
}
protected void setTotalActions(int actions) {
this.totalActions = actions;
}
public long getTimeSoFar() {
return timeSoFar;
}
public long getEstimatedTimeLeft() {
return estimatedTimeLeft;
}
protected void setTimeSoFar(long timeSoFar) {
this.timeSoFar = timeSoFar;
}
protected void setEstimatedTimeLeft(long estimatedTimeLeft) {
this.estimatedTimeLeft = estimatedTimeLeft;
}
public boolean isFinished() {
return isFinished;
}
protected void setFinished(boolean isFinished) {
this.isFinished = isFinished;
}
protected void setTotal3BV(long total3bv) {
total3BV = total3bv;
}
protected void setTotal3BVSolved(long total3bvSolved) {
total3BVSolved = total3bvSolved;
}
public BigDecimal getTotalGamesValue() {
return totalGamesValue;
}
public void setTotalGamesValue(BigDecimal totalGamesValue) {
this.totalGamesValue = totalGamesValue;
}
}

View File

@ -0,0 +1,10 @@
package minesweeper.solver.bulk;
public abstract class BulkListener {
/**
* This is run at regular intervals and should be used to provide any out put that is needed
*/
abstract public void intervalAction(BulkEvent event);
}

View File

@ -0,0 +1,42 @@
package minesweeper.solver.bulk;
import java.util.Random;
import minesweeper.gamestate.GameFactory;
import minesweeper.gamestate.GameStateModel;
import minesweeper.settings.GameSettings;
import minesweeper.settings.GameType;
import minesweeper.solver.settings.SolverSettings;
public class BulkPlayer extends BulkController {
protected final GameType gameType;
protected final GameSettings gameSettings;
//private List<Action> preActions;
/**
* Use the bulk controller to play games from the beginning
*/
public BulkPlayer(Random seeder, int gamesToPlay, GameType gameType, GameSettings gameSettings, SolverSettings solverSettings, int workers) {
super(seeder, gamesToPlay, solverSettings, workers);
this.gameType = gameType;
this.gameSettings = gameSettings;
}
public BulkPlayer(Random seeder, int gamesToPlay, GameType gameType, GameSettings gameSettings, SolverSettings solverSettings, int workers, int bufferPerWorker) {
super(seeder, gamesToPlay, solverSettings, workers, bufferPerWorker);
this.gameType = gameType;
this.gameSettings = gameSettings;
}
protected GameStateModel getGameState(long seed) {;
GameStateModel gs = GameFactory.create(gameType, gameSettings, seed);
return gs;
}
}

View File

@ -0,0 +1,44 @@
package minesweeper.solver.bulk;
import java.math.BigDecimal;
import minesweeper.gamestate.GameStateModel;
public class BulkRequest {
protected final static BulkRequest WAIT = new BulkRequest() {
{
action = BulkAction.WAIT;
}
};
protected final static BulkRequest STOP = new BulkRequest() {
{
action = BulkAction.STOP;
}
};
public enum BulkAction {
STOP,
WAIT,
RUN;
}
protected BulkAction action;
protected int sequence; // the sequence number for this request
protected int slot; // the slot the request is to be store in the buffer
protected GameStateModel gs;
protected int guesses = 0;
protected BigDecimal fairness = BigDecimal.ZERO;
protected BigDecimal gameValue = BigDecimal.ONE;
protected boolean startedOkay = true;
public GameStateModel getGame( ) {
return this.gs;
}
public int getGuesses() {
return this.guesses;
}
}

View File

@ -0,0 +1,47 @@
package minesweeper.solver.bulk;
import java.util.Random;
import minesweeper.gamestate.GameStateModel;
import minesweeper.solver.RolloutGenerator;
import minesweeper.solver.settings.SolverSettings;
import minesweeper.structure.Action;
import minesweeper.structure.Location;
public class BulkRollout extends BulkController {
protected final RolloutGenerator generator;
protected final Location safeTile;
protected final Location startTile;
/**
* Use the bulk controller to play games from the beginning
*/
public BulkRollout(Random seeder, int gamesToPlay, RolloutGenerator generator, Location startTile, boolean safeStart, SolverSettings solverSettings, int workers) {
super(seeder, gamesToPlay, solverSettings, workers);
this.generator = generator;
this.startTile = startTile;
if (safeStart) {
this.safeTile = startTile;
} else {
this.safeTile = null;
}
}
protected GameStateModel getGameState(long seed) {
GameStateModel gs = generator.generateGame(seed, safeTile);
// play the start tile and return the game
gs.doAction(new Action(startTile, Action.CLEAR));
return gs;
}
}

View File

@ -0,0 +1,142 @@
package minesweeper.solver.bulk;
import java.math.BigDecimal;
import minesweeper.gamestate.GameStateModel;
import minesweeper.solver.Solver;
import minesweeper.solver.bulk.BulkRequest.BulkAction;
import minesweeper.solver.settings.SolverSettings;
import minesweeper.structure.Action;
public class BulkWorker implements Runnable {
private boolean stop = false;
private final BulkController controller;
private final SolverSettings solverSettings;
protected BulkWorker(BulkController controller, SolverSettings solverSettings) {
this.controller = controller;
this.solverSettings = solverSettings;
}
@Override
public void run() {
//System.out.println(Thread.currentThread().getName() + " is starting");
BulkRequest request = controller.getNextRequest(null);
while (!stop) {
if (request.action == BulkAction.STOP) {
stop = true;
break;
} else if (request.action == BulkAction.WAIT) { // wait and then ask again
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
request = controller.getNextRequest(null);
} else {
//System.out.println("Playing game sequence " + request.sequence);
// play the game
playGame(request);
// return it to the controller
request = controller.getNextRequest(request);
}
}
//System.out.println(Thread.currentThread().getName() + " is stopping");
}
private void playGame(BulkRequest request) {
int state;
// if the game is won or lost already then nothing to do. This can be the case since we don't know what state the Game State model is in.
if (request.gs.getGameState() == GameStateModel.WON || request.gs.getGameState() == GameStateModel.LOST) {
return;
}
Solver solver = new Solver(request.gs, this.solverSettings, false);
solver.setPlayStyle(controller.getPlayStyle());
//solver.setFlagFree(controller.getPlayStyle().flagless);
//solver.setPlayChords(controller.getPlayStyle().useChords);
int loopCounter = 0;
play: while (true) {
loopCounter++;
if (loopCounter % 1000 == 0) {
System.err.println("Game " + request.gs.showGameKey() + " is looping");
break play;
}
Action[] moves;
try {
solver.start();
moves = solver.getResult();
} catch (Exception e) {
System.err.println("Game " + request.gs.showGameKey() + " has thrown an exception!");
e.printStackTrace();
return;
}
if (moves.length == 0) {
System.err.println(request.gs.getSeed() + " - No moves returned by the solver");
return;
}
// play all the moves until all done, or the game is won or lost
for (int i=0; i < moves.length; i++) {
BigDecimal prob = moves[i].getBigProb();
if (prob.compareTo(BigDecimal.ZERO) <= 0 || prob.compareTo(BigDecimal.ONE) > 0) {
System.err.println("Game (" + request.gs.showGameKey() + ") move with probability of " + prob + "! - " + moves[i].toString());
}
boolean result = request.gs.doAction(moves[i]);
state = request.gs.getGameState();
// only monitor good guesses (brute force, probability engine, zonal, opening book and hooks)
if (state == GameStateModel.STARTED || state == GameStateModel.WON) {
if (!moves[i].isCertainty() ) {
request.guesses++;
//request.fairness = request.fairness + 1d;
request.fairness = request.fairness.add(BigDecimal.ONE).subtract(prob);
}
} else { // otherwise the guess resulted in a loss
if (!moves[i].isCertainty()) {
request.guesses++;
//request.fairness = request.fairness - prob.doubleValue() / (1d - prob.doubleValue());
request.fairness = request.fairness.subtract(prob);
}
}
if (state == GameStateModel.LOST && moves[i].isCertainty()) {
System.err.println("Game (" + request.gs.showGameKey() + ") lost on move with probability = " + prob + " :" + moves[i].toString());
}
if (state == GameStateModel.LOST || state == GameStateModel.WON) {
break play;
}
}
}
request.gameValue = solver.getWinValue();
}
protected void stop() {
stop = true;
}
}

View File

@ -0,0 +1,18 @@
package minesweeper.solver.bulk;
/**
* The "postAction" method is run after each game finishes
*/
public abstract class GamePostListener {
/**
* This is run after each game finishes
*/
abstract public void postAction(BulkRequest request);
/**
* Place the results you want to show here
*/
abstract public void postResults();
}

View File

@ -0,0 +1,12 @@
package minesweeper.solver.bulk;
import minesweeper.gamestate.GameStateModel;
public abstract class GamePreListener {
/**
* This is run before each game starts
*/
abstract public void preAction(GameStateModel game);
}

View File

@ -0,0 +1,20 @@
package minesweeper.solver.coach;
public interface CoachModel {
abstract public void clearScreen();
abstract public void writeLine(String text);
abstract public void setOkay();
abstract public void setWarn();
abstract public void setError();
abstract public void kill();
abstract public boolean analyseFlags();
}

View File

@ -0,0 +1,40 @@
package minesweeper.solver.coach;
/**
* A implementation of the CoachModel class which does nothing
* @author David
*
*/
public class CoachSilent implements CoachModel {
@Override
public void clearScreen() {
}
@Override
public void writeLine(String text) {
}
@Override
public void setOkay() {
}
@Override
public void setWarn() {
}
@Override
public void setError() {
}
@Override
public void kill() {
}
@Override
public boolean analyseFlags() {
return false;
}
}

View File

@ -0,0 +1,296 @@
package minesweeper.solver.constructs;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import minesweeper.structure.Location;
/**
* A Box contains all the squares that share the same witnesses. A consequence of this is that they
* must have the same probability of being a mine.
* @author David
*
*/
public class Box {
final private List<Witness> adjWitnesses;
final private List<Square> squares = new ArrayList<>();
private int maxMines;
private int minMines;
final private int uid; // this is a sequential count given to each box as it is created; 0, 1, 2, etc
private boolean processed = false;
private boolean dominated = false;
// defer guessing is set when the box is part of a enclosed edge where knowing the
// number of mines left will solve the edge
private boolean deferGuessing = false;
// this is used to indicate how many tiles in the box must not contain mine.
private int emptyTiles = 0;
private int edgeLength;
private BigDecimal safety;
private BigInteger tally = BigInteger.ZERO;
public Box(Square square, int uid) {
this.uid = uid;
adjWitnesses = square.getWitnesses();
squares.add(square);
// connect this box to all the adjacent witnesses
for (Witness w: adjWitnesses) {
w.addBox(this);
}
}
/**
* Once all the squares have been added we can do some calculations
*/
public void calculate(int minesLeft) {
maxMines = Math.min(squares.size(), minesLeft); // can't have more mines then there are squares to put them in or mines left to discover
minMines = 0;
for (Witness adjWit: adjWitnesses) {
// can't have more mines than the lowest constraint
if (adjWit.getMines() < maxMines) {
maxMines = adjWit.getMines();
}
// if an adjacent witness has all it's tiles in this box then we need at least that many tiles in the box
if (this.squares.size() > 1 && adjWit.getMines() > minMines) {
boolean subset = true;
int adjWitMinMines = adjWit.getMines(); // the minimum number of mines left if we assume every outside tile contains a mine
for (Square square: adjWit.getSquares()) {
if (!this.contains(square)) {
subset = false;
adjWitMinMines--;
if (adjWitMinMines <= minMines) { // worse then our current minimum
break;
}
}
}
minMines = Math.max(minMines, adjWitMinMines);
//System.out.println("Tile " + w + " places " + w.getMines() + " in a box");
}
// this has a small improvement on time, but (for some reason) has a small -ve impact on win rate. Need to investigate more.
// must be at least this many mines in the box.
//int workMin = w.getMines() - (w.getSquares().size() -this.squares.size()); // mines left for this witness - tiles adjacent not in the box (where the mines could be)
//if (workMin > minMines) {
// minMines = workMin;
//}
// this seems tohave no impact on time or win rate
// if the witness only has 1 adjacent box - i.e. us then the minimum number of mines must also be set
//if (w.getBoxes().size() == 1) {
// minMines = w.getMines();
//}
}
}
/**
* A Square fits the Box if they have the same witnesses
* @param square
* @return
*/
public boolean fitsBox(Square square) {
// they can't share the same witnesses if they have different number of them
if (adjWitnesses.size() != square.getWitnesses().size()) {
return false;
}
//check that each witness of the candidate square is also a witness for the box
for (Witness w: square.getWitnesses()) {
boolean found = false;
for (Witness boxWitness: adjWitnesses) {
if (w.equals(boxWitness)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
public boolean contains(Location l) {
for (Square squ: squares) {
if (squ.equals(l)) {
return true;
}
}
return false;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Box)) {
return false;
}
Box box = (Box) obj;
// to be equal they must have the same number of squares
if (this.squares.size() != box.squares.size()) {
return false;
}
for (Square sq1: this.squares) {
boolean found = false;
for (Square sq2: box.squares) {
if (sq1.equals(sq2)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
/**
* Add this square to the box
* @param square
*/
public void addSquare(Square square) {
squares.add(square);
}
/**
* Get witness that are adjacent to this box
*/
public List<Witness> getWitnesses() {
return this.adjWitnesses;
}
/**
* Get squares that are in this box
*/
public List<Square> getSquares() {
return this.squares;
}
public boolean isProcessed() {
return this.processed;
}
public void setProcessed(boolean processed) {
this.processed = processed;
}
public int getUID() {
return this.uid;
}
public int getMaxMines() {
return this.maxMines;
}
public int getMinMines() {
return this.minMines;
}
public void setDominated() {
this.dominated = true;
}
public boolean isDominated() {
return this.dominated;
}
public void setDeferGuessing() {
this.deferGuessing = true;
}
public boolean doDeferGuessing() {
return this.deferGuessing;
}
public int getEmptyTiles() {
return emptyTiles;
}
public void setEdgeLength(int length) {
this.edgeLength = length;
}
public int getEdgeLength() {
return this.edgeLength;
}
public void setSafety(BigDecimal safety) {
this.safety = safety;
}
public BigDecimal getSafety() {
return this.safety;
}
public void setTally(BigInteger tally) {
this.tally = tally;
}
public BigInteger getTally() {
return this.tally;
}
public void incrementEmptyTiles() {
this.emptyTiles++;
// can't have more mines than there are squares to put them
if (this.maxMines > this.squares.size() - this.emptyTiles) {
this.maxMines = this.squares.size() - this.emptyTiles;
if (this.maxMines < this.minMines) {
System.out.println("Illegal Mines: max " + maxMines + " min " + minMines);
}
}
}
public void display() {
System.out.print("Box Witnesses: ");
for (Witness w: adjWitnesses) {
System.out.print(w.toString());
}
System.out.println("");
System.out.print("Box Squares: ");
for (Square squ: squares) {
System.out.print(squ.toString());
}
System.out.println("");
System.out.println("Mines: max " + maxMines + " min " + minMines);
}
}

View File

@ -0,0 +1,140 @@
package minesweeper.solver.constructs;
import java.math.BigDecimal;
import java.util.Comparator;
import minesweeper.gamestate.MoveMethod;
import minesweeper.structure.Action;
import minesweeper.structure.Location;
public class CandidateLocation extends Location {
private final BigDecimal prob;
private String description = "";
private final int adjSquares;
private final int adjFlags;
private final boolean dead; // Whether the tile is dead
private final boolean deferGuessing; // Whether the tile is not a good idea to guess
public CandidateLocation(int x, int y, BigDecimal prob, int adjSquares, int adjFlags) {
this(x, y, prob, adjSquares, adjFlags, false, false);
}
public CandidateLocation(int x, int y, BigDecimal prob, int adjSquares, int adjFlags, boolean dead, boolean deferGuessing) {
super(x,y);
this.prob = prob;
this.adjSquares = adjSquares;
this.adjFlags = adjFlags;
this.dead = dead;
this.deferGuessing = deferGuessing;
}
public BigDecimal getProbability() {
return this.prob;
}
public boolean isDead() {
return this.dead;
}
public void setDescription(String desc) {
this.description = desc;
}
public void appendDescription(String desc) {
if (this.description != "") {
this.description = this.description + " " + desc;
} else {
this.description = desc;
}
}
public String getDescription() {
return this.description;
}
public boolean getDeferGuessing() {
return this.deferGuessing;
}
public Action buildAction(MoveMethod method) {
String comment = description;
if (prob.compareTo(BigDecimal.ZERO) == 0) { // the best move is to place a flag
return new Action(this, Action.FLAG, method, comment, BigDecimal.ONE);
} else {
return new Action(this, Action.CLEAR, method, comment, prob);
}
}
/**
* This sorts by highest probability of not being a mine then the number of adjacent flags, unrevealed squares and finally Location order
*/
static public final Comparator<CandidateLocation> SORT_BY_PROB_FLAG_FREE = new Comparator<CandidateLocation>() {
@Override
public int compare(CandidateLocation o1, CandidateLocation o2) {
int c = 0;
c = -o1.prob.compareTo(o2.prob); // highest probability first
if (c == 0) {
if (o1.deferGuessing && !o2.deferGuessing) {
c = -1;
} else if (!o1.deferGuessing && o2.deferGuessing) {
c = 1;
} else {
c = -(o1.adjFlags - o2.adjFlags); // highest number of flags 2nd
if (c == 0) {
c= o1.adjSquares - o2.adjSquares; // lowest adjacent free squares
if (c == 0) {
c = o1.sortOrder - o2.sortOrder; // location order
}
}
}
}
return c;
}
};
/**
* This sorts by highest probability of not being a mine then the number unrevealed squares (lowest first), then of adjacent flags (highest first) ,and finally Location order
*/
static public final Comparator<CandidateLocation> SORT_BY_PROB_FREE_FLAG = new Comparator<CandidateLocation>() {
@Override
public int compare(CandidateLocation o1, CandidateLocation o2) {
int c = -o1.prob.compareTo(o2.prob); // highest probability first
if (c == 0) {
int a1, a2;
if (o1.adjSquares == 0) {
a1 = 9;
} else {
a1 = o1.adjSquares;
}
if (o2.adjSquares == 0) {
a2 = 9;
} else {
a2 = o2.adjSquares;
}
c= a1 - a2; // lowest adjacent free squares (except zero treated as 9)
if (c == 0) {
c = -(o1.adjFlags - o2.adjFlags); // highest number of flags
if (c == 0) {
c = o1.sortOrder - o2.sortOrder; // location order
}
}
}
return c;
}
};
}

View File

@ -0,0 +1,108 @@
package minesweeper.solver.constructs;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
import minesweeper.structure.Location;
public class ChordLocation extends Location {
private final int benefit;
private final int cost;
private final int exposedTiles;
private final BigDecimal netBenefit;
private final BigDecimal scale;
private final List<Location> mines;
public ChordLocation(int x, int y, int benefit, int cost, int exposedTiles, BigDecimal scale, List<Location> mines) {
super(x, y);
this.benefit = benefit;
this.cost = cost;
this.exposedTiles = exposedTiles;
this.netBenefit = chordReward(benefit, cost).multiply(scale);
//this.netBenefit = chordReward(benefit, cost);
this.scale = scale; // probability of being a mine
this.mines = mines;
}
public int getBenefit() {
return this.benefit;
}
public int getCost() {
return this.cost;
}
public BigDecimal getNetBenefit() {
return this.netBenefit;
}
public int getExposedTileCount() {
return this.exposedTiles;
}
public List<Location> getMines() {
return this.mines;
}
public BigDecimal getScale() {
return this.scale;
}
static public final BigDecimal chordReward(int benefit, int cost) {
BigDecimal netBenefit; // benefit as a ratio of the cost
/*
if (cost == 0) {
netBenefit = BigDecimal.valueOf(benefit);
} else {
netBenefit = BigDecimal.valueOf(benefit - cost).divide(BigDecimal.valueOf(cost), 10, RoundingMode.HALF_UP); // benefit as a ratio of the cost
}
*/
/*
if (cost == 0) {
netBenefit = BigDecimal.valueOf(benefit);
} else {
netBenefit = BigDecimal.valueOf(benefit).divide(BigDecimal.valueOf(cost), 10, RoundingMode.HALF_UP); // benefit as a ratio of the cost
}
*/
netBenefit = BigDecimal.valueOf(benefit - cost); // absolute benefit without regard for the cost
return netBenefit;
}
static public final Comparator<ChordLocation> SORT_BY_BENEFIT_DESC = new Comparator<ChordLocation>() {
@Override
public int compare(ChordLocation o1, ChordLocation o2) {
int c = o2.netBenefit.compareTo(o1.netBenefit);
if (c==0) {
c = o2.exposedTiles - o1.exposedTiles;
}
if (c==0) {
c = o2.scale.compareTo(o1.scale);
}
return c;
//if (o2.netBenefit == o1.netBenefit) {
// return o2.exposedTiles - o1.exposedTiles;
//} else {
// return o2.netBenefit.compareTo(o1.netBenefit);
//}
}
};
}

View File

@ -0,0 +1,144 @@
package minesweeper.solver.constructs;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Comparator;
import java.util.List;
import minesweeper.gamestate.MoveMethod;
import minesweeper.structure.Action;
import minesweeper.structure.Location;
public class EvaluatedLocation extends Location {
private final BigDecimal safeProbability;
private BigDecimal weight;
private final BigDecimal maxValueProgress;
private String description = "";
private BigDecimal expectedClears;
private final int fixedClears; //number of tiles which are clears regardless of what value is revealed
private List<Box> emptyBoxes;
private boolean pruned = false;
private boolean deferGuessing = false;
public EvaluatedLocation(int x, int y, BigDecimal safeProbability, BigDecimal weight, BigDecimal expectedClears, int fixedClears,
List<Box> emptyBoxes, BigDecimal maxValueProgress) {
super(x,y);
this.safeProbability = safeProbability;
this.weight = weight.setScale(8, RoundingMode.UP); // give a slight bump up, so those coming later have to be actually better
this.expectedClears = expectedClears;
this.fixedClears = fixedClears;
this.maxValueProgress = maxValueProgress;
this.emptyBoxes = emptyBoxes;
}
public BigDecimal getProbability() {
return this.safeProbability;
}
public BigDecimal getWeighting() {
return this.weight;
}
public BigDecimal getMaxValueProgress() {
return maxValueProgress;
}
public List<Box> getEmptyBoxes() {
return emptyBoxes;
}
public Action buildAction(MoveMethod method) {
String comment = description;
return new Action(this, Action.CLEAR, method, comment, safeProbability);
}
public void setPruned() {
this.pruned = true;
}
public void setDeferGuessing(boolean deferGuessing) {
this.deferGuessing = deferGuessing;
}
public boolean isDeferGuessing() {
return this.deferGuessing;
}
@Override
public String toString() {
String prunedString;
if (this.pruned) {
prunedString = " ** Pruned";
} else {
prunedString = "";
}
return super.toString() + " Fixed clears is " + fixedClears + " expected clears is " + expectedClears.toPlainString()
+ ", final weight is " + weight + ", maximum tile value prob is " + maxValueProgress + ", defer guessing " + deferGuessing + prunedString;
}
/**
* This sorts by ...
*/
static public final Comparator<EvaluatedLocation> SORT_BY_WEIGHT = new Comparator<EvaluatedLocation>() {
@Override
public int compare(EvaluatedLocation o1, EvaluatedLocation o2) {
int c = 0;
if (c == 0) {
if (o1.deferGuessing && !o2.deferGuessing) {
c = 1;
} else if (!o1.deferGuessing && o2.deferGuessing) {
c = -1;
}
}
if (c == 0) {
c = -o1.weight.compareTo(o2.weight); // tile with the highest weighting
}
if (c == 0) {
c = -o1.expectedClears.compareTo(o2.expectedClears); // then highest expected number of clears
}
return c;
}
};
static public final Comparator<EvaluatedLocation> SORT_BY_SAFETY_MINIMAX = new Comparator<EvaluatedLocation>() {
@Override
public int compare(EvaluatedLocation o1, EvaluatedLocation o2) {
int c = 0;
c = -o1.safeProbability.compareTo(o2.safeProbability); // safest tiles
if (c == 0) {
c = o1.maxValueProgress.compareTo(o2.maxValueProgress); // then lowest max value ... the Minimax;
}
// go back to the weight option
if (c == 0) {
c = SORT_BY_WEIGHT.compare(o1, o2);
}
return c;
}
};
}

View File

@ -0,0 +1,151 @@
package minesweeper.solver.constructs;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import minesweeper.solver.Solver;
import minesweeper.structure.Location;
public class InformationLocation extends Location {
//public final static BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);
public class ByValue {
public final int value;
public final int clears;
public final BigDecimal probability;
private ByValue(int value, int clears, BigDecimal prob) {
this.value = value;
this.clears = clears;
this.probability = prob;
}
}
private BigDecimal safety;
private BigDecimal expectedClears;
private BigDecimal progressProbability;
private BigDecimal weighting;
private BigDecimal expectedSolutionSpaceReduction;
private BigDecimal mTanzerRatio;
private BigDecimal secondarySafety;
private BigDecimal longTermSafety;
//private BigDecimal poweredRatio;
private List<ByValue> byValues;
public InformationLocation(int x, int y) {
super(x, y);
}
public void calculate() {
BigDecimal expClears = BigDecimal.ZERO;
BigDecimal progProb = BigDecimal.ZERO;
BigDecimal ess = BigDecimal.ONE.subtract(this.safety); // expect solution space = p(mine) + sum[ P(n)*p(n) ]
BigDecimal powerClears = BigDecimal.ZERO;
if (byValues == null) {
return;
}
for (ByValue bv: byValues) {
//essr = essr.add(bv.probability.multiply(BigDecimal.ONE.subtract(bv.probability))); // sum of p(1-p)
ess = ess.add(bv.probability.multiply(bv.probability)); // sum of p^2
if (bv.clears != 0) {
progProb = progProb.add(bv.probability);
expClears = expClears.add(bv.probability.multiply(BigDecimal.valueOf(bv.clears)));
//powerClears = powerClears.add(bv.probability.multiply(new BigDecimal(Math.pow(1.15d, bv.clears))));
}
}
this.expectedClears = expClears;
this.progressProbability = progProb;
this.expectedSolutionSpaceReduction = ess;
BigDecimal bonus = BigDecimal.ONE.add(progressProbability.multiply(Solver.PROGRESS_VALUE));
this.weighting = this.safety.multiply(bonus);
if (this.safety.compareTo(BigDecimal.ONE) != 0) {
this.mTanzerRatio = this.progressProbability.divide(BigDecimal.ONE.subtract(safety), 4, RoundingMode.HALF_DOWN);
}
//if (this.prob.compareTo(BigDecimal.ONE) != 0) {
// this.poweredRatio = powerClears.divide(BigDecimal.ONE.subtract(prob), 4, RoundingMode.HALF_DOWN);
//}
}
public BigDecimal getSafety() {
return this.safety;
}
public void setSafety(BigDecimal prob) {
this.safety = prob;
}
public void setByValue(int value, int clears, BigDecimal prob) {
if (byValues == null) {
byValues = new ArrayList<>();
}
byValues.add(new ByValue(value, clears, prob));
}
public void setSecondarySafety(BigDecimal safety2) {
this.secondarySafety = safety2;
}
public void setLongTermSafety(BigDecimal lts) {
this.longTermSafety = lts;
}
public BigDecimal getSecondarySafety() {
return this.secondarySafety;
}
public BigDecimal getLongTermSafety() {
return this.longTermSafety;
}
public List<ByValue> getByValueData() {
return this.byValues;
}
public BigDecimal getExpectedClears() {
return expectedClears;
}
public BigDecimal getProgressProbability() {
return progressProbability;
}
public BigDecimal getWeighting() {
return weighting;
}
public BigDecimal getExpectedSolutionSpaceReduction() {
return this.expectedSolutionSpaceReduction;
}
public BigDecimal getMTanzerRatio() {
return this.mTanzerRatio;
}
//public BigDecimal getPoweredRatio() {
// return this.poweredRatio;
//}
}

View File

@ -0,0 +1,47 @@
package minesweeper.solver.constructs;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import minesweeper.structure.Location;
public class LinkedLocation extends Location {
private Set<Location> partners = new HashSet<>();
private int links = 0;
public LinkedLocation(int x, int y, List<? extends Location> partner) {
super(x, y);
incrementLinks(partner);
}
public void incrementLinks(List<? extends Location> partner) {
for (Location p: partner) {
if (partners.add(p)) {
links++;
}
}
}
public int getLinksCount() {
return links;
}
public Set<Location> getLinkedLocations() {
return partners;
}
static public final Comparator<LinkedLocation> SORT_BY_LINKS_DESC = new Comparator<LinkedLocation>() {
@Override
public int compare(LinkedLocation o1, LinkedLocation o2) {
return o2.links - o1.links;
}
};
}

View File

@ -0,0 +1,45 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver.constructs;
import java.util.ArrayList;
import java.util.List;
import minesweeper.structure.Location;
/**
*
* @author David
*/
public class Square extends Location {
private final List<Witness> witnesses = new ArrayList<>();
private int webNum = 0;
public Square(Location loc) {
super(loc.x, loc.y);
}
public void addWitness(Witness wit) {
witnesses.add(wit);
}
public List<Witness> getWitnesses() {
return witnesses;
}
public int getWebNum() {
return webNum;
}
public void setWebNum(int webNum) {
this.webNum = webNum;
}
}

View File

@ -0,0 +1,143 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver.constructs;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import minesweeper.solver.Solver;
import minesweeper.structure.Location;
/**
*
* @author David
*/
public class Witness extends Location {
private final int minesToFind; // the number of mines left to find
private final int iterations;
private int webNum = 0;
private final List<Square> squares;
private final List<Box> boxes = new ArrayList<>();
private boolean processed = false;
public Witness(Location loc, int mines, List<Square> adjSqu) {
super(loc.x, loc.y);
this.minesToFind = mines;
squares = adjSqu;
this.iterations = Solver.combination(mines, squares.size()).intValue();
}
public List<Square> getSquares() {
return this.squares;
}
public int getMines() {
return minesToFind;
}
public void addSquare(Square squ) {
squares.add(squ);
}
public void addBox(Box box) {
boxes.add(box);
}
public List<Box> getBoxes() {
return this.boxes;
}
public int getWebNum() {
return webNum;
}
public boolean isProcessed() {
return this.processed;
}
public void setProcessed(boolean processed) {
this.processed = processed;
}
public void setWebNum(int webNum) {
this.webNum = webNum;
}
// if two witnesses have the same Squares around them they are equivalent
public boolean equivalent(Witness wit) {
// if the number of squares is different then they can't be equal
if (squares.size() != wit.getSquares().size()) {
return false;
}
// if the locations are too far apart they can't share the same squares
if (Math.abs(wit.x - this.x) > 2 || Math.abs(wit.y - this.y) > 2) {
return false;
}
for (Square l1: squares) {
boolean found = false;
for (Square l2: wit.getSquares()) {
if (l2.equals(l1)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
public boolean overlap(Witness w) {
// if the locations are too far apart they can't share any of the same squares
if (Math.abs(w.x - this.x) > 2 || Math.abs(w.y - this.y) > 2) {
return false;
}
boolean result = false;
top: for (Square s: w.squares) {
for (Square s1: this.squares) {
if (s.equals(s1)) {
result = true;
break top;
}
}
}
return result;
}
/**
* This sorts by the number of iterations around this witness descending
*/
static public final Comparator<Witness> SORT_BY_ITERATIONS_DESC = new Comparator<Witness>() {
@Override
public int compare(Witness o1, Witness o2) {
return -(o1.iterations - o2.iterations);
}
};
}

View File

@ -0,0 +1,23 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver.constructs;
import minesweeper.structure.Location;
/**
*
* @author David
*/
public class WitnessData {
public Location location;
public boolean witnessRestFlag = true;
public boolean witnessRestClear = true;
public int witnessGood;
public int currentFlags;
public boolean alwaysSatisfied;
}

View File

@ -0,0 +1,49 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver.iterator;
import minesweeper.structure.Location;
/**
*
* @author David
*/
abstract public class Iterator {
final int numberBalls;
final int numberHoles;
public Iterator(int n, int m) {
this.numberBalls = n;
this.numberHoles = m;
}
public int[] getSample(int start) {
return null;
}
public int[] getSample() {
return getSample(numberBalls - 1);
}
public int getBalls() {
return numberBalls;
}
public int getHoles() {
return numberHoles;
}
// if this is true then the checkSample logic can ignore this witness
// This is used by the WitnessWebIterator since the IndependentWitnesses
// must always be satisified.
public boolean witnessAlwaysSatisfied(Location l) {
return false;
}
}

View File

@ -0,0 +1,65 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver.iterator;
/**
*
* @author David
*/
public class RandomIterator extends Iterator {
int max;
int count=0;
int[] shuffle;
int[] sample;
public RandomIterator(int n, int m, int max) {
super(n,m);
this.max = max;
shuffle = new int[m];
for (int i=0; i < shuffle.length; i++) {
shuffle[i] = i;
}
sample = new int[n];
}
@Override
public int[] getSample() {
if (count >= max) {
return null;
}
count++;
int top = shuffle.length -1;
// create a random sample
for (int i=0; i < sample.length; i++) {
int e = (int) Math.floor(Math.random()*top);
sample[i] = shuffle[e];
// swap shuffle[e] to the top off the array
shuffle[e] = shuffle[top];
shuffle[top] = sample[i];
// reduce top by 1 so, shuffle[e] can't be picked again
top--;
}
return sample;
}
}

View File

@ -0,0 +1,75 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver.iterator;
/**
*
* @author David
*/
public class SequentialIterator extends Iterator {
final private int[] sample;
private boolean more = true;
private int index;
// a sequential iterator that puts n-balls in m-holes once in each possible way
public SequentialIterator(int n, int m) {
super(n,m);
sample = new int[n];
index = n - 1;
for (int i=0; i < sample.length; i++) {
sample[i] = i;
}
// reduce the iterator by 1, since the first getSample() will increase it
// by 1 again
sample[index]--;
}
@Override
public int[] getSample(int start) {
if (!more) {
System.err.println("trying to iterate after the end");
return null;
}
index = start;
// add on one to the iterator
sample[index]++;
// if we have rolled off the end then move backwards until we can fit
// the next iteration
while (sample[index] >= numberHoles - numberBalls + 1 + index) {
if (index == 0) {
more = false;
return null;
} else {
index--;
sample[index]++;
}
}
// roll forward
while (index != numberBalls - 1) {
index++;
sample[index] = sample[index-1] + 1;
}
return sample;
}
}

View File

@ -0,0 +1,227 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.solver.iterator;
import java.util.ArrayList;
import java.util.List;
import minesweeper.solver.WitnessWeb;
import minesweeper.solver.constructs.Witness;
import minesweeper.structure.Location;
/**
*
* @author David
*/
public class WitnessWebIterator extends Iterator {
private int[] sample;
private List<Location> location;
private SequentialIterator[] cogs;
private int[] squareOffset;
private int[] mineOffset;
private int iterationsDone = 0;
final private int top;
final private int bottom;
private boolean done = false;
private WitnessWeb web;
public WitnessWebIterator(WitnessWeb web, int mines) {
this(web, mines, -1);
}
// create an iterator which is like a set of rotating wheels
public WitnessWebIterator(WitnessWeb web, int mines, int rotation) {
super(mines, web.getSquares().size());
this.web = web;
// if we are setting the position of the top cog then it can't ever change
if (rotation == -1) {
bottom = 0;
} else {
bottom = 1;
}
int indSquares = 0;
int indMines = 0;
cogs = new SequentialIterator[web.getIndependentWitnesses().size() + 1];
squareOffset = new int[web.getIndependentWitnesses().size() + 1];
mineOffset = new int[web.getIndependentWitnesses().size() + 1];
int cogi = 0;
List<Location> loc = new ArrayList<>();
// create an array of locations in the order of independent witnesses
for (Witness w: web.getIndependentWitnesses()) {
squareOffset[cogi] = indSquares;
mineOffset[cogi] = indMines;
//System.out.println(w.getMines() + " mines in " + w.getSquares().size() + " tiles");
cogs[cogi] = new SequentialIterator(w.getMines(), w.getSquares().size());
//System.out.println("Cog has " + cogs[cogi].numberBalls + " mines and " + cogs[cogi].numberHoles + " squares");
cogi++;
indSquares = indSquares + w.getSquares().size();
indMines = indMines + w.getMines();
loc.addAll(w.getSquares());
//for (Square s: w.getSquares()) {
// loc.add(s);
//}
}
//System.out.println("Mines left = " + (mines - indMines));
//System.out.println("Squares left = " + (web.getSquares().length - indSquares));
// the last cog has the remaining squares and mines
//add the rest of the locations
for (Location l: web.getSquares()) {
boolean skip = false;
for (Location m: loc) {
if (l.equals(m)) {
skip = true;
break;
}
}
if (!skip) {
loc.add(l);
}
}
location = loc;
// if there are more mines left then squares then no solution is possible
// if there are not enough mines to satisfy the minimum we know are needed
if (mines - indMines > web.getSquares().size() - indSquares
|| indMines > mines) {
done = true;
top = 0;
return;
}
// if there are no mines left then no need for a cog
if (mines > indMines) {
squareOffset[cogi] = indSquares;
mineOffset[cogi] = indMines;
cogs[cogi] = new SequentialIterator(mines - indMines, web.getSquares().size() - indSquares);
top = cogi;
} else {
top = cogi - 1;
}
sample = new int[mines];
// if we are locking and rotating the top cog then do it
if (rotation != -1) {
for (int i=0; i < rotation; i++) {
cogs[0].getSample();
}
}
// now set up the initial sample position
for (int i=0; i < top; i++) {
int[] s = cogs[i].getSample();
for (int j=0; j < s.length; j++) {
sample[mineOffset[i] + j] = squareOffset[i] + s[j];
}
}
/*
for (int i=0; i < sample.length; i++) {
System.out.print(sample[i] + " ");
}
System.out.println("");
*/
}
@Override
public int[] getSample(int start) {
// now we are running in parallel we need to ensure that this code isn't being run by more than 1 thread at a time
//synchronized (this) {
if (done) {
return null;
}
int index = top;
int[] s = cogs[index].getSample();
while (s == null && index != bottom) {
index--;
s = cogs[index].getSample();
}
if (index == bottom && s == null) {
done = true;
return null;
}
for (int j=0; j < s.length; j++) {
sample[mineOffset[index] + j] = squareOffset[index] + s[j];
}
index++;
while (index <= top) {
cogs[index] = new SequentialIterator(cogs[index].getBalls(), cogs[index].getHoles());
s = cogs[index].getSample();
for (int j=0; j < s.length; j++) {
sample[mineOffset[index] + j] = squareOffset[index] + s[j];
}
index++;
}
/*
for (int i: sample) {
System.out.print(i + " ");
}
System.out.println();
*/
iterationsDone++;
return sample;
//return Arrays.copyOf(sample, sample.length);
//}
}
public List<Location> getLocations() {
return location;
}
public int getIterations() {
return iterationsDone;
}
// if the location is a Independent witness then we know it will always
// have exactly the correct amount of mines around it since that is what
// this iterator does
@Override
public boolean witnessAlwaysSatisfied(Location l) {
for (Witness w: web.getIndependentWitnesses()) {
if (w.equals(l)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,16 @@
package minesweeper.solver.settings;
public enum PlayStyle {
FLAGGED(false, false),
NO_FLAG(true, false),
EFFICIENCY(false, true),
NO_FLAG_EFFICIENCY(true, true);
public final boolean flagless;
public final boolean efficiency;
private PlayStyle(boolean flagless, boolean efficiency) {
this.flagless = flagless;
this.efficiency = efficiency;
}
}

View File

@ -0,0 +1,126 @@
package minesweeper.solver.settings;
import java.math.BigInteger;
public class SettingsFactory {
public enum Setting {
NO_BRUTE_FORCE,
TINY_ANALYSIS,
SMALL_ANALYSIS,
LARGE_ANALYSIS,
VERY_LARGE_ANALYSIS,
MAX_ANALYSIS;
}
final static public SolverSettings GetSettings(Setting setting) {
if (setting == Setting.SMALL_ANALYSIS) {
return smallAnalysis();
} else if (setting == Setting.TINY_ANALYSIS) {
return tinyAnalysis();
} else if (setting == Setting.LARGE_ANALYSIS) {
return largeAnalysis();
} else if (setting == Setting.NO_BRUTE_FORCE) {
return noBruteForce();
} else if (setting == Setting.VERY_LARGE_ANALYSIS) {
return veryLargeAnalysis();
} else if (setting == Setting.MAX_ANALYSIS) {
return maxAnalysis();
}
return smallAnalysis();
}
private static SolverSettings noBruteForce() {
SolverSettings settings = new SolverSettings();
settings.bruteForceMaxSolutions = 0;
settings.bruteForceVariableSolutions = 0;
settings.bruteForceMaxNodes = 0;
settings.bruteForceTreeDepth = 10;
settings.bruteForceMaxIterations = BigInteger.ZERO;
return settings;
};
private static SolverSettings tinyAnalysis() {
SolverSettings settings = new SolverSettings();
settings.bruteForceMaxSolutions = 40;
settings.bruteForceVariableSolutions = 15;
settings.bruteForceMaxNodes = 150000;
settings.bruteForceTreeDepth = 10;
settings.bruteForceMaxIterations = new BigInteger("1000000"); // 5 million
return settings;
};
/**
* Does trivial, Local and Probability Engine searches.
* Does a small brute force search with a 400 solution deep analysis.
* This is suitable for bulk runs.
*/
private static SolverSettings smallAnalysis() {
SolverSettings settings = new SolverSettings();
settings.bruteForceMaxSolutions = 400;
settings.bruteForceVariableSolutions = 250;
settings.bruteForceMaxNodes = 300000;
settings.bruteForceTreeDepth = 10;
settings.bruteForceMaxIterations = new BigInteger("10000000"); // 10 million
return settings;
};
/**
* Does trivial, Local and Probability Engine searches.
* Does a large brute force search with a 4000 solution deep analysis.
* This is probably not suitable for bulk runs.
*/
private static SolverSettings largeAnalysis() {
SolverSettings settings = new SolverSettings();
settings.bruteForceMaxSolutions = 4000;
settings.bruteForceVariableSolutions = 2000;
settings.bruteForceMaxNodes = 20000000; // 20 million
settings.bruteForceTreeDepth = 10;
settings.bruteForceMaxIterations = new BigInteger("10000000"); // 10 million
return settings;
};
private static SolverSettings veryLargeAnalysis() {
SolverSettings settings = new SolverSettings();
settings.bruteForceMaxSolutions = 40000;
settings.bruteForceVariableSolutions = 20000;
settings.bruteForceMaxNodes = 200000000; // 200 million
settings.bruteForceTreeDepth = 3;
settings.bruteForceMaxIterations = new BigInteger("50000000"); // 50 million
return settings;
};
private static SolverSettings maxAnalysis() {
SolverSettings settings = new SolverSettings();
settings.bruteForceMaxSolutions = 200000;
settings.bruteForceVariableSolutions = 100000;
settings.bruteForceMaxNodes = 2000000000; // 2000 million
settings.bruteForceTreeDepth = 3;
settings.bruteForceMaxIterations = new BigInteger("500000000"); // 500 million
return settings;
};
}

View File

@ -0,0 +1,210 @@
package minesweeper.solver.settings;
import java.math.BigDecimal;
import java.math.BigInteger;
import minesweeper.structure.Location;
public class SolverSettings {
private final static BigDecimal PROGRESS_CONTRIBUTION = new BigDecimal("0.052");
public enum GuessMethod {
SAFETY_PROGRESS("Safety with progress"),
SECONDARY_SAFETY_PROGRESS("Secondary safety with progress");
public final String name;
private GuessMethod(String name) {
this.name = name;
}
}
protected BigDecimal progressContribution = PROGRESS_CONTRIBUTION;
protected int bruteForceVariableSolutions = 200;
protected int bruteForceMaxSolutions = 400;
protected int bruteForceMaxNodes = 50000;
protected int bruteForceTreeDepth = 50;
protected BigInteger bruteForceMaxIterations = new BigInteger("50000000"); // 50 million
protected boolean doTiebreak = true;
protected int rolloutSolutions = 0;
protected boolean doDomination = true;
protected boolean do5050Check = true;
protected boolean doLongTermSafety = true;
protected boolean testMode = false;
protected Location startLocation;
protected GuessMethod guessMethod = GuessMethod.SECONDARY_SAFETY_PROGRESS;
protected boolean singleThread = false;
private boolean locked;
public SolverSettings lockSettings() {
locked = true;
return this;
}
public SolverSettings setTieBreak(boolean doTiebreak) {
if (!locked) {
this.doTiebreak = doTiebreak;
}
return this;
}
public SolverSettings setDomination(boolean doDomination) {
if (!locked) {
this.doDomination = doDomination;
}
return this;
}
public SolverSettings setRolloutSolutions(int rolloutSolutions) {
if (!locked) {
this.rolloutSolutions = rolloutSolutions;
}
return this;
}
public SolverSettings set5050Check(boolean check) {
if (!locked) {
this.do5050Check = check;
}
return this;
}
public SolverSettings setLongTermSafety(boolean isLongTermSafety) {
if (!locked) {
this.doLongTermSafety = isLongTermSafety;
}
return this;
}
public SolverSettings setTestMode(boolean isTestMode) {
if (!locked) {
this.testMode = isTestMode;
}
return this;
}
/**
* Only use a single thread when running the solver
*/
public SolverSettings setSingleThread(boolean singleThread) {
if (!locked) {
this.singleThread = singleThread;
}
return this;
}
public SolverSettings setGuessMethod(GuessMethod guessMethod) {
if (!locked) {
this.guessMethod = guessMethod;
}
return this;
}
public SolverSettings setStartLocation(Location start) {
// this can be changed
this.startLocation = start;
return this;
}
public SolverSettings setProgressContribution(BigDecimal contribution) {
if (contribution == null) {
this.progressContribution = PROGRESS_CONTRIBUTION;
} else {
this.progressContribution = contribution;
}
return this;
}
public int getBruteForceMaxSolutions() {
return bruteForceMaxSolutions;
}
public int getBruteForceVariableSolutions() {
return bruteForceVariableSolutions;
}
public int getBruteForceMaxNodes() {
return bruteForceMaxNodes;
}
public int getBruteForceTreeDepth() {
return bruteForceTreeDepth;
}
public BigInteger getBruteForceMaxIterations() {
return bruteForceMaxIterations;
}
public boolean isDoTiebreak() {
return doTiebreak;
}
public boolean isDoDomination() {
return doDomination;
}
public boolean isDo5050Check() {
return this.do5050Check;
}
public boolean considerLongTermSafety() {
return this.doLongTermSafety;
}
public boolean isTestMode() {
return testMode;
}
public boolean isSingleThread() {
return singleThread;
}
/*
public boolean isExperimentalScoring() {
return this.experimentalScoring;
}
*/
public int getRolloutSolutions() {
return this.rolloutSolutions;
}
public boolean isLocked() {
return locked;
}
public GuessMethod getGuessMethod() {
return guessMethod;
}
public Location getStartLocation() {
return this.startLocation;
}
public BigDecimal getProgressContribution() {
return this.progressContribution;
}
}

View File

@ -0,0 +1,25 @@
package minesweeper.solver.utility;
import java.math.BigDecimal;
public class BigDecimalCache {
private static BigDecimal[] cache = new BigDecimal[4000];
static {
for (int i=0; i < cache.length; i++) {
cache[i] = BigDecimal.valueOf(i);
}
}
public static BigDecimal get(int i) {
if (i < cache.length) {
return cache[i];
} else {
return BigDecimal.valueOf(i);
}
}
}

View File

@ -0,0 +1,143 @@
package minesweeper.solver.utility;
import java.math.BigInteger;
public class Binomial {
private final int max;
private final PrimeSieve ps;
private final BigInteger[][] binomialLookup;
private final int lookupLimit;
public Binomial(int max, int lookup) {
this.max = max;
ps = new PrimeSieve(this.max);
if (lookup < 10) {
lookup = 10;
}
this.lookupLimit = lookup;
final int lookup2 = lookup / 2;
binomialLookup = new BigInteger[lookup + 1][lookup2 + 1];
for (int total = 1; total <= lookup; total++) {
for (int choose = 0; choose <= total / 2; choose++) {
try {
binomialLookup[total][choose] = generate(choose, total);
//System.out.println("Binomial " + total + " choose " + choose + " is " + binomialLookup[total][choose]);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public BigInteger generate(int k, int n) throws Exception {
if (n == 0 && k == 0) {
return BigInteger.ONE;
}
if (n < 1 || n > max) {
throw new Exception("Binomial: 1 <= n and n <= max required, but n was " + n + " and max was " + max);
}
if (0 > k || k > n) {
throw new Exception("Binomial: 0 <= k and k <= n required, but n was " + n + " and k was " + k);
}
int choose = Math.min(k, n-k);
if (n <= lookupLimit && binomialLookup[n][choose] != null) {
return binomialLookup[n][choose];
} else if (choose < 125) {
return combination(choose, n);
} else {
return combinationLarge(choose, n);
}
}
private static BigInteger combination(int mines, int squares) {
BigInteger top = BigInteger.ONE;
BigInteger bot = BigInteger.ONE;
int range = Math.min(mines, squares - mines);
// calculate the combination.
for (int i = 0; i < range; i++) {
top = top.multiply(BigInteger.valueOf(squares - i));
bot = bot.multiply(BigInteger.valueOf(i+1));
}
BigInteger result = top.divide(bot);
return result;
}
private BigInteger combinationLarge(int k, int n) throws Exception {
if ((k == 0) || (k == n)) return BigInteger.ONE;
int n2 = n / 2;
if (k > n2) {
k = n - k;
}
int nk = n - k;
int rootN = (int) Math.floor(Math.sqrt(n));
BigInteger result = BigInteger.ONE;
for (int prime : ps.getPrimesIterable(2, n)) {
if (prime > nk) {
result = result.multiply(BigInteger.valueOf(prime));
continue;
}
if (prime > n2) {
continue;
}
if (prime > rootN) {
if (n % prime < k % prime) {
result = result.multiply(BigInteger.valueOf(prime));
}
continue;
}
int r = 0, N = n, K = k, p = 1;
while (N > 0) {
r = (N % prime) < (K % prime + r) ? 1 : 0;
if (r == 1)
{
p *= prime;
}
N /= prime;
K /= prime;
}
if (p > 1) {
result = result.multiply(BigInteger.valueOf(p));
}
}
return result;
}
}

View File

@ -0,0 +1,69 @@
package minesweeper.solver.utility;
public class Logger {
/**
* Nothing gets logged
*/
public final static Logger NO_LOGGING = new Logger(Level.NONE);
public enum Level {
DEBUG(1),
INFO(2),
WARN(3),
ERROR(4),
ALWAYS(90),
NONE(99);
private int value;
private Level(int value) {
this.value = value;
}
}
private final String logName;
private final Level logLevel;
private final String prefix;
public Logger(Level level) {
this(level, "");
}
public Logger(Level level, String logName) {
this.logLevel = level;
this.logName = logName;
if (this.logName.isEmpty()) {
this.prefix = " ";
} else {
this.prefix = " " + this.logName + " ";
}
}
public void log(Level level, String format, Object... parms) {
if (level.value < logLevel.value) {
return;
}
String output;
try {
output = String.format(format, parms);
} catch (Exception e) { // if it goes wrong show the unformated information
StringBuilder sb = new StringBuilder();
sb.append(format);
sb.append(" Parms:");
for (Object parm: parms) {
sb.append("[");
sb.append(parm);
sb.append("]");
}
output = sb.toString();
}
System.out.println(level + prefix + output);
}
}

View File

@ -0,0 +1,109 @@
package minesweeper.solver.utility;
import java.util.Iterator;
public class PrimeSieve {
// iterator for prime numbers
private class Primes implements Iterable<Integer>, Iterator<Integer> {
private int index = 0;
private int stop;
private int nextPrime;
private Primes(int start, int stop) {
this.index = start;
this.stop = stop;
this.nextPrime = findNext();
}
@Override
public Iterator<Integer> iterator() {
return this;
}
@Override
public boolean hasNext() {
return (nextPrime != -1);
}
@Override
public Integer next() {
int result = nextPrime;
nextPrime = findNext();
return result;
}
private int findNext() {
int next = -1;
while (index <= stop && next == -1) {
if (!composite[index]) {
next = index;
}
index++;
}
return next;
}
}
private final boolean[] composite;
private final int max;
public PrimeSieve(int n) {
if (n < 2) {
max = 2;
} else {
max = n;
}
composite = new boolean[max + 1];
final int rootN = (int) Math.floor(Math.sqrt(n));
for (int i=2; i < rootN; i++) {
// if this is a prime number (not composite) then sieve the array
if (!composite[i]) {
int index = i + i;
while (index <= max) {
composite[index] = true;
index = index + i;
}
}
}
}
public boolean isPrime(int n) throws Exception {
if (n <= 1 || n > max) {
throw new Exception("Test value " + n + " is out of range 2 - " + max);
}
return !composite[n];
}
protected Iterable<Integer> getPrimesIterable(int start, int stop) throws Exception {
if (start > stop) {
throw new Exception("start " + start + " must be <= to stop " + stop);
}
if (start <= 1 || start > max) {
throw new Exception("Start value " + start + " is out of range 2 - " + max);
}
if (stop <= 1 || stop > max) {
throw new Exception("Stop value " + stop + " is out of range 2 - " + max);
}
return new Primes(start, stop);
}
}

View File

@ -0,0 +1,27 @@
package minesweeper.solver.utility;
public class ProgressMonitor {
private String step;
private int maxProgress;
private int progress;
public void SetMaxProgress(String step, int max) {
this.step = step;
this.maxProgress = max;
this.progress = 0;
}
public int getMaxProgress() {
return this.maxProgress;
}
public void setProgress(int progress) {
this.progress = progress;
}
public int getProgress() {
return this.progress;
}
}

View File

@ -0,0 +1,116 @@
package minesweeper.solver.utility;
import java.text.DecimalFormat;
import java.text.NumberFormat;
public class Timer {
private final static NumberFormat FORMAT = new DecimalFormat("###0.000");
private long start;
private boolean running = false;
private long duration = 0;
private final String label;
public Timer(String label) {
this.label = label;
}
public Timer start() {
if (running) { // if already started then ignore
return this;
}
start = System.nanoTime();
running = true;
return this;
}
public Timer stop() {
if (!running) { // if not running then ignore
return this;
}
running = false;
duration = duration + System.nanoTime() - start;
return this;
}
/**
* Return the duration in milliseconds
* @return
*/
public double getDuration() {
long result;
if (running) {
result = duration + System.nanoTime() - start;
} else {
result = duration;
}
double milli = (double) result / 1000000d;
return milli;
}
static public String humanReadable(long ms) {
long milliseconds = ms % 1000;
long rem = (ms - milliseconds) / 1000;
long seconds = rem % 60;
rem = (rem - seconds) / 60;
long minutes = rem % 60;
long hours = (rem - minutes) /60;
String result;
if (hours > 0) {
result = hours(hours) + " " + minutes(minutes);
} else if (minutes > 0) {
result = minutes(minutes) + " " + seconds(seconds);
} else if (seconds > 0){
result = seconds(seconds);
} else {
result = "< 1 second";
}
return result;
}
static private String hours(long hours) {
if (hours == 1) {
return "1 hour";
} else {
return hours + " hours";
}
}
static private String minutes(long minutes) {
if (minutes == 0) {
return "";
} else if (minutes == 1) {
return "1 minute";
} else {
return minutes + " minutes";
}
}
static private String seconds(long seconds) {
if (seconds == 0) {
return "";
} else if (seconds == 1) {
return "1 second";
} else {
return seconds + " seconds";
}
}
@Override
public String toString() {
return label + " " + FORMAT.format(getDuration()) + " milliseconds";
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry combineaccessrules="false" kind="src" path="/WindowController"/>
<classpathentry combineaccessrules="false" kind="src" path="/MineSweeperSolver"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -0,0 +1,2 @@
/bin/
/build/

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Minesweeper</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.xtext.ui.shared.xtextBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.xtext.ui.shared.xtextNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,11 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.8

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="ASCII"?>
<anttasks:AntTask xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:anttasks="http://org.eclipse.fx.ide.jdt/1.0" buildDirectory="${project}/build">
<deploy>
<application name="Minesweeper"/>
<info/>
</deploy>
<signjar/>
</anttasks:AntTask>

View File

@ -0,0 +1,88 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper;
import javafx.application.Platform;
/**
*
* @author David
*/
public class Animator implements Runnable{
private Thread animator;
private ScreenController scon;
private boolean stopped = false;
private long gameTime = 0;
public Animator(ScreenController scon) {
this.scon = scon;
// start animating the display pane - see the run method
animator = new Thread(this, "Animator");
}
public void start() {
animator.start();
}
public void stop() {
stopped = true;
}
@Override
public void run() {
long timeDiff, sleep, timeDelay;
timeDelay = 50;
gameTime = System.currentTimeMillis();
while (!stopped) {
Platform.runLater(new Runnable() {
@Override public void run() {
scon.updateTime();
scon.moveCheck();
scon.highlightMove();
}
});
// calculate how long the work took
timeDiff = System.currentTimeMillis() - gameTime;
// pause for the ramainder of timeDelay
sleep = timeDelay - timeDiff;
if (sleep < 0) {
sleep = 2;
}
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
System.out.println("interrupted");
}
gameTime += timeDelay;
}
System.out.println("Animator thread stopped");
}
}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane fx:id="window" prefHeight="169.0" prefWidth="240.0" stylesheets="@Screen.css" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="minesweeper.CustomController">
<children>
<TextField fx:id="heightText" alignment="CENTER_RIGHT" layoutX="68.0" layoutY="29.0" prefHeight="25.0" prefWidth="49.0">
<effect>
<DropShadow />
</effect>
</TextField>
<Label layoutX="15.0" layoutY="65.0" text="Width" />
<Label layoutX="13.0" layoutY="33.0" text="Height" />
<Label layoutX="15.0" layoutY="98.0" text="Mines" />
<TextField fx:id="widthText" alignment="CENTER_RIGHT" layoutX="68.0" layoutY="61.0" prefHeight="25.0" prefWidth="49.0">
<effect>
<DropShadow />
</effect>
</TextField>
<TextField fx:id="minesText" alignment="CENTER_RIGHT" layoutX="68.0" layoutY="94.0" prefHeight="25.0" prefWidth="49.0">
<effect>
<DropShadow />
</effect>
</TextField>
<Label layoutX="13.0" layoutY="6.0" prefHeight="17.0" prefWidth="137.0" text="Choose custom settings" />
<Button layoutX="135.0" layoutY="29.0" mnemonicParsing="false" onAction="#handleOkayButton" prefHeight="25.0" prefWidth="78.0" text="Okay" textAlignment="CENTER">
<effect>
<InnerShadow />
</effect>
</Button>
<Button layoutX="135.0" layoutY="61.0" mnemonicParsing="false" onAction="#handleCancelButton" prefHeight="25.0" prefWidth="78.0" text="Cancel">
<effect>
<InnerShadow />
</effect>
</Button>
<Label layoutX="11.0" layoutY="132.0" text="Game #" />
<TextField fx:id="gameCodeText" layoutX="68.0" layoutY="128.0">
<effect>
<DropShadow />
</effect>
</TextField>
</children>
</AnchorPane>

View File

@ -0,0 +1,230 @@
package minesweeper;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import minesweeper.coach.HelperController;
import minesweeper.gamestate.GameStateModel;
import minesweeper.settings.GameSettings;
public class CustomController {
@FXML
private AnchorPane window;
@FXML private TextField heightText;
@FXML private TextField widthText;
@FXML private TextField minesText;
@FXML private TextField gameCodeText;
private Stage stage;
private Scene scene;
private int height;
private int width;
private int mines;
private GameSettings gameSettings;
private long gameCode;
private static CustomController custom;
private boolean wasCancelled = false;
/**
* Initializes the controller class.
*/
/*
@Override
public void initialize(URL url, ResourceBundle rb) {
System.out.println("Entered Custom Control initialize method");
}
*/
@FXML
void initialize() {
System.out.println("Entered Custom Control initialize method");
if (heightText == null) {
System.out.println("heightText is null");
}
}
@FXML
private void handleOkayButton(ActionEvent event) {
this.mines = StringToInteger(minesText.getText(), 1, 20000, this.mines);
this.width = StringToInteger(widthText.getText(), 2, 200, this.width);
this.height = StringToInteger(heightText.getText(), 2, 200, this.height);
try {
this.gameCode = Long.parseLong(gameCodeText.getText());
} catch (NumberFormatException e) {
this.gameCode = 0;
}
if (mines > width * height - 1) {
mines = width * height - 1;
}
gameSettings = GameSettings.create(width, height, mines);
stage.close();
}
@FXML
private void handleCancelButton(ActionEvent event) {
wasCancelled = true;
stage.close();
}
public static CustomController launch(Window owner, GameStateModel game) {
// if we have already created it then show it and return
if (custom != null) {
//custom.stage.show();
custom.wasCancelled = false;
return custom;
}
if (CustomController.class.getResource("Custom.fxml") == null) {
System.out.println("Custom.fxml not found");
}
// create the helper screen
FXMLLoader loader = new FXMLLoader(CustomController.class.getResource("Custom.fxml"));
Parent root = null;
try {
root = (Parent) loader.load();
} catch (IOException ex) {
System.err.println(ex.getMessage());
}
custom = loader.getController();
//helperController = loader.getController();
if (custom == null) {
System.out.println("Custom is null");
}
if (root == null) {
System.out.println("Root is null");
}
custom.scene = new Scene(root);
custom.stage = new Stage();
custom.stage.setScene(custom.scene);
custom.stage.setTitle("Custom board");
custom.stage.getIcons().add(Graphics.getMine());
custom.stage.setResizable(false);
custom.stage.initOwner(owner);
custom.stage.initModality(Modality.WINDOW_MODAL);
custom.width = game.getWidth();
custom.height = game.getHeight();
custom.mines = game.getMines();
custom.widthText.setText(String.valueOf(custom.width));
custom.heightText.setText(String.valueOf(custom.height));
custom.minesText.setText(String.valueOf(custom.mines));
//Stage st = Minesweeper.getStage();
//custom.stage.setX(st.getX()+ st.getWidth());
//custom.stage.setY(st.getY());
custom.stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
@Override
public void handle(WindowEvent event) {
System.out.println("Entered OnCloseRequest handler");
custom.wasCancelled = true;
}
});
return custom;
}
private int StringToInteger(String text, int min, int max, int dflt) {
int val = dflt;
try {
val = Integer.parseInt(text);
} catch (NumberFormatException e) {
}
val = Math.max(val, min);
val = Math.min(val, max);
return val;
}
public static CustomController getCustomController() {
return custom;
}
public Stage getStage() {
return this.stage;
}
public GameSettings getGameSettings() {
return this.gameSettings;
}
public int getMines() {
return this.mines;
}
public int getWidth() {
return this.width;
}
public int getHeight() {
return this.height;
}
public long getGameCode() {
return this.gameCode;
}
public boolean wasCancelled() {
return wasCancelled;
}
}

View File

@ -0,0 +1,83 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper;
import javafx.scene.image.Image;
/**
*
* @author David
*/
public class Graphics {
public static final double SIZE = 50;
private static final Image button;
private static final Image mineBang;
private static final Image flag;
private static final Image[] number = new Image[9];
private static final Image mine;
static {
button = clean(new Image(Graphics.class.getResource("resources/ms_button.png").toExternalForm(), SIZE, SIZE, true, true));
mineBang = clean(new Image(Graphics.class.getResource("resources/ms_mine_bang.png").toExternalForm(), SIZE, SIZE, true, true));
flag = clean(new Image(Graphics.class.getResource("resources/ms_flag.png").toExternalForm(), SIZE, SIZE, true, true));
mine = clean(new Image(Graphics.class.getResource("resources/ms_mine.png").toExternalForm(), SIZE, SIZE, true, true));
number[0] = clean(new Image(Graphics.class.getResource("resources/ms_zero.png").toExternalForm(), SIZE, SIZE, true, true));
number[1] = clean(new Image(Graphics.class.getResource("resources/ms_one.png").toExternalForm(), SIZE, SIZE, true, true));
number[2] = clean(new Image(Graphics.class.getResource("resources/ms_two.png").toExternalForm(), SIZE, SIZE, true, true));
number[3] = clean(new Image(Graphics.class.getResource("resources/ms_three.png").toExternalForm(), SIZE, SIZE, true, true));
number[4] = clean(new Image(Graphics.class.getResource("resources/ms_four.png").toExternalForm(), SIZE, SIZE, true, true));
number[5] = clean(new Image(Graphics.class.getResource("resources/ms_five.png").toExternalForm(), SIZE, SIZE, true, true));
number[6] = clean(new Image(Graphics.class.getResource("resources/ms_six.png").toExternalForm(), SIZE, SIZE, true, true));
number[7] = clean(new Image(Graphics.class.getResource("resources/ms_seven.png").toExternalForm(), SIZE, SIZE, true, true));
number[8] = clean(new Image(Graphics.class.getResource("resources/ms_eight.png").toExternalForm(), SIZE, SIZE, true, true));
}
static public Image getNumber(int c) {
return number[c];
}
static public Image getMineBang() {
return mineBang;
}
static public Image getMine() {
return mine;
}
static public Image getFlag() {
return flag;
}
static public Image getButton() {
return button;
}
// in case we want to do some image manipulation
static private Image clean(Image image) {
return image;
}
}

View File

@ -0,0 +1,247 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper;
import java.io.File;
import java.util.Optional;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import minesweeper.gamestate.GameFactory;
import minesweeper.gamestate.GameStateModel;
import minesweeper.gamestate.GameStateModelViewer;
import minesweeper.gamestate.GameStateReader;
import minesweeper.gamestate.GameStateStandardWith8;
import minesweeper.gamestate.msx.GameStateX;
import minesweeper.gamestate.msx.ScreenScanner;
import minesweeper.settings.GameSettings;
import minesweeper.settings.GameType;
import minesweeper.solver.Solver;
/**
*
* @author David
*/
public class Minesweeper extends Application {
public final static String VERSION = "1.04b";
public static final String TITLE = "Minesweeper coach (" + VERSION + ") Solver version " + Solver.VERSION;
private static GameStateModelViewer myGame;
private static GameSettings gameSettings;
private static Stage myStage = null;
private static ScreenController myController;
@Override
public void start(Stage stage) throws Exception {
myStage = stage;
// this creates a hard game on start-up
createNewGame(ScreenController.DIFFICULTY_EXPERT, GameType.STANDARD, null);
System.out.println("creating root");
FXMLLoader loader = new FXMLLoader(getClass().getResource("Screen.fxml"));
Parent root = (Parent) loader.load();
myController = loader.getController();
System.out.println("root created");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle(TITLE);
stage.getIcons().add(Graphics.getMine());
stage.setX(50);
stage.setY(50);
stage.setWidth(1000);
stage.setHeight(650);
stage.show();
//stage.setResizable(false);
myController.newGame(ScreenController.DIFFICULTY_EXPERT);
stage.setOnHidden(null);
// actions to perform when a close request is received
stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
@Override
public void handle(WindowEvent event) {
System.out.println("Minesweeper window has received a close request");
//event.consume();
myController.kill();
Platform.exit();
}
});
}
// use the difficulty set in the menu system
static public GameStateModel newGame() {
return createNewGame(myController.getDifficulty(), myController.getGameType(), null);
}
// force a difficulty setting
static public GameStateModel createNewGame(int difficulty, GameType gameType, File fileSelected) {
long gameCode = 0;
gameSettings = GameSettings.EXPERT;
switch (difficulty) {
case ScreenController.DIFFICULTY_BEGINNER:
gameSettings = GameSettings.BEGINNER;
break;
case ScreenController.DIFFICULTY_ADVANCED:
gameSettings = GameSettings.ADVANCED;
break;
case ScreenController.DIFFICULTY_EXPERT:
gameSettings = GameSettings.EXPERT;
break;
case ScreenController.DEFER_TO_MINESWEEPERX:
ScreenScanner scanner = new ScreenScanner("Minesweeper X");
if (!scanner.isValid()) {
System.out.println("MinsweeperX not found");
Alert alert = new Alert(AlertType.ERROR, "MinesweeperX can't be found: ensure it is maximised and unobstructed");
Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
return myGame; // old game
}
}
myGame = new GameStateX(scanner);
System.out.println("X = " + myGame.getWidth() + " Y =" + myGame.getHeight());
break;
case ScreenController.DIFFICULTY_FILE:
GameStateModelViewer game;
try {
game = GameStateReader.load(fileSelected);
myGame = game;
} catch (Exception e) {
Alert alert = new Alert(AlertType.ERROR, e.getLocalizedMessage());
Optional<ButtonType> result = alert.showAndWait();
return null;
}
break;
case ScreenController.DIFFICULTY_CUSTOM:
CustomController custom = CustomController.getCustomController();
gameSettings = custom.getGameSettings();
gameCode = custom.getGameCode();
break;
default:
gameSettings = GameSettings.EXPERT;
}
// if we are shadowing minesweeperX then we don't need to do any more
if (difficulty == ScreenController.DEFER_TO_MINESWEEPERX || difficulty == ScreenController.DIFFICULTY_FILE) {
return myGame;
}
myGame = GameFactory.create(gameType, gameSettings, gameCode);
//myGame = new GameStateStandardWith8(gameSettings);
/*
switch (gameType) {
case GameType.:
if (gameCode == 0) {
myGame = new GameStateEasy(gameSettings);
} else {
myGame = new GameStateEasy(gameSettings, gameCode);
}
break;
case ScreenController.GAMETYPE_NORMAL:
if (gameCode == 0) {
myGame = new GameStateStandard(gameSettings);
} else {
myGame = new GameStateStandard(gameSettings, gameCode);
}
break;
case ScreenController.GAMETYPE_HARD:
if (gameCode == 0) {
myGame = new GameStateHard(gameSettings);
} else {
myGame = new GameStateHard(gameSettings, gameCode);
}
break;
default:
if (gameCode == 0) {
myGame = new GameStateStandard(gameSettings);
} else {
myGame = new GameStateStandard(gameSettings, gameCode);
}
}
*/
return myGame;
}
static public GameStateModelViewer getGame() {
return myGame;
}
static public void playGame(GameStateModelViewer gs) {
myGame = gs;
myController.newGame(gs);
}
static public GameSettings getGameSettings() {
return gameSettings;
}
static public Stage getStage() {
return myStage;
}
@Override
public void stop() {
myController.stop();
}
/**
* The main() method is ignored in correctly deployed JavaFX application.
* main() serves only as fallback in case the application can not be
* launched through deployment artifacts, e.g., in IDEs with limited FX
* support. NetBeans ignores main().
*
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}

View File

@ -0,0 +1,56 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper;
import javafx.scene.Node;
/**
*
* @author David
*/
public class Rotator implements Runnable {
private Thread rotator;
Node object;
public Rotator(Node object) {
this.object = object;
rotator = new Thread(this, "Rotator");
}
public void start() {
rotator.start();
}
@Override
public void run() {
for (int i=0; i < 360; i=i+20) {
this.object.setRotate(i);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
System.out.println("interrupted");
}
}
this.object.setRotate(0);
}
}

View File

@ -0,0 +1,47 @@
/*
Document : Screen
Created on : 29-Nov-2013, 15:48:32
Author : David
Description:
Purpose of the stylesheet follows.
*/
.root {
-fx-background-color: linear-gradient(GREEN, DARKGREEN);
-fx-background-color: linear-gradient(#61a2b1, #2A5058);
}
.menu-bar {
-fx-background-color: linear-gradient(#61a2b1, #2A5058);
}
#myPane {
-fx-border-color: black;
-fx-border-width: 1;
-fx-border-style: solid outside;
}
#scoreLabel {
-fx-border-color: black;
-fx-border-width: 1;
-fx-border-style: solid outside;
-fx-border-radius: 15;
}
#timeLabel {
-fx-border-color: black;
-fx-border-width: 1;
-fx-border-style: solid outside;
-fx-border-radius: 15;
}

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.net.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.shape.*?>
<?import javafx.scene.text.*?>
<?scenebuilder-stylesheet Screen.css?>
<AnchorPane id="AnchorPane" fx:id="window" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="424.0" prefWidth="591.0" style="-fx-background-color: green;" styleClass="mainFxmlClass" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="minesweeper.ScreenController">
<children>
<Pane fx:id="myPane" minWidth="-1.0" onMousePressed="#mouseDown" onMouseReleased="#mouseUp" pickOnBounds="true" prefHeight="261.0" prefWidth="455.0" style="&#10;" AnchorPane.bottomAnchor="35.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="70.0" />
<MenuBar prefWidth="455.0" style="-fx-background-color: white;" stylesheets="@Screen.css" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<menus>
<Menu mnemonicParsing="false" text="Game">
<items>
<RadioMenuItem id="easyMode" fx:id="easyMode" mnemonicParsing="false" onAction="#handleDifficulty" text="Easy (9x9x10)">
<toggleGroup>
<ToggleGroup fx:id="Difficulty" />
</toggleGroup>
</RadioMenuItem>
<RadioMenuItem fx:id="mediumMode" mnemonicParsing="false" onAction="#handleDifficulty" text="Medium (16x16x40)" toggleGroup="$Difficulty" />
<RadioMenuItem fx:id="hardMode" mnemonicParsing="false" onAction="#handleDifficulty" selected="true" text="Hard (30x16x99)" toggleGroup="$Difficulty" />
<RadioMenuItem fx:id="msxMode" mnemonicParsing="false" onAction="#handleDifficulty" text="MinesweeperX" toggleGroup="$Difficulty" />
<RadioMenuItem fx:id="fromFile" mnemonicParsing="false" onAction="#handleDifficulty" text="From file..." toggleGroup="$Difficulty" />
<RadioMenuItem fx:id="customMode" mnemonicParsing="false" onAction="#handleDifficulty" text="Custom..." toggleGroup="$Difficulty" />
<SeparatorMenuItem mnemonicParsing="false" text="Game type" />
<Menu mnemonicParsing="false" text="Game type">
<items>
<RadioMenuItem fx:id="gameTypeEasy" mnemonicParsing="false" onAction="#handleGameType" text="Easy (zero on start)">
<toggleGroup>
<ToggleGroup fx:id="gameType" />
</toggleGroup>
</RadioMenuItem>
<RadioMenuItem fx:id="gameTypeNormal" mnemonicParsing="false" onAction="#handleGameType" selected="true" text="Standard (safe start)" toggleGroup="$gameType" />
<RadioMenuItem fx:id="gameTypeHard" mnemonicParsing="false" onAction="#handleGameType" text="Hard (unsafe start)" toggleGroup="$gameType" />
</items>
</Menu>
<SeparatorMenuItem mnemonicParsing="false" />
<MenuItem fx:id="saveBoard" mnemonicParsing="false" onAction="#saveBoardHandle" text="Save board..." />
<SeparatorMenuItem mnemonicParsing="false" />
<MenuItem mnemonicParsing="false" onAction="#exitGameHandle" text="Exit game" />
</items>
</Menu>
<Menu mnemonicParsing="false" text="Coach">
<items>
<CheckMenuItem fx:id="showMove" mnemonicParsing="false" selected="true" text="Show best move" />
<CheckMenuItem fx:id="showTooltips" mnemonicParsing="false" selected="true" text="Show tooltips" />
<CheckMenuItem fx:id="acceptGuess" mnemonicParsing="false" selected="false" text="Accept guesses" />
<CheckMenuItem fx:id="showMines" mnemonicParsing="false" onAction="#showMinesToggled" text="Show mines" />
<CheckMenuItem fx:id="dumpTree" mnemonicParsing="false" onAction="#dumpTreeToggled" text="Dump probability tree" />
<CheckMenuItem fx:id="probHeatMap" disable="true" mnemonicParsing="false" onAction="#probHeatMapToggled" text="Probability heat map" />
</items>
</Menu>
<Menu mnemonicParsing="false" text="Options">
<items>
<MenuItem mnemonicParsing="false" onAction="#handleCopyToClipboard" text="Copy seed to clipboard" />
<Menu mnemonicParsing="false" text="Play style">
<items>
<RadioMenuItem fx:id="psFlagging" mnemonicParsing="false" onAction="#setPlayStyle" selected="true" text="Flagging">
<toggleGroup>
<ToggleGroup fx:id="playStyle" />
</toggleGroup>
</RadioMenuItem>
<RadioMenuItem fx:id="psNoFlagging" mnemonicParsing="false" onAction="#setPlayStyle" text="No flagging" toggleGroup="$playStyle" />
<RadioMenuItem fx:id="psEfficiency" mnemonicParsing="false" onAction="#setPlayStyle" text="Efficiency" toggleGroup="$playStyle" />
<RadioMenuItem fx:id="psNfEfficiency" mnemonicParsing="false" onAction="#setPlayStyle" text="No flag efficiency" toggleGroup="$playStyle" />
</items>
</Menu>
<Menu mnemonicParsing="false" text="Guessing method">
<items>
<RadioMenuItem fx:id="standardGuess" mnemonicParsing="false" text="Safety with progress">
<toggleGroup>
<ToggleGroup fx:id="GuessType" />
</toggleGroup>
</RadioMenuItem>
<RadioMenuItem fx:id="secondarySafetyGuess" mnemonicParsing="false" selected="true" text="Secondary safety with progress" toggleGroup="$GuessType" />
</items>
</Menu>
<Menu mnemonicParsing="false" text="Random numbers">
<items>
<RadioMenuItem fx:id="rngJava" mnemonicParsing="false" selected="true" text="Java random numbers">
<toggleGroup>
<ToggleGroup fx:id="RNG" />
</toggleGroup>
</RadioMenuItem>
<RadioMenuItem fx:id="rngKiss64" mnemonicParsing="false" text="KISS64 random numbers" toggleGroup="$RNG" />
</items>
</Menu>
<Menu mnemonicParsing="false" text="Brute force analysis">
<items>
<RadioMenuItem fx:id="sol400" mnemonicParsing="false" text="400 solutions">
<toggleGroup>
<ToggleGroup fx:id="BFDA" />
</toggleGroup>
</RadioMenuItem>
<RadioMenuItem fx:id="sol4000" mnemonicParsing="false" selected="true" text="4000 solutions" toggleGroup="$BFDA" />
</items>
</Menu>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Bulk run">
<items>
<MenuItem mnemonicParsing="false" onAction="#handleNewBulkRun" text="New bulk run..." />
</items>
</Menu>
</menus>
</MenuBar>
<Label fx:id="scoreLabel" alignment="TOP_CENTER" layoutY="28.0" minHeight="24.0" prefHeight="37.0" prefWidth="56.0" style="-fx-border-color: Black; -fx-border-radius: 15; -fx-border-style: solid outside;" text="000" textAlignment="LEFT" textFill="BLACK" AnchorPane.leftAnchor="10.0">
<font>
<Font name="System Bold" size="24.0" fx:id="x1" />
</font>
</Label>
<Label fx:id="timeLabel" alignment="TOP_CENTER" contentDisplay="LEFT" font="$x1" layoutY="28.0" prefHeight="37.0" prefWidth="56.0" style="-fx-border-color: Black; -fx-border-style: solid outside; -fx-border-radius: 15;" text="000" textAlignment="LEFT" textFill="BLACK" AnchorPane.rightAnchor="10.0" />
<Button id="button" fx:id="automateButton" alignment="TOP_CENTER" minWidth="-Infinity" onAction="#handleAutomateButton" prefWidth="100.0" text="Automate" textAlignment="CENTER" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="10.0" />
<Circle fx:id="highlight" centerX="50.0" centerY="50.0" fill="#fff500" layoutX="281.0" layoutY="-11.0" mouseTransparent="true" opacity="0.49" radius="26.25" stroke="BLACK" strokeType="INSIDE" strokeWidth="4.0" />
<HBox alignment="CENTER" layoutX="10.0" layoutY="28.0" minHeight="-Infinity" prefHeight="37.0" prefWidth="571.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="28.0">
<children>
<Button fx:id="newGameButton" alignment="TOP_CENTER" minWidth="-Infinity" onAction="#handleNewGameButton" prefHeight="25.0" prefWidth="100.0" text="New Game" textAlignment="CENTER" />
</children>
</HBox>
</children>
<stylesheets>
<URL value="@Screen.css" />
</stylesheets>
</AnchorPane>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,253 @@
package minesweeper.bulk;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.Random;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import minesweeper.Graphics;
import minesweeper.gamestate.GameStateModel;
import minesweeper.random.DefaultRNG;
import minesweeper.settings.GameSettings;
import minesweeper.settings.GameType;
import minesweeper.solver.settings.SolverSettings;
import minesweeper.structure.Location;
public class BulkController {
private final static DecimalFormat PERCENT = new DecimalFormat("#0.000%");
@FXML private AnchorPane window;
@FXML private TextField gameCount;
@FXML private TextField gameSeed;
@FXML private Label winPercentage;
@FXML private ProgressBar progressRun;
@FXML private Label progressRunLabel;
@FXML private TextField startLocX;
@FXML private TextField startLocY;
@FXML private CheckBox showGames;
@FXML private CheckBox winsOnly;
private Stage stage;
private Scene scene;
private int gamesMax;
private long gameGenerator;
private GameSettings gameSettings;
private GameType gameType;
private Location startLocation;
private SolverSettings preferences;
//private ResultsController resultsController;
private BulkRunner bulkRunner;
private boolean wasCancelled = false;
@FXML
void initialize() {
System.out.println("Entered Bulk Screen initialize method");
}
@FXML
private void handleNewSeedButton(ActionEvent event) {
gameSeed.setText(String.valueOf(new Random().nextLong()));
}
@FXML
private void handleOkayButton(ActionEvent event) {
System.out.println("handleOkayButton method entered");
if (bulkRunner != null && !bulkRunner.isFinished()) {
System.out.println("Previous bulk run still running");
return;
}
if (gameCount.getText().trim().isEmpty()) {
gamesMax = 1000;
} else {
try {
gamesMax = Integer.parseInt(gameCount.getText().trim());
if (gamesMax < 1) {
gamesMax = 1000;
}
} catch (NumberFormatException e) {
gamesMax = 1000;
}
}
gameCount.setText(String.valueOf(gamesMax));
if (gameSeed.getText().trim().isEmpty()) {
gameGenerator = new Random().nextLong();
} else {
try {
gameGenerator = Long.parseLong(gameSeed.getText().trim());
} catch (NumberFormatException e) {
gameGenerator = new Random().nextLong();
//gameSeed.setText("");
}
}
startLocation = null;
if (!startLocX.getText().trim().isEmpty() && !startLocY.getText().trim().isEmpty()) {
try {
int startX = Integer.parseInt(startLocX.getText().trim());
int startY = Integer.parseInt(startLocY.getText().trim());
if (startX >= 0 && startX < gameSettings.width && startY >= 0 && startY < gameSettings.height) {
startLocation = new Location(startX, startY);
System.out.println("Start location set to " + startLocation.toString());
}
} catch (NumberFormatException e) {
}
}
gameSeed.setText(String.valueOf(gameGenerator));
bulkRunner = new BulkRunner(this, gamesMax, gameSettings, gameType, gameGenerator, startLocation, showGames.isSelected(), winsOnly.isSelected(), preferences);
new Thread(bulkRunner, "Bulk Run").start();
}
@FXML
private void handleCancelButton(ActionEvent event) {
System.out.println("handleCancelButton method entered");
stage.close();
}
public static BulkController launch(Window owner, GameSettings gameSettings, GameType gameType, SolverSettings preferences ) {
if (BulkController.class.getResource("BulkScreen.fxml") == null) {
System.out.println("BulkScreen.fxml not found");
}
// create the bulk runner screen
FXMLLoader loader = new FXMLLoader(BulkController.class.getResource("BulkScreen.fxml"));
Parent root = null;
try {
root = (Parent) loader.load();
} catch (IOException ex) {
System.err.println(ex.getMessage());
}
BulkController custom = loader.getController();
if (custom == null) {
System.out.println("Custom is null");
}
if (root == null) {
System.out.println("Root is null");
}
custom.gameSettings = gameSettings;
custom.gameType = gameType;
custom.preferences = preferences;
custom.scene = new Scene(root);
custom.stage = new Stage();
custom.stage.setScene(custom.scene);
custom.stage.setTitle("Bulk run - " + gameSettings.description() + ", " + gameType.name + ", " + DefaultRNG.getRNG(1).shortname());
custom.stage.getIcons().add(Graphics.getMine());
custom.stage.setResizable(false);
custom.stage.initOwner(owner);
custom.stage.initModality(Modality.WINDOW_MODAL);
custom.stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
@Override
public void handle(WindowEvent event) {
System.out.println("Entered OnCloseRequest handler");
if (custom.bulkRunner != null) {
custom.bulkRunner.forceStop();
}
System.gc();
}
});
custom.gameCount.setText("1000");
custom.progressRun.setProgress(0d);
custom.progressRunLabel.setText("");
//custom.resultsController = ResultsController.launch(null, gameSettings, gameType);
custom.getStage().show();
return custom;
}
public Stage getStage() {
return this.stage;
}
public boolean wasCancelled() {
return wasCancelled;
}
public void update(int steps, int maxSteps, int wins) {
Platform.runLater(new Runnable() {
@Override public void run() {
double prog = (double) steps / (double) maxSteps;
progressRun.setProgress(prog);
progressRunLabel.setText(steps + "(" + wins + ") /" + maxSteps);
double winPerc = (double) wins / (double) steps;
double err = Math.sqrt(winPerc * ( 1- winPerc) / (double) steps) * 1.9599d;
winPercentage.setText(PERCENT.format(winPerc) + " +/- " + PERCENT.format(err));
}
});
}
/*
public void storeResult(GameStateModel gs) {
resultsController.update(gs);
}
*/
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="554.0" prefWidth="446.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="minesweeper.bulk.ResultsController">
<children>
<Button alignment="CENTER" layoutX="379.0" layoutY="401.0" mnemonicParsing="false" onAction="#handlePlayButton" prefHeight="25.0" prefWidth="84.0" text="Play game" AnchorPane.bottomAnchor="5.0" AnchorPane.rightAnchor="20.0" />
<TableView fx:id="resultsTable" prefHeight="414.0" prefWidth="550.0" AnchorPane.bottomAnchor="40.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columns>
<TableColumn fx:id="columnCount" prefWidth="79.0" style="-fx-alignment: CENTER_RIGHT;" text="Number" />
<TableColumn fx:id="columnSeed" prefWidth="165.0" style="-fx-alignment: CENTER;" text="Seed" />
<TableColumn fx:id="columnComplete" prefWidth="99.0" style="-fx-alignment: CENTER_RIGHT;" text="Tiles revealed" />
<TableColumn fx:id="columnResult" prefWidth="88.0" style="-fx-alignment: CENTER;" text="Result" />
</columns>
</TableView>
</children>
</AnchorPane>

View File

@ -0,0 +1,154 @@
package minesweeper.bulk;
import minesweeper.gamestate.GameFactory;
import minesweeper.gamestate.GameStateModel;
import minesweeper.random.DefaultRNG;
import minesweeper.random.RNG;
import minesweeper.settings.GameSettings;
import minesweeper.settings.GameType;
import minesweeper.solver.Solver;
import minesweeper.solver.settings.SolverSettings;
import minesweeper.structure.Action;
import minesweeper.structure.Location;
public class BulkRunner implements Runnable {
private boolean stop = false;
private final int maxSteps;
private final long seed;
private final BulkController controller;
private final GameSettings gameSettings;
private final GameType gameType;
private final Location startLocation;
private final SolverSettings preferences;
//private final Random seeder;
private int steps = 0;
private int wins = 0;
private final RNG seeder;
private ResultsController resultsController;
private boolean showGames;
private boolean winsOnly;
public BulkRunner(BulkController controller, int iterations, GameSettings gameSettings, GameType gameType,
long seed, Location startLocation, boolean showGames, boolean winsOnly, SolverSettings preferences) {
maxSteps = iterations;
this.seed = seed;
this.controller = controller;
this.gameSettings = gameSettings;
this.gameType = gameType;
this.startLocation = startLocation;
this.seeder = DefaultRNG.getRNG(seed);
this.showGames = showGames;
this.winsOnly = winsOnly;
this.preferences = preferences;
if (startLocation != null) {
this.preferences.setStartLocation(startLocation);
}
if (showGames) {
resultsController = ResultsController.launch(null, gameSettings, gameType);
}
}
@Override
public void run() {
System.out.println("At BulkRunner run method");
while (!stop && steps < maxSteps) {
steps++;
GameStateModel gs = GameFactory.create(gameType, gameSettings, seeder.random(0));
Solver solver = new Solver(gs, preferences, false);
boolean win = playGame(gs, solver);
if (win) {
wins++;
}
if (showGames && (win || !win && !winsOnly)) {
if (!resultsController.update(gs)) { // this returns false if the window has been closed
showGames = false;
resultsController = null;
System.out.println("Results window has been closed... will no longer send data to it");
}
}
controller.update(steps, maxSteps, wins);
}
stop = true;
System.out.println("BulkRunner run method ending");
}
private boolean playGame(GameStateModel gs, Solver solver) {
int state;
play: while (true) {
Action[] moves;
try {
solver.start();
moves = solver.getResult();
} catch (Exception e) {
System.out.println("Game " + gs.showGameKey() + " has thrown an exception!");
stop = true;
return false;
}
if (moves.length == 0) {
System.err.println("No moves returned by the solver for game " + gs.showGameKey());
stop = true;
return false;
}
// play all the moves until all done, or the game is won or lost
for (int i=0; i < moves.length; i++) {
boolean result = gs.doAction(moves[i]);
state = gs.getGameState();
if (state == GameStateModel.LOST || state == GameStateModel.WON) {
break play;
}
}
}
if (state == GameStateModel.LOST) {
return false;
} else {
return true;
}
}
public void forceStop() {
System.out.println("Bulk run being requested to stop");
stop = true;
}
public boolean isFinished() {
return stop;
}
}

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="278.0" prefWidth="403.0" style="-fx-background-color: green;" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="minesweeper.bulk.BulkController">
<children>
<Button layoutX="317.0" layoutY="242.0" mnemonicParsing="false" onAction="#handleOkayButton" prefHeight="25.0" prefWidth="72.0" text="Start" />
<ProgressBar fx:id="progressRun" layoutX="96.0" layoutY="171.0" prefHeight="25.0" prefWidth="293.0" progress="0.21" style="-fx-control-inner-background: palegreen; -fx-accent: darkgreen;" />
<TextField fx:id="gameCount" layoutX="95.0" layoutY="18.0" style="-fx-background-color: silver;" />
<Label layoutX="15.0" layoutY="132.0" prefHeight="25.0" prefWidth="72.0" text="Win rate">
<font>
<Font size="14.0" />
</font></Label>
<Label layoutX="14.0" layoutY="18.0" prefHeight="25.0" prefWidth="72.0" text="Games" textAlignment="RIGHT">
<font>
<Font size="14.0" />
</font>
</Label>
<Label layoutX="15.0" layoutY="171.0" prefHeight="25.0" prefWidth="72.0" text="Progress" textAlignment="RIGHT">
<font>
<Font size="14.0" />
</font>
</Label>
<Label fx:id="progressRunLabel" alignment="CENTER" layoutX="112.0" layoutY="171.0" prefHeight="25.0" prefWidth="267.0" text="590/1000">
<font>
<Font name="System Bold" size="16.0" />
</font>
</Label>
<Label fx:id="winPercentage" layoutX="96.0" layoutY="131.0" prefHeight="27.0" prefWidth="293.0">
<font>
<Font name="System Bold" size="18.0" />
</font>
</Label>
<Label layoutX="14.0" layoutY="57.0" prefHeight="25.0" prefWidth="72.0" text="Seed" textAlignment="RIGHT">
<font>
<Font size="14.0" />
</font>
</Label>
<TextField fx:id="gameSeed" layoutX="95.0" layoutY="57.0" style="-fx-background-color: silver;" />
<Button layoutX="253.0" layoutY="57.0" mnemonicParsing="false" onAction="#handleNewSeedButton" prefHeight="25.0" prefWidth="72.0" text="New seed" />
<Label layoutX="14.0" layoutY="100.0" prefHeight="25.0" prefWidth="92.0" text="Start Loc: X=" textAlignment="RIGHT">
<font>
<Font size="14.0" />
</font>
</Label>
<TextField fx:id="startLocX" layoutX="95.0" layoutY="100.0" prefHeight="25.0" prefWidth="43.0" style="-fx-background-color: silver;" />
<Label layoutX="145.0" layoutY="100.0" prefHeight="25.0" prefWidth="29.0" text="Y=" textAlignment="RIGHT">
<font>
<Font size="14.0" />
</font>
</Label>
<TextField fx:id="startLocY" layoutX="165.0" layoutY="100.0" prefHeight="25.0" prefWidth="43.0" style="-fx-background-color: silver;" />
<CheckBox fx:id="showGames" layoutX="99.0" layoutY="211.0" mnemonicParsing="false" text="Show game results">
<font>
<Font size="14.0" />
</font>
</CheckBox>
<CheckBox fx:id="winsOnly" layoutX="99.0" layoutY="244.0" mnemonicParsing="false" text="Wins only">
<font>
<Font size="14.0" />
</font>
</CheckBox>
</children>
</AnchorPane>

View File

@ -0,0 +1,178 @@
package minesweeper.bulk;
import java.io.IOException;
import java.text.DecimalFormat;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TableView.TableViewSelectionModel;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import minesweeper.Graphics;
import minesweeper.Minesweeper;
import minesweeper.gamestate.GameFactory;
import minesweeper.gamestate.GameStateModel;
import minesweeper.gamestate.GameStateModelViewer;
import minesweeper.settings.GameSettings;
import minesweeper.settings.GameType;
public class ResultsController {
private Stage stage;
private Scene scene;
@FXML private TableView<TableData> resultsTable;
@FXML private TableColumn<TableData, Integer> columnCount;
@FXML private TableColumn<TableData, Long> columnSeed;
@FXML private TableColumn<TableData, Integer> columnComplete;
@FXML private TableColumn<TableData, String> columnResult;
private GameSettings gameSettings;
private GameType gameType;
private int count = 0;
private boolean closed = false;
private ObservableList<TableData> items = FXCollections.observableArrayList();
@FXML
void initialize() {
System.out.println("Entered Bulk Result Screen initialize method");
}
@FXML
private void handlePlayButton(ActionEvent event) {
System.out.println("handlePlayButton method entered");
TableData selected = resultsTable.getSelectionModel().getSelectedItem();
long seed = selected.seedProperty().longValue();
System.out.println("Selected seed " + seed);
GameStateModelViewer gs = GameFactory.create(gameType, gameSettings, seed);
Minesweeper.playGame(gs);
}
public static ResultsController launch(Window owner, GameSettings gameSettings, GameType gameType) {
if (ResultsController.class.getResource("BulkScreen.fxml") == null) {
System.out.println("BulkScreen.fxml not found");
}
// create the bulk runner screen
FXMLLoader loader = new FXMLLoader(ResultsController.class.getResource("BulkResults.fxml"));
Parent root = null;
try {
root = (Parent) loader.load();
} catch (IOException ex) {
System.err.println(ex.getMessage());
}
ResultsController custom = loader.getController();
if (custom == null) {
System.out.println("Custom is null");
}
if (root == null) {
System.out.println("Root is null");
}
custom.scene = new Scene(root);
custom.stage = new Stage();
custom.stage.setScene(custom.scene);
custom.stage.setTitle("Bulk run results");
custom.stage.getIcons().add(Graphics.getMine());
custom.stage.setResizable(false);
custom.stage.initOwner(owner);
custom.stage.initModality(Modality.WINDOW_MODAL);
custom.stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
@Override
public void handle(WindowEvent event) {
System.out.println("Entered OnCloseRequest handler for Bulk Results Screen");
custom.closed = true;
custom.items.clear(); // clear down the table to encourage the items to be garbage collected
}
});
custom.resultsTable.setItems(custom.items);
custom.resultsTable.getSelectionModel();
custom.columnCount.setCellValueFactory(new PropertyValueFactory<TableData, Integer>("count"));
custom.columnSeed.setCellValueFactory(new PropertyValueFactory<TableData, Long>("seed"));
custom.columnComplete.setCellValueFactory(new PropertyValueFactory<TableData, Integer>("complete"));
custom.columnResult.setCellValueFactory(new PropertyValueFactory<TableData, String>("result"));
custom.getStage().show();
custom.gameType = gameType;
custom.gameSettings = gameSettings;
//System.out.println("Columns = " + custom.resultsTable.getColumns().size());
return custom;
}
public Stage getStage() {
return this.stage;
}
public boolean update(GameStateModel gs) {
if (closed) { // if the window has been closed then let anyone who calls know
return false;
}
count++;
final TableData td = new TableData(count, gs);
Platform.runLater(new Runnable() {
@Override public void run() {
items.add(td);
}
});
return true;
}
}

View File

@ -0,0 +1,74 @@
package minesweeper.bulk;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.FloatProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleFloatProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import minesweeper.gamestate.GameStateModel;
public class TableData {
private final static BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);
private final static DecimalFormat PERCENT = new DecimalFormat("#0.000%");
private final IntegerProperty count;
private final LongProperty seed;
private final IntegerProperty complete;
private final StringProperty result;
public TableData(int count, GameStateModel gs) {
this.count = new SimpleIntegerProperty(count);
seed = new SimpleLongProperty(gs.getSeed());
/*
BigDecimal areaToReveal = BigDecimal.valueOf(gs.getx() * gs.gety() - gs.getMines());
BigDecimal areaLeftToReveal = BigDecimal.valueOf(gs.getHidden() - gs.getMines());
BigDecimal percentageToDo = ONE_HUNDRED.multiply(BigDecimal.ONE.subtract(areaLeftToReveal.divide(areaToReveal, 6, RoundingMode.HALF_UP))).setScale(2, RoundingMode.HALF_UP);
System.out.println(percentageToDo + " " + (gs.getGameState() == GameStateModel.WON) + " " + areaLeftToReveal);
*/
complete = new SimpleIntegerProperty(gs.getWidth() * gs.getHeight() - gs.getHidden());
if (gs.getGameState() == GameStateModel.WON) {
result = new SimpleStringProperty("Won");
} else {
result = new SimpleStringProperty("Lost");
}
}
public LongProperty seedProperty() {
return seed;
}
public IntegerProperty completeProperty() {
return complete;
}
public StringProperty resultProperty() {
return result;
}
public IntegerProperty countProperty() {
return count;
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.net.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="300.0" prefWidth="477.0" styleClass="mainFxmlClass" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="minesweeper.coach.HelperController">
<children>
<TextArea fx:id="text" editable="false" focusTraversable="false" prefHeight="241.0" prefWidth="420.0" text="" wrapText="true" AnchorPane.bottomAnchor="25.0" AnchorPane.leftAnchor="25.0" AnchorPane.rightAnchor="25.0" AnchorPane.topAnchor="25.0" />
</children>
<stylesheets>
<URL value="@helper.css" />
</stylesheets>
</AnchorPane>

View File

@ -0,0 +1,177 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package minesweeper.coach;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import minesweeper.Graphics;
import minesweeper.Minesweeper;
import minesweeper.solver.Solver;
import minesweeper.solver.coach.CoachModel;
/**
* FXML Controller class
*
* @author David
*/
public class HelperController implements CoachModel, Initializable {
private static final Background BG_Red = new Background(new BackgroundFill(Color.RED, null, null));
private static final Background BG_Green = new Background(new BackgroundFill(Color.GREEN, null, null));
private static final Background BG_Orange = new Background(new BackgroundFill(Color.ORANGE, null, null));
@FXML
private TextArea text;
//private HelperController helperController;
public Stage stage;
public Scene scene;
/**
* Initializes the controller class.
*/
@Override
public void initialize(URL url, ResourceBundle rb) {
System.out.println("Entered Helper Control initialize method");
// TODO
}
/*
public TextArea getTextArea() {
return text;
}
*/
public void kill() {
stage.close();
System.out.println("Killing the Helper Control Object");
}
public static HelperController launch() {
// create the helper screen
FXMLLoader loader = new FXMLLoader(HelperController.class.getResource("Helper.fxml"));
Parent root = null;
try {
root = (Parent) loader.load();
} catch (IOException ex) {
System.err.println(ex.getMessage());
}
HelperController coach = loader.getController();
//helperController = loader.getController();
coach.scene = new Scene(root);
coach.stage = new Stage();
coach.stage.setScene(coach.scene);
coach.stage.setTitle(Minesweeper.TITLE + " solver " + Solver.VERSION);
coach.stage.getIcons().add(Graphics.getFlag());
coach.writeLine("Minesweeper coach dedicated to Annie");
//((AnchorPane) scene.getRoot()).setBackground(BG_Green);
coach.setOkay();
Stage st = Minesweeper.getStage();
coach.stage.setX(st.getX()+ st.getWidth());
coach.stage.setY(st.getY());
coach.align();
coach.stage.show();
return coach;
}
final public void align() {
Stage st = Minesweeper.getStage();
if (st != null) {
stage.setX(st.getX()+ st.getWidth());
stage.setY(st.getY());
}
stage.setHeight(500);
}
@Override
final public void setOkay() {
setColor(BG_Green);
}
@Override
final public void setWarn() {
setColor(BG_Orange);
}
@Override
final public void setError() {
setColor(BG_Red);
}
private void setColor(Background c) {
((AnchorPane) scene.getRoot()).setBackground(c);
}
@Override
public void clearScreen() {
Platform.runLater(new Runnable() {
@Override public void run() {
text.clear();
}
});
}
@Override
public void writeLine(String line) {
Platform.runLater(new Runnable() {
@Override public void run() {
String textLine = text.getText();
if (textLine.equals("")) {
textLine = line;
} else {
textLine = textLine + "\n" + line;
}
text.setText(textLine);
}
});
}
@Override
public boolean analyseFlags() {
return true;
}
}

View File

@ -0,0 +1,7 @@
/*
* Empty Stylesheet file.
*/
.mainFxmlClass {
}

View File

@ -0,0 +1,160 @@
package minesweeper.gamestate.msx;
import minesweeper.gamestate.GameStateModel;
import minesweeper.gamestate.GameStateModelViewer;
import minesweeper.settings.GameSettings;
import minesweeper.structure.Location;
public class GameStateX extends GameStateModelViewer {
private ScreenScanner scanner;
public GameStateX(ScreenScanner scanner) {
super(GameSettings.create(scanner.getColumns(), scanner.getRows(), scanner.getMines()));
this.scanner = scanner;
doAutoComplete = false; // minesweeperX will do this itself
}
@Override
protected void placeFlagHandle(Location m) {
scanner.flag(m.x, m.y);
// give it time to set the flag
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
scanner.updateField();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
protected boolean clearSquareHitMine(Location m) {
scanner.clear(m.x, m.y);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
scanner.updateField();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// since the click could have expanded more areas we need to allow for that
for (int x=0; x < scanner.getColumns(); x++) {
for (int y=0; y < scanner.getRows(); y++) {
if (scanner.getValue(x, y) != ScreenScanner.HIDDEN && scanner.getValue(x, y) != ScreenScanner.FLAG) {
setRevealed(x,y);
}
// if a flag has been revealed by minesweeperX then place that in the model (this happens at the end of a game)
if (scanner.getValue(x, y) == ScreenScanner.FLAG && this.query(new Location(x,y)) == HIDDEN) {
setFlag(x,y);
}
}
}
return scanner.isGameLost();
}
@Override
protected void startHandle(Location m) {
}
@Override
protected int queryHandle(int x, int y) {
int value = scanner.getValue(x, y);
if (value == ScreenScanner.HIDDEN) {
System.out.println("value at (" + x + "," + y + ") is hidden and yet queried!");
}
if (value == ScreenScanner.FLAG) {
System.out.println("value at (" + x + "," + y + ") is a flag and yet queried!");
}
if (value == ScreenScanner.HIDDEN) {
return GameStateModel.HIDDEN;
} else if (value == ScreenScanner.EMPTY) {
return 0;
} else if (value == ScreenScanner.FLAG) {
return GameStateModel.HIDDEN;
} else if (value == ScreenScanner.BOMB) {
return GameStateModel.MINE;
} else if (value == ScreenScanner.EXPLODED_BOMB) {
return GameStateModel.EXPLODED_MINE;
} else {
return value;
}
}
@Override
/**
* No privileged access since MinesweeperX can't show it
*/
public int privilegedQuery(Location m, boolean showMines) {
return query(m);
}
@Override
protected boolean clearSurroundHandle(Location m) {
scanner.clearAll(m.x, m.y);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
scanner.updateField();
} catch (Exception e) {
e.printStackTrace();
}
// since the click could have expanded more areas we need to allow for that
for (int x=0; x < scanner.getColumns(); x++) {
for (int y=0; y < scanner.getRows(); y++) {
if (scanner.getValue(x, y) != ScreenScanner.HIDDEN && scanner.getValue(x, y) != ScreenScanner.FLAG) {
setRevealed(x,y);
}
// if a flag has been revealed by minesweeperX then place that in the model (this happens at the end of a game)
if (scanner.getValue(x, y) == ScreenScanner.FLAG && this.query(new Location(x,y)) == HIDDEN) {
setFlag(x,y);
}
}
}
return true;
}
@Override
public String showGameKey() {
return "Minesweeper X";
}
}

View File

@ -0,0 +1,17 @@
package minesweeper.gamestate.msx;
public class ScreenLocation {
public int topX, topY, botX, botY;
public boolean found = false;
final int botOffsetX=15;
final int botOffsetY=15;
final int offsetX=15;
final int offsetY=-101;
int width;
int height;
}

Some files were not shown because too many files have changed in this diff Show More