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

View File

@ -0,0 +1,606 @@
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace MinesweeperSolver {
/**
* Performs a brute force search on the provided squares using the iterator
*
*/
public class Cruncher {
public const sbyte BOMB = -10;
readonly private SolverInfo information;
private readonly WitnessWebIterator iterator;
readonly private List<SolverTile> tiles;
readonly private List<BoxWitness> witnesses;
readonly private sbyte[] currentFlagsTiles;
readonly private sbyte[] currentFlagsWitnesses;
private int candidates = 0; // number of samples which satisfy the current board state
private readonly BruteForceAnalysis bfa;
public Cruncher(SolverInfo information, WitnessWebIterator iterator, List<BoxWitness> witnesses, BruteForceAnalysis bfa) {
this.information = information;
this.iterator = iterator; // the iterator
this.tiles = iterator.getTiles(); // the tiles the iterator is iterating over
this.witnesses = witnesses; // the dependent witnesses (class BoxWitness) which need to be checked to see if they are satisfied
this.bfa = bfa;
// determine how many found mines are currently next to each tile
this.currentFlagsTiles = new sbyte[this.tiles.Count];
for (int i = 0; i < this.tiles.Count; i++) {
this.currentFlagsTiles[i] = (sbyte) this.information.AdjacentTileInfo(this.tiles[i]).mines;
}
// determine how many found mines are currently next to each witness
this.currentFlagsWitnesses = new sbyte[this.witnesses.Count];
for (int i = 0; i < this.witnesses.Count; i++) {
this.currentFlagsWitnesses[i] = (sbyte) this.information.AdjacentTileInfo(this.witnesses[i].GetTile()).mines;
}
}
public static BruteForceAnalysis PerformBruteForce(SolverInfo information, WitnessWebIterator[] iterators, List<BoxWitness> witnesses) {
BruteForceAnalysis bfa = new BruteForceAnalysis(information, iterators[0].getTiles(), SolverMain.MAX_BFDA_SOLUTIONS, null);
Cruncher[] crunchers = new Cruncher[iterators.Length];
Task[] tasks = new Task[iterators.Length];
for (int i=0; i < iterators.Length; i++) {
crunchers[i] = new Cruncher(information, iterators[i], witnesses, bfa);
Cruncher cruncher = crunchers[i];
tasks[i] = Task.Factory.StartNew(() => { cruncher.Crunch(); });
}
Task.WaitAll(tasks);
int solutions = 0;
int iterations = 0;
for (int i = 0; i < iterators.Length; i++) {
solutions = solutions + crunchers[i].GetSolutionsFound();
iterations = iterations + iterators[i].GetIterations();
}
information.Write("Solutions found by brute force " + solutions + " after " + iterations + " iterations");
return bfa;
}
public void Crunch() {
int[] sample = this.iterator.GetSample();
while (sample != null) {
if (this.CheckSample(sample)) {
candidates++;
}
sample = this.iterator.GetSample();
}
}
public List<SolverTile> getTiles() {
return iterator.getTiles();
}
// this checks whether the positions of the mines are a valid candidate solution
private bool CheckSample(int[] sample) {
// get the tiles which are mines in this sample
SolverTile[] mine = new SolverTile[sample.Length];
for (int i = 0; i < sample.Length; i++) {
mine[i] = this.tiles[sample[i]];
}
for (int i = 0; i < this.witnesses.Count; i++) {
int flags1 = this.currentFlagsWitnesses[i];
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(this.witnesses[i].GetTile())) {
flags2++;
}
}
int flags3 = this.witnesses[i].GetTile().GetValue(); // number of flags indicated on the tile
if (flags3 != flags1 + flags2) {
//Console.WriteLine("Failed");
return false;
}
}
//if it is a good solution then calculate the distribution if required
//Console.WriteLine("Solution found");
sbyte[] solution = new sbyte[this.tiles.Count];
for (int i = 0; i < this.tiles.Count; i++) {
bool 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) {
sbyte flags2 = this.currentFlagsTiles[i];
// count how many candidate mines are next to this square
for (int j = 0; j < mine.Length; j++) {
if (mine[j].IsAdjacent(this.tiles[i])) {
flags2++;
}
}
solution[i] = flags2;
} else {
solution[i] = BOMB;
}
}
bfa.AddSolution(solution);
/*
string output = "";
for (int i = 0; i < mine.length; i++) {
output = output + mine[i].asText();
}
console.log(output);
*/
return true;
}
public BruteForceAnalysis GetBruteForceAnalysis() {
return this.bfa;
}
public int GetSolutionsFound() {
return this.candidates;
}
}
// create an iterator which is like a set of rotating wheels
public class WitnessWebIterator {
private int[] sample;
private SequentialIterator[] cogs;
private int[] squareOffset;
private int[] mineOffset;
private List<SolverTile> tiles;
private int iterationsDone = 0;
readonly private int top;
readonly private int bottom;
private bool done = false;
/*
// if rotation is -1 then this does all the possible iterations
// if rotation is not - 1 then this locks the first 'cog' in that position and iterates the remaining cogs. This allows parallel processing based on the position of the first 'cog'
public WitnessWebIterator(ProbabilityEngine pe, List<SolverTile> allCoveredTiles, int rotation) {
this.tiles = new List<SolverTile>(); // list of tiles being iterated over
this.cogs = new SequentialIterator[pe.GetIndependentWitnesses().Count + 1]; // array of cogs
this.squareOffset = new int[pe.GetIndependentWitnesses().Count + 1]; // int array
this.mineOffset = new int[pe.GetIndependentWitnesses().Count + 1]; // int array
this.iterationsDone = 0;
this.done = false;
//this.probabilityEngine = pe;
// if we are setting the position of the top cog then it can't ever change
if (rotation == -1) {
this.bottom = 0;
} else {
this.bottom = 1;
}
List<SolverTile> loc = new List<SolverTile>(); // array of locations
List<BoxWitness> indWitnesses = pe.GetIndependentWitnesses();
int cogi = 0;
int indSquares = 0;
int indMines = 0;
// create an array of locations in the order of independent witnesses
foreach (BoxWitness w in indWitnesses) {
this.squareOffset[cogi] = indSquares;
this.mineOffset[cogi] = indMines;
this.cogs[cogi] = new SequentialIterator(w.GetMinesToFind(), w.GetAdjacentTiles().Count);
cogi++;
indSquares = indSquares + w.GetAdjacentTiles().Count;
indMines = indMines + w.GetMinesToFind();
loc.AddRange(w.GetAdjacentTiles());
}
//System.out.println("Mines left = " + (mines - indMines));
//System.out.println("Squrs left = " + (web.getSquares().length - indSquares));
// the last cog has the remaining squares and mines
//add the rest of the locations
for (int i = 0; i < allCoveredTiles.Count; i++) {
SolverTile l = allCoveredTiles[i];
bool skip = false;
for (int j = 0; j < loc.Count; j++) {
SolverTile m = loc[j];
if (l.IsEqual(m)) {
skip = true;
break;
}
}
if (!skip) {
loc.Add(l);
}
}
this.tiles = loc;
SolverInfo information = pe.GetSolverInfo();
int minesLeft = information.GetMinesLeft() - information.GetExcludedMineCount();
int tilesLeft = information.GetTilesLeft() - information.GetExcludedTiles().Count;
information.Write("Mines left " + minesLeft);
information.Write("Independent Mines " + indMines);
information.Write("Tiles left " + tilesLeft);
information.Write("Independent tiles " + indSquares);
// 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 (minesLeft - indMines > tilesLeft - indSquares
|| indMines > minesLeft) {
this.done = true;
this.top = 0;
Console.WriteLine("Nothing to do in this iterator");
return;
}
// if there are no mines left then no need for a cog
if (minesLeft > indMines) {
this.squareOffset[cogi] = indSquares;
this.mineOffset[cogi] = indMines;
this.cogs[cogi] = new SequentialIterator(minesLeft - indMines, tilesLeft - indSquares);
this.top = cogi;
} else {
top = cogi - 1;
}
//this.top = this.cogs.Length - 1;
this.sample = new int[minesLeft]; // make the sample array the size of the number of mines
// if we are locking and rotating the top cog then do it
if (rotation != -1) {
for (var i = 0; i < rotation; i++) {
this.cogs[0].GetNextSample();
}
}
// now set up the initial sample position
for (int i = 0; i < this.top; i++) {
int[] s = this.cogs[i].GetNextSample();
for (int j = 0; j < s.Length; j++) {
this.sample[this.mineOffset[i] + j] = this.squareOffset[i] + s[j];
}
}
}
*/
// if rotation is -1 then this does all the possible iterations
// if rotation is not - 1 then this locks the first 'cog' in that position and iterates the remaining cogs. This allows parallel processing based on the position of the first 'cog'
public WitnessWebIterator(SolverInfo information, List<BoxWitness> independentWitnesses, List<BoxWitness> depdendentWitnesses
, List<SolverTile> allCoveredTiles, int minesLeft, int tilesLeft, int rotation) {
this.tiles = new List<SolverTile>(); // list of tiles being iterated over
int cogs;
if (independentWitnesses == null) {
cogs = 1;
} else {
cogs = independentWitnesses.Count + 1;
}
this.cogs = new SequentialIterator[cogs]; // array of cogs
this.squareOffset = new int[cogs]; // int array
this.mineOffset = new int[cogs]; // int array
this.iterationsDone = 0;
this.done = false;
//this.probabilityEngine = pe;
// if we are setting the position of the top cog then it can't ever change
if (rotation == -1) {
this.bottom = 0;
} else {
this.bottom = 1;
}
List<SolverTile> loc = new List<SolverTile>(); // array of locations
int cogi = 0;
int indSquares = 0;
int indMines = 0;
// create an array of locations in the order of independent witnesses
if (independentWitnesses != null) {
foreach (BoxWitness w in independentWitnesses) {
this.squareOffset[cogi] = indSquares;
this.mineOffset[cogi] = indMines;
this.cogs[cogi] = new SequentialIterator(w.GetMinesToFind(), w.GetAdjacentTiles().Count);
cogi++;
indSquares = indSquares + w.GetAdjacentTiles().Count;
indMines = indMines + w.GetMinesToFind();
loc.AddRange(w.GetAdjacentTiles());
}
}
//System.out.println("Mines left = " + (mines - indMines));
//System.out.println("Squrs left = " + (web.getSquares().length - indSquares));
// the last cog has the remaining squares and mines
//add the rest of the locations
for (int i = 0; i < allCoveredTiles.Count; i++) {
SolverTile l = allCoveredTiles[i];
bool skip = false;
for (int j = 0; j < loc.Count; j++) {
SolverTile m = loc[j];
if (l.IsEqual(m)) {
skip = true;
break;
}
}
if (!skip) {
loc.Add(l);
}
}
this.tiles = loc;
information.Write("Mines left " + minesLeft);
information.Write("Independent Mines " + indMines);
information.Write("Tiles left " + tilesLeft);
information.Write("Independent tiles " + indSquares);
// 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 (minesLeft - indMines > tilesLeft - indSquares
|| indMines > minesLeft) {
this.done = true;
this.top = 0;
Console.WriteLine("Nothing to do in this iterator");
return;
}
// if there are no mines left then no need for a cog
if (minesLeft > indMines) {
this.squareOffset[cogi] = indSquares;
this.mineOffset[cogi] = indMines;
this.cogs[cogi] = new SequentialIterator(minesLeft - indMines, tilesLeft - indSquares);
this.top = cogi;
} else {
top = cogi - 1;
}
//this.top = this.cogs.Length - 1;
this.sample = new int[minesLeft]; // make the sample array the size of the number of mines
// if we are locking and rotating the top cog then do it
if (rotation != -1) {
for (var i = 0; i < rotation; i++) {
this.cogs[0].GetNextSample();
}
}
// now set up the initial sample position
for (int i = 0; i < this.top; i++) {
int[] s = this.cogs[i].GetNextSample();
for (int j = 0; j < s.Length; j++) {
this.sample[this.mineOffset[i] + j] = this.squareOffset[i] + s[j];
}
}
}
public int[] GetSample() {
if (this.done) {
Console.WriteLine("**** attempting to iterator when already completed ****");
return null;
}
int index = this.top;
int[] s = this.cogs[index].GetNextSample();
while (s == null && index != this.bottom) {
index--;
s = this.cogs[index].GetNextSample();
}
if (index == this.bottom && s == null) {
this.done = true;
return null;
}
for (int j = 0; j < s.Length; j++) {
this.sample[this.mineOffset[index] + j] = this.squareOffset[index] + s[j];
}
index++;
while (index <= this.top) {
this.cogs[index] = new SequentialIterator(this.cogs[index].GetNumberBalls(), this.cogs[index].GetNumberHoles());
s = this.cogs[index].GetNextSample();
for (int j = 0; j < s.Length; j++) {
this.sample[this.mineOffset[index] + j] = this.squareOffset[index] + s[j];
}
index++;
}
/*
String output = "";
for (int j = 0; j < sample.Length; j++) {
output = output + this.sample[j] + " ";
}
Console.WriteLine(output);
*/
this.iterationsDone++;
return this.sample;
}
public List<SolverTile> getTiles() {
return this.tiles;
}
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
public bool WitnessAlwaysSatisfied(SolverTile location) {
for (var i = 0; i < this.probabilityEngine.independentWitness.length; i++) {
if (this.probabilityEngine.independentWitness[i].equals(location)) {
return true;
}
}
return false;
}
*/
}
public class SequentialIterator {
readonly private int[] sample;
readonly private int numberHoles;
readonly private int numberBalls;
private bool more;
private int index;
// a sequential iterator that puts n-balls in m-holes once in each possible way
public SequentialIterator(int n, int m) {
this.numberHoles = m;
this.numberBalls = n;
this.sample = new int[n];
this.more = true;
this.index = n - 1;
for (int i = 0; i < n; i++) {
this.sample[i] = i;
}
// reduce the iterator by 1, since the first getSample() will increase it
// by 1 again
this.sample[this.index]--;
//Console.WriteLine("Sequential Iterator has " + this.numberBalls + " mines and " + this.numberHoles + " squares");
}
public int[] GetNextSample() {
if (!this.more) {
Console.WriteLine("**** Trying to iterate after the end ****");
return null;
}
this.index = this.numberBalls - 1;
// add on one to the iterator
this.sample[this.index]++;
// if we have rolled off the end then move backwards until we can fit
// the next iteration
while (this.sample[this.index] >= this.numberHoles - this.numberBalls + 1 + this.index) {
if (this.index == 0) {
this.more = false;
return null;
} else {
this.index--;
this.sample[this.index]++;
}
}
// roll forward
while (this.index != this.numberBalls - 1) {
this.index++;
this.sample[this.index] = this.sample[this.index - 1] + 1;
}
return this.sample;
}
public int GetNumberBalls() {
return this.numberBalls;
}
public int GetNumberHoles() {
return this.numberHoles;
}
}
}

View File

@ -0,0 +1,837 @@
using System;
using System.Collections.Generic;
using System.Text;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperSolver {
public class BruteForceAnalysis {
// used to hold all the solutions left in the game
public class SolutionTable {
private readonly object locker = new object();
private readonly BruteForceAnalysis bfa;
private readonly sbyte[][] solutions;
private int size = 0;
public SolutionTable(BruteForceAnalysis bfa, int maxSize) {
this.bfa = bfa;
solutions = new sbyte[maxSize][];
}
public void AddSolution(sbyte[] solution) {
lock(locker) {
solutions[size] = solution;
size++;
}
}
public int GetSize() {
return size;
}
public sbyte[] Get(int index) {
return solutions[index];
}
public void SortSolutions(int start, int end, int index) {
Array.Sort(solutions, start, end - start, bfa.sorters[index]);
}
}
/**
* This sorts solutions by the value of a position
*/
public class SortSolutions : IComparer<sbyte[]> {
private readonly int sortIndex;
public SortSolutions(int index) {
sortIndex = index;
}
public int Compare(sbyte[] o1, sbyte[] o2) {
return o1[sortIndex] - o2[sortIndex];
}
}
/**
* A key to uniquely identify a position
*/
public class Position {
private readonly byte[] position;
private int hash;
public Position(int size) {
position = new byte[size];
for (int i = 0; i < position.Length; i++) {
position[i] = 15;
}
}
public Position(Position p, int index, int value) {
// copy and update to reflect the new position
position = new byte[p.position.Length];
Array.Copy(p.position, position, p.position.Length);
position[index] = (byte)(value + 50);
}
// copied from String hash
public override int GetHashCode() {
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;
}
public override bool Equals(Object o) {
if (o is 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.
*/
public class LivingLocation : IComparable<LivingLocation> {
//private int winningLines = 0;
public bool pruned = false;
public readonly short index;
public int mineCount = 0; // number of remaining solutions which have a mine in this position
public int maxSolutions = 0; // the maximum number of solutions that can be remaining after clicking here
public int zeroSolutions = 0; // the number of solutions that have a '0' value here
public sbyte maxValue = -1;
public sbyte minValue = -1;
public byte count; // number of possible values at this location
public Node[] children;
private readonly BruteForceAnalysis bfa;
public LivingLocation(BruteForceAnalysis bfa, short index) {
this.index = index;
this.bfa = bfa;
}
/**
* 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
*/
public void BuildChildNodes(Node parent) {
// sort the solutions by possible values
bfa.allSolutions.SortSolutions(parent.startLocation, parent.endLocation, this.index);
int index = parent.startLocation;
// skip over the mines
while (index < parent.endLocation && bfa.allSolutions.Get(index)[this.index] == Cruncher.BOMB) {
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);
if (!bfa.cache.TryGetValue(pos, out Node temp1)) { // if value not in cache
Node temp = new Node(bfa, pos);
temp.startLocation = index;
// find all solutions for this values at this location
while (index < parent.endLocation && bfa.allSolutions.Get(index)[this.index] == i) {
index++;
}
temp.endLocation = index;
work[i] = temp;
} else { // value in cache
//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;
bfa.cacheHit++;
bfa.cacheWinningLines = bfa.cacheWinningLines + temp1.winningLines;
// skip past these details in the array
while (index < parent.endLocation && bfa.allSolutions.Get(index)[this.index] <= i) {
index++;
}
}
}
if (index != parent.endLocation) {
Console.WriteLine("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;
}
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
*/
public class Node {
public Position position; // representation of the position we are analysing / have reached
public int winningLines = 0; // this is the number of winning lines below this position in the tree
public int work = 0; // this is a measure of how much work was needed to calculate WinningLines value
private bool fromCache = false; // indicates whether this position came from the cache
public int startLocation; // the first solution in the solution array that applies to this position
public int endLocation; // the last + 1 solution in the solution array that applies to this position
public List<LivingLocation> livingLocations; // these are the locations which need to be analysed
public LivingLocation bestLiving; // after analysis this is the location that represents best play
private readonly BruteForceAnalysis bfa;
public Node(BruteForceAnalysis bfa, int size) {
position = new Position(size);
this.bfa = bfa;
}
public Node(BruteForceAnalysis bfa, Position position) {
this.position = position;
this.bfa = bfa;
}
public List<LivingLocation> GetLivingLocations() {
return livingLocations;
}
public int GetSolutionSize() {
return endLocation - startLocation;
}
/**
* Get the probability of winning the game from the position this node represents (winningLines / solution size)
* @return
*/
public double GetProbability() {
return ((double)winningLines) / GetSolutionSize();
//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
*/
public int GetWinningLines(LivingLocation move) {
//if we can never exceed the cutoff then no point continuing
if (SolverMain.PRUNE_BF_ANALYSIS && this.GetSolutionSize() - move.mineCount <= this.winningLines) {
move.pruned = true;
return 0;
}
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
*/
public int GetWinningLines(int depth, LivingLocation move, int cutoff) {
int result = 0;
bfa.processCount++;
if (bfa.processCount > SolverMain.BRUTE_FORCE_ANALYSIS_MAX_NODES) {
return 0;
}
int notMines = this.GetSolutionSize() - move.mineCount;
move.BuildChildNodes(this);
foreach (Node child in move.children) {
if (child == null) {
continue; // continue the loop but ignore this entry
}
int maxWinningLines = result + notMines;
// if the max possible winning lines is less than the current cutoff then no point doing the analysis
if (SolverMain.PRUNE_BF_ANALYSIS && maxWinningLines <= cutoff) {
move.pruned = true;
return 0;
}
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().Count == 0) { // 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
foreach (LivingLocation childMove in 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 (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;
bfa.cacheSize++;
bfa.cache.Add(child.position, child);
} else {
this.work = this.work + child.work;
}
}
}
if (depth > SolverMain.BRUTE_FORCE_ANALYSIS_TREE_DEPTH) { // 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
}
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)
*/
public void DetermineLivingLocations(List<LivingLocation> liveLocs, int index) {
List<LivingLocation> living = new List<LivingLocation>(liveLocs.Count);
foreach (LivingLocation live in 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 = bfa.ResetValues();
int mines = 0;
int maxSolutions = 0;
byte count = 0;
sbyte minValue = 0;
sbyte maxValue = 0;
for (int j = startLocation; j < endLocation; j++) {
value = bfa.allSolutions.Get(j)[live.index];
if (value != Cruncher.BOMB) {
//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 (sbyte 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(bfa, live.index);
alive.mineCount = mines;
alive.count = count;
alive.minValue = minValue;
alive.maxValue = maxValue;
alive.maxSolutions = maxSolutions;
alive.zeroSolutions = valueCount[0];
living.Add(alive);
}
}
living.Sort();
//Collections.sort(living);
this.livingLocations = living;
}
public override int GetHashCode() {
return position.GetHashCode();
}
public override bool Equals(Object o) {
if (o is Node) {
return position.Equals(((Node)o).position);
} else {
return false;
}
}
}
// start of main class
private static readonly String INDENT = "................................................................................";
//private static readonly BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);
public int processCount = 0;
private readonly SolverInfo solver;
private readonly int maxSolutionSize;
//private Node top;
private readonly List<SolverTile> locations; // the positions being analysed
private readonly List<SolverTile> startLocations; // the positions which will be considered for the first move
private readonly SolutionTable allSolutions;
//private readonly String scope;
private Node currentNode;
private SolverTile expectedMove;
private readonly SortSolutions[] sorters;
private int cacheHit = 0;
public int cacheSize = 0;
private int cacheWinningLines = 0;
private bool allDead = false; // this is true if all the locations are dead
private bool tooMany = false;
private bool completed = false;
// some work areas to prevent having to instantiate many 1000's of copies of them
//private final boolean[] values = new boolean[9];
private readonly int[] valueCount = new int[9];
private Dictionary<Position, Node> cache = new Dictionary<Position, Node>(5000);
public BruteForceAnalysis(SolverInfo solver, List<SolverTile> locations, int size, List<SolverTile> startLocations) {
this.solver = solver;
this.locations = locations;
this.maxSolutionSize = size;
//this.top = new Node();
sorters = new SortSolutions[locations.Count];
for (int i = 0; i < sorters.Length; i++) {
sorters[i] = new SortSolutions(i);
}
this.allSolutions = new SolutionTable(this, size);
this.startLocations = startLocations;
}
public void AddSolution(sbyte[] solution) {
if (solution.Length != locations.Count) {
throw new Exception("Solution does not have the correct number of locations");
}
if (allSolutions.GetSize() >= maxSolutionSize) {
tooMany = true;
return;
}
/*
String text = "";
for (int i=0; i < solution.length; i++) {
text = text + solution[i] + " ";
}
solver.display(text);
*/
allSolutions.AddSolution(solution);
}
public void process() {
long start = DateTime.Now.Ticks;
solver.Write("----- Brute Force Deep Analysis starting ----");
solver.Write(allSolutions.GetSize() + " solutions in BruteForceAnalysis");
// create the top node
Node top = buildTopNode(allSolutions);
if (top.GetLivingLocations().Count == 0) {
allDead = true;
}
int best = 0;
foreach (LivingLocation move in top.GetLivingLocations()) {
// check that the move is in the startLocation list
if (startLocations != null) {
bool found = false;
foreach (SolverTile l in startLocations) {
if (locations[move.index].Equals(l)) {
found = true;
break;
}
}
if (!found) { // if not then skip this move
solver.Write(move.index + " " + locations[move.index].AsText() + " is not a starting location");
continue;
}
}
int winningLines = top.GetWinningLines(move); // calculate the number of winning lines if this move is played
if (best < winningLines || (top.bestLiving != null && best == winningLines && top.bestLiving.mineCount < move.mineCount)) {
best = winningLines;
top.bestLiving = move;
}
double singleProb = (allSolutions.GetSize() - move.mineCount) / allSolutions.GetSize();
if (move.pruned) {
solver.Write(move.index + " " + locations[move.index].AsText() + " is living with " + move.count + " possible values and probability " + singleProb + ", this location was pruned");
} else {
solver.Write(move.index + " " + locations[move.index].AsText() + " is living with " + move.count + " possible values and probability " + singleProb + ", winning lines " + winningLines);
}
}
top.winningLines = best;
currentNode = top;
if (processCount < SolverMain.BRUTE_FORCE_ANALYSIS_MAX_NODES) {
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 = DateTime.Now.Ticks;
solver.Write("Total nodes in cache = " + cacheSize + ", total cache hits = " + cacheHit + ", total winning lines saved = " + this.cacheWinningLines);
solver.Write("process took " + (end - start) + " milliseconds and explored " + processCount + " nodes");
solver.Write("----- Brute Force Deep Analysis finished ----");
}
/**
* Builds a top of tree node based on the solutions provided
*/
private Node buildTopNode(SolutionTable solutionTable) {
Node result = new Node(this, locations.Count);
result.startLocation = 0;
result.endLocation = solutionTable.GetSize();
List<LivingLocation> living = new List<LivingLocation>();
for (short i = 0; i < locations.Count; i++) {
int value;
int[] valueCount = ResetValues();
int mines = 0;
int maxSolutions = 0;
byte count = 0;
sbyte minValue = 0;
sbyte maxValue = 0;
for (int j = 0; j < result.GetSolutionSize(); j++) {
if (solutionTable.Get(j)[i] != Cruncher.BOMB) {
value = solutionTable.Get(j)[i];
//values[value] = true;
valueCount[value]++;
} else {
mines++;
}
}
for (sbyte 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(this, i);
alive.mineCount = mines;
alive.count = count;
alive.minValue = minValue;
alive.maxValue = maxValue;
alive.maxSolutions = maxSolutions;
alive.zeroSolutions = valueCount[0];
living.Add(alive);
} else {
solver.Write(locations[i].AsText() + " is dead with value " + minValue);
}
}
living.Sort();
//Collections.sort(living);
result.livingLocations = living;
return result;
}
public int[] ResetValues() {
for (int i = 0; i < valueCount.Length; i++) {
valueCount[i] = 0;
}
return valueCount;
}
public int GetSolutionCount() {
return allSolutions.GetSize();
}
public int GetNodeCount() {
return processCount;
}
public SolverAction GetNextMove() {
LivingLocation bestLiving = getBestLocation(currentNode);
if (bestLiving == null) {
return null;
}
SolverTile loc = this.locations[bestLiving.index];
//solver.display("first best move is " + loc.display());
double prob = 1 - bestLiving.mineCount / currentNode.GetSolutionSize();
while (!loc.IsHidden()) {
int value = loc.GetValue();
currentNode = bestLiving.children[value];
bestLiving = getBestLocation(currentNode);
if (bestLiving == null) {
return null;
}
prob = 1 - ((double) bestLiving.mineCount) / currentNode.GetSolutionSize();
loc = this.locations[bestLiving.index];
}
solver.Write("mines = " + bestLiving.mineCount + " solutions = " + 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 = (100 / (bestLiving.children[i].GetSolutionSize())) + "%";
} else {
probText = bestLiving.children[i].GetProbability() * 100 + "%";
}
solver.Write("Value of " + i + " leaves " + bestLiving.children[i].GetSolutionSize() + " solutions and winning probability " + probText + " (work size " + bestLiving.children[i].work + ")");
}
//String text = " (solve " + (currentNode.GetProbability() * 100) + "%)";
SolverAction action = new SolverAction(loc, ActionType.Clear, 0.5);
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 line1 = INDENT.Substring(0, depth * 3) + condition + " Solve chance " + node.GetProbability() * 100 + "%";
Console.WriteLine(line1);
return;
}
SolverTile loc = this.locations[node.bestLiving.index];
double prob = 1 - node.bestLiving.mineCount / node.GetSolutionSize();
String line = INDENT.Substring(0, depth * 3) + condition + " play " + loc.AsText() + " Survival chance " + prob * 100 + "%, Solve chance " + node.GetProbability() * 100 + "%";
Console.WriteLine(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);
}
}
}
public bool IsComplete() {
return this.completed;
}
public SolverTile GetExpectedMove() {
return expectedMove;
}
//private String percentage(double prob) {
// return Action.FORMAT_2DP.format(prob.multiply(ONE_HUNDRED));
//}
public bool GetAllDead() {
return allDead;
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Numerics;
namespace MinesweeperSolver {
public class Combination {
public static BigInteger Calculate(int mines, int squares) {
long start = DateTime.Now.Ticks;
BigInteger top = 1;
BigInteger bot = 1;
var range = Math.Min(mines, squares - mines);
// calculate the combination.
for (int i = 0; i < range; i++) {
top = top * (squares - i);
bot = bot * (i + 1);
}
BigInteger result = top / bot;
//SolverMain.Write(squares + " pick " + mines + " in " + result + " ways");
//SolverMain.Write("Combination duration " + (DateTime.Now.Ticks - start) + " ticks");
return result;
}
private static readonly BigInteger[] power10n = { BigInteger.One, new BigInteger(10), new BigInteger(100), new BigInteger(1000), new BigInteger(10000), new BigInteger(100000), new BigInteger(1000000) };
private static readonly int[] power10 = { 1, 10, 100, 1000, 10000, 100000, 1000000 };
public static double DivideBigIntegerToDouble(BigInteger numerator, BigInteger denominator, int dp) {
var work = numerator * power10n[dp] / denominator;
var result = (double) work / power10[dp];
return result;
}
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>60591e9a-f066-4af3-b1e3-b20b2b2b0b1d</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>MinesweeperSolver</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Binomial.cs" />
<Compile Include="$(MSBuildThisFileDirectory)BruteForce.cs" />
<Compile Include="$(MSBuildThisFileDirectory)BruteForceAnalysis.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Combination.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PrimeSieve.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolutionCounter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ProbabilityEngine.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverAction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverActionHeader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverMain.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverTile.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>60591e9a-f066-4af3-b1e3-b20b2b2b0b1d</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="MinesweeperSolver.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

View File

@ -0,0 +1,129 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace MinesweeperSolver {
public class PrimeSieve {
// iterator for prime numbers
public class Primes : IEnumerable<int>, IEnumerator<int> {
private int index = 0;
private readonly int stop;
private int nextPrime;
private readonly bool[] composite;
public int Current => Next();
object IEnumerator.Current => Next();
public Primes(bool[] composite, int start, int stop) {
this.index = start;
this.stop = stop;
this.composite = composite;
this.nextPrime = findNext();
}
public int 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;
}
public void Dispose() {
}
public bool MoveNext() {
return (nextPrime != -1);
}
public void Reset() {
throw new NotImplementedException();
}
public IEnumerator<int> GetEnumerator() {
return this;
}
IEnumerator IEnumerable.GetEnumerator() {
return this;
}
}
private readonly bool[] composite;
private readonly int max;
public PrimeSieve(int n) {
if (n < 2) {
max = 2;
} else {
max = n;
}
composite = new bool[max + 1];
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 bool IsPrime(int n) {
if (n <= 1 || n > max) {
throw new Exception("Test value " + n + " is out of range 2 - " + max);
}
return !composite [n];
}
public IEnumerable<int> getPrimesIterable(int start, int stop) {
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(composite, start, stop);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,859 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperSolver {
public class SolutionCounter {
private readonly int[][] SMALL_COMBINATIONS = new int[][] { new int[] { 1 }, new int[] { 1, 1 }, new int[] { 1, 2, 1 }, new int[] { 1, 3, 3, 1 }, new int[] { 1, 4, 6, 4, 1 }, new int[] { 1, 5, 10, 10, 5, 1 }, new int[] { 1, 6, 15, 20, 15, 6, 1 }, new int[] { 1, 7, 21, 35, 35, 21, 7, 1 }, new int[] { 1, 8, 28, 56, 70, 56, 28, 8, 1 } };
private readonly List<SolverTile> witnessed;
private readonly List<BoxWitness> prunedWitnesses = new List<BoxWitness>(); // a subset of allWitnesses with equivalent witnesses removed
private readonly List<Box> boxes = new List<Box>();
private readonly List<BoxWitness> boxWitnesses = new List<BoxWitness>();
private bool[] mask;
private readonly Dictionary<SolverTile, Box> boxLookup = new Dictionary<SolverTile, Box>(); // a lookup which finds which box a tile belongs to (an efficiency enhancement)
//private readonly List<DeadCandidate> deadCandidates = new List<DeadCandidate>();
private List<ProbabilityLine> workingProbs = new List<ProbabilityLine>();
private readonly List<ProbabilityLine> heldProbs = new List<ProbabilityLine>();
private readonly List<EdgeStore> edgeStore = new List<EdgeStore>(); // stores independent edge analysis until we know we have to merge them
private readonly int minesLeft;
private readonly int tilesLeft;
private readonly int tilesOffEdge;
private readonly int minTotalMines;
private readonly int maxTotalMines;
private int recursions;
// used to find the range of mine counts which offer the 2.5% - 97.5% weighted average
private int edgeMinesMin;
private int edgeMinesMax;
private int edgeMinesMinLeft;
private int edgeMinesMaxLeft;
private int mineCountUpperCutoff;
private int mineCountLowerCutoff;
private bool truncatedProbs = false; // this gets set when the number of held probabilies exceeds the permitted threshold
private BigInteger finalSolutionCount = 0;
private BigInteger solutionCountMultiplier = 1;
private int clearCount = 0;
private readonly SolverInfo information;
public SolutionCounter(SolverInfo information, List<SolverTile> allWitnesses, List<SolverTile> allWitnessed, int squaresLeft, int minesLeft) {
this.information = information;
this.witnessed = allWitnessed;
// constraints in the game
this.minesLeft = minesLeft;
this.tilesLeft = squaresLeft;
this.tilesOffEdge = squaresLeft - allWitnessed.Count; // squares left off the edge and unrevealed
this.minTotalMines = minesLeft - this.tilesOffEdge; //we can't use so few mines that we can't fit the remainder elsewhere on the board
this.maxTotalMines = minesLeft;
this.mineCountUpperCutoff = minesLeft;
this.mineCountLowerCutoff = minTotalMines;
information.Write("Tiles off edge " + tilesOffEdge);
//this.boxProb = []; // the probabilities end up here
// generate a BoxWitness for each witness tile and also create a list of pruned witnesses for the brute force search
int pruned = 0;
foreach (SolverTile wit in allWitnesses) {
BoxWitness boxWit = new BoxWitness(information, wit);
// if the witness is a duplicate then don't store it
bool duplicate = false;
foreach (BoxWitness w in this.boxWitnesses) {
if (w.Equivalent(boxWit)) {
//if (boardState.getWitnessValue(w) - boardState.countAdjacentConfirmedFlags(w) != boardState.getWitnessValue(wit) - boardState.countAdjacentConfirmedFlags(wit)) {
// boardState.display(w.display() + " and " + wit.display() + " share unrevealed squares but have different mine totals!");
// validWeb = false;
//}
duplicate = true;
break;
}
}
if (!duplicate) {
this.prunedWitnesses.Add(boxWit);
} else {
pruned++;
}
this.boxWitnesses.Add(boxWit); // all witnesses are needed for the probability engine
}
information.Write("Pruned " + pruned + " witnesses as duplicates");
information.Write("There are " + this.boxWitnesses.Count + " Box witnesses");
// allocate each of the witnessed squares to a box
int uid = 0;
foreach (SolverTile tile in this.witnessed) {
// for each adjacent tile see if it is a witness
int count = 0;
//foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) {
// if (information.GetWitnesses().Contains(adjTile)) {
// count++;
// }
//}
// count how many adjacent witnesses the tile has
foreach (SolverTile tile1 in allWitnesses) {
if (tile.IsAdjacent(tile1)) {
count++;
}
}
// see if the witnessed tile fits any existing boxes
bool found = false;
foreach (Box box in this.boxes) {
if (box.Fits(tile, count)) {
box.Add(tile);
boxLookup.Add(tile, box); // add this to the lookup
found = true;
break;
}
}
// if not found create a new box and store it
if (!found) {
Box box = new Box(this.boxWitnesses, tile, uid++);
this.boxes.Add(box);
boxLookup.Add(tile, box); // add this to the lookup
}
}
// calculate the min and max mines for each box
foreach (Box box in this.boxes) {
box.Calculate(this.minesLeft);
//console.log("Box " + box.tiles[0].asText() + " has min mines = " + box.minMines + " and max mines = " + box.maxMines);
}
// Report how many boxes each witness is adjacent to
foreach (BoxWitness boxWit in this.boxWitnesses) {
information.Write("Witness " + boxWit.GetTile().AsText() + " is adjacent to " + boxWit.GetBoxes().Count + " boxes and has " + boxWit.GetMinesToFind() + " mines to find");
}
}
// calculate a probability for each un-revealed tile on the board
public void Process() {
this.mask = new bool[this.boxes.Count];
// create an initial solution of no mines anywhere
ProbabilityLine held = new ProbabilityLine(this.boxes.Count);
held.SetSolutionCount(1);
this.heldProbs.Add(held);
// add an empty probability line to get us started
this.workingProbs.Add(new ProbabilityLine(this.boxes.Count));
NextWitness nextWitness = FindFirstWitness();
while (nextWitness != null) {
// mark the new boxes as processed - which they will be soon
foreach (Box box in nextWitness.GetNewBoxes()) {
this.mask[box.GetUID()] = true;
}
this.workingProbs = MergeProbabilities(nextWitness);
nextWitness = FindNextWitness(nextWitness);
}
CalculateBoxProbabilities();
}
// take the next witness details and merge them into the currently held details
private List<ProbabilityLine> MergeProbabilities(NextWitness nw) {
List<ProbabilityLine> newProbs = new List<ProbabilityLine>();
foreach (ProbabilityLine pl in this.workingProbs) {
int missingMines = nw.GetBoxWitness().GetMinesToFind() - (int)CountPlacedMines(pl, nw);
if (missingMines < 0) {
//console.log("Missing mines < 0 ==> ignoring line");
// too many mines placed around this witness previously, so this probability can't be valid
} else if (missingMines == 0) {
//console.log("Missing mines = 0 ==> keeping line as is");
newProbs.Add(pl); // witness already exactly satisfied, so nothing to do
} else if (nw.GetNewBoxes().Count == 0) {
//console.log("new boxes = 0 ==> ignoring line since nowhere for mines to go");
// nowhere to put the new mines, so this probability can't be valid
} else {
List<ProbabilityLine> result = DistributeMissingMines(pl, nw, missingMines, 0);
newProbs.AddRange(result);
}
}
//if (newProbs.length == 0) {
// console.log("Returning no lines from merge probability !!");
//}
return newProbs;
}
// counts the number of mines already placed
private BigInteger CountPlacedMines(ProbabilityLine pl, NextWitness nw) {
BigInteger result = 0;
foreach (Box b in nw.GetOldBoxes()) {
result = result + pl.GetMineBoxCount(b.GetUID());
}
return result;
}
// 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) {
//console.log("Distributing " + missingMines + " missing mines to box " + nw.newBoxes[index].uid);
this.recursions++;
if (this.recursions % 100000 == 0) {
information.Write("Solution Counter recursision = " + recursions);
}
List<ProbabilityLine> result = new List<ProbabilityLine>();
// if there is only one box left to put the missing mines we have reach the end of this branch of recursion
if (nw.GetNewBoxes().Count - index == 1) {
// if there are too many for this box then the probability can't be valid
if (nw.GetNewBoxes()[index].GetMaxMines() < missingMines) {
//console.log("Abandon (1)");
return result;
}
// if there are too few for this box then the probability can't be valid
if (nw.GetNewBoxes()[index].GetMinMines() > missingMines) {
//console.log("Abandon (2)");
return result;
}
// if there are too many for this game then the probability can't be valid
if (pl.GetMineCount() + missingMines > this.maxTotalMines) {
//console.log("Abandon (3)");
return result;
}
// otherwise place the mines in the probability line
pl.SetMineBoxCount(nw.GetNewBoxes()[index].GetUID(), new BigInteger(missingMines));
pl.SetMineCount(pl.GetMineCount() + missingMines);
result.Add(pl);
//console.log("Distribute missing mines line after " + pl.mineBoxCount);
return result;
}
// this is the recursion
int maxToPlace = Math.Min(nw.GetNewBoxes()[index].GetMaxMines(), missingMines);
for (int i = nw.GetNewBoxes()[index].GetMinMines(); i <= maxToPlace; i++) {
ProbabilityLine npl = ExtendProbabilityLine(pl, nw.GetNewBoxes()[index], i);
List<ProbabilityLine> r1 = DistributeMissingMines(npl, nw, missingMines - i, index + 1);
result.AddRange(r1);
}
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) {
//console.log("Extended probability line: Adding " + mines + " mines to box " + newBox.uid);
//console.log("Extended probability line before" + pl.mineBoxCount);
ProbabilityLine result = new ProbabilityLine(this.boxes.Count);
result.SetMineCount(pl.GetMineCount() + mines);
result.CopyMineBoxCount(pl);
result.SetMineBoxCount(newBox.GetUID(), new BigInteger(mines));
//console.log("Extended probability line after " + result.mineBoxCount);
return result;
}
// here we store the information we have found about this independent edge for later use
private void StoreProbabilities() {
List<ProbabilityLine> crunched = CrunchByMineCount(this.workingProbs);
//Console.WriteLine("Edge has " + crunched.Count + " probability lines after consolidation");
edgeStore.Add(new EdgeStore(crunched, this.mask)); // store the existing data
workingProbs.Clear(); // and get a new list for the next independent edge
this.workingProbs.Add(new ProbabilityLine(this.boxes.Count)); // add a new starter probability line
this.mask = new bool[boxes.Count];
}
// this combines newly generated probabilities with ones we have already stored from other independent sets of witnesses
private void CombineProbabilities() {
List<ProbabilityLine> result = new List<ProbabilityLine>();
if (this.workingProbs.Count == 0) {
information.Write("working probabilites list is empty!!");
return;
}
// see if we can find a common divisor
BigInteger hcd = workingProbs[0].GetSolutionCount();
foreach (ProbabilityLine pl in workingProbs) {
hcd = BigInteger.GreatestCommonDivisor(hcd, pl.GetSolutionCount());
}
foreach (ProbabilityLine pl in heldProbs) {
hcd = BigInteger.GreatestCommonDivisor(hcd, pl.GetSolutionCount());
}
information.Write("Greatest Common Divisor is " + hcd);
solutionCountMultiplier = solutionCountMultiplier * hcd;
int mineCountMin = workingProbs[0].GetMineCount() + heldProbs[0].GetMineCount();
// shrink the window
edgeMinesMinLeft = edgeMinesMinLeft - workingProbs[0].GetMineCount();
edgeMinesMaxLeft = edgeMinesMaxLeft - workingProbs[workingProbs.Count - 1].GetMineCount();
foreach (ProbabilityLine pl in workingProbs) {
BigInteger plSolCount = pl.GetSolutionCount() / hcd;
foreach (ProbabilityLine epl in heldProbs) {
// if the mine count can never reach the lower cuttoff then ignore it
if (pl.GetMineCount() + epl.GetMineCount() + edgeMinesMaxLeft < this.mineCountLowerCutoff) {
continue;
}
// if the mine count will always be pushed beyonf the upper cuttoff then ignore it
if (pl.GetMineCount() + epl.GetMineCount() + edgeMinesMinLeft > this.mineCountUpperCutoff) {
continue;
}
ProbabilityLine newpl = new ProbabilityLine(this.boxes.Count);
newpl.SetMineCount(pl.GetMineCount() + epl.GetMineCount());
BigInteger eplSolCount = epl.GetSolutionCount() / hcd;
newpl.SetSolutionCount(pl.GetSolutionCount() * eplSolCount);
for (int k = 0; k < this.boxes.Count; k++) {
BigInteger w1 = pl.GetMineBoxCount(k) * eplSolCount;
BigInteger w2 = epl.GetMineBoxCount(k) * plSolCount;
newpl.SetMineBoxCount(k, w1 + w2);
}
result.Add(newpl);
}
}
//Console.WriteLine("Solution multiplier is " + solutionCountMultiplier);
this.heldProbs.Clear();
// if result is empty this is an impossible position
if (result.Count == 0) {
Console.WriteLine("Impossible position encountered");
return;
}
if (result.Count == 1) {
heldProbs.AddRange(result);
return;
}
// sort into mine order
result.Sort();
// and combine them into a single probability line for each mine count
int mc = result[0].GetMineCount();
ProbabilityLine npl = new ProbabilityLine(this.boxes.Count);
npl.SetMineCount(mc);
int startMC = mc;
foreach (ProbabilityLine pl in result) {
if (pl.GetMineCount() != mc) {
this.heldProbs.Add(npl);
mc = pl.GetMineCount();
npl = new ProbabilityLine(this.boxes.Count);
npl.SetMineCount(mc);
}
npl.SetSolutionCount(npl.GetSolutionCount() + pl.GetSolutionCount());
for (int j = 0; j < this.boxes.Count; j++) {
npl.SetMineBoxCount(j, npl.GetMineBoxCount(j) + pl.GetMineBoxCount(j));
}
}
this.heldProbs.Add(npl);
}
private List<ProbabilityLine> CrunchByMineCount(List<ProbabilityLine> target) {
//if (target.Count == 0) {
// return target;
//}
// sort the solutions by number of mines
target.Sort();
List<ProbabilityLine> result = new List<ProbabilityLine>();
int mc = target[0].GetMineCount();
ProbabilityLine npl = new ProbabilityLine(this.boxes.Count);
npl.SetMineCount(mc);
foreach (ProbabilityLine pl in target) {
if (pl.GetMineCount() != mc) {
result.Add(npl);
mc = pl.GetMineCount();
npl = new ProbabilityLine(this.boxes.Count);
npl.SetMineCount(mc);
}
MergeLineProbabilities(npl, pl);
}
//if (npl.GetMineCount() >= minTotalMines) {
result.Add(npl);
//}
//information.Write("Probability line has " + npl.GetMineCount() + " mines");
return result;
}
// calculate how many ways this solution can be generated and roll them into one
private void MergeLineProbabilities(ProbabilityLine npl, ProbabilityLine pl) {
BigInteger solutions = 1;
for (int i = 0; i < this.boxes.Count; i++) {
solutions = solutions * (BigInteger)SMALL_COMBINATIONS[this.boxes[i].GetTiles().Count][(int)pl.GetMineBoxCount(i)];
}
npl.SetSolutionCount(npl.GetSolutionCount() + solutions);
for (int i = 0; i < this.boxes.Count; i++) {
if (this.mask[i]) { // if this box has been involved in this solution - if we don't do this the hash gets corrupted by boxes = 0 mines because they weren't part of this edge
npl.SetMineBoxCount(i, npl.GetMineBoxCount(i) + pl.GetMineBoxCount(i) * solutions);
}
}
}
// return any witness which hasn't been processed
private NextWitness FindFirstWitness() {
BoxWitness excluded = null;
foreach (BoxWitness boxWit in this.boxWitnesses) {
if (!boxWit.IsProcessed()) {
return new NextWitness(boxWit);
} else if (!boxWit.IsProcessed()) {
excluded = boxWit;
}
}
if (excluded != null) {
return new NextWitness(excluded);
}
return null;
}
// look for the next witness to process
private NextWitness FindNextWitness(NextWitness prevWitness) {
// flag the last set of details as processed
prevWitness.GetBoxWitness().SetProcessed(true);
foreach (Box newBox in prevWitness.GetNewBoxes()) {
newBox.SetProcessed(true);
}
int bestTodo = 99999;
BoxWitness bestWitness = null;
// and find a witness which is on the boundary of what has already been processed
foreach (Box b in this.boxes) {
if (b.IsProcessed()) {
foreach (BoxWitness w in b.GetBoxWitnesses()) {
if (!w.IsProcessed()) {
int todo = 0;
foreach (Box b1 in w.GetBoxes()) {
if (!b1.IsProcessed()) {
todo++;
}
}
if (todo == 0) { // prioritise the witnesses which have the least boxes left to process
return new NextWitness(w);
} else if (todo < bestTodo) {
bestTodo = todo;
bestWitness = w;
}
}
}
}
}
if (bestWitness != null) {
return new NextWitness(bestWitness);
} else {
information.Write("Ending independent edge");
}
//independentGroups++;
// since we have calculated all the mines in an independent set of witnesses we can crunch them down and store them for later
// store the details for this edge
StoreProbabilities();
// get an unprocessed witness
NextWitness nw = FindFirstWitness();
if (nw != null) {
information.Write("Starting a new independent edge");
}
// If there is nothing else to process then either do the local clears or calculate the probabilities
if (nw == null) {
edgeStore.Sort(EdgeStore.SortByLineCount);
AnalyseAllEdges();
long start = DateTime.Now.Ticks;
foreach (EdgeStore edgeDetails in edgeStore) {
workingProbs = edgeDetails.data;
CombineProbabilities();
}
information.Write("Combined all edges in " + (DateTime.Now.Ticks - start) + " ticks");
}
// return the next witness to process
return nw;
}
// take a look at what edges we have and show some information - trying to get some ideas on how we can get faster good guesses
private void AnalyseAllEdges() {
//Console.WriteLine("Number of tiles off the edge " + tilesOffEdge);
//Console.WriteLine("Number of mines to find " + minesLeft);
this.edgeMinesMin = 0;
this.edgeMinesMax = 0;
foreach (EdgeStore edge in edgeStore) {
int edgeMinMines = edge.data[0].GetMineCount();
int edgeMaxMines = edge.data[edge.data.Count - 1].GetMineCount();
edgeMinesMin = edgeMinesMin + edgeMinMines;
edgeMinesMax = edgeMinesMax + edgeMaxMines;
}
information.Write("Min mines on all edges " + edgeMinesMin + ", max " + edgeMinesMax);
this.edgeMinesMaxLeft = this.edgeMinesMax; // these values are used in the merge logic to reduce the number of lines need to keep
this.edgeMinesMinLeft = this.edgeMinesMin;
this.mineCountLowerCutoff = this.edgeMinesMin;
this.mineCountUpperCutoff = Math.Min(this.edgeMinesMax, this.minesLeft); // can't have more mines than are left
// comment this out when doing large board analysis
return;
// the code below reduces the range of mine count values to just be the 'significant' range
List<ProbabilityLine> store = new List<ProbabilityLine>();
List<ProbabilityLine> store1 = new List<ProbabilityLine>();
ProbabilityLine init = new ProbabilityLine(0);
init.SetSolutionCount(1);
store.Add(init);
// combine all the edges to determine the relative weights of the mine count
foreach (EdgeStore edgeDetails in edgeStore) {
foreach (ProbabilityLine pl in edgeDetails.data) {
BigInteger plSolCount = pl.GetSolutionCount();
foreach (ProbabilityLine epl in store) {
if (pl.GetMineCount() + epl.GetMineCount() <= this.maxTotalMines) {
ProbabilityLine newpl = new ProbabilityLine(0);
newpl.SetMineCount(pl.GetMineCount() + epl.GetMineCount());
BigInteger eplSolCount = epl.GetSolutionCount();
newpl.SetSolutionCount(pl.GetSolutionCount() * eplSolCount);
store1.Add(newpl);
}
}
}
store.Clear();
// sort into mine order
store1.Sort();
int mc = store1[0].GetMineCount();
ProbabilityLine npl = new ProbabilityLine(0);
npl.SetMineCount(mc);
foreach (ProbabilityLine pl in store1) {
if (pl.GetMineCount() != mc) {
store.Add(npl);
mc = pl.GetMineCount();
npl = new ProbabilityLine(0);
npl.SetMineCount(mc);
}
npl.SetSolutionCount(npl.GetSolutionCount() + pl.GetSolutionCount());
}
store.Add(npl);
store1.Clear();
}
BigInteger total = 0;
int mineValues = 0;
foreach (ProbabilityLine pl in store) {
if (pl.GetMineCount() >= this.minTotalMines) { // if the mine count for this solution is less than the minimum it can't be valid
BigInteger mult = SolverMain.Calculate(this.minesLeft - pl.GetMineCount(), this.tilesOffEdge); //# of ways the rest of the board can be formed
total = total + mult * pl.GetSolutionCount();
mineValues++;
}
}
//this.mineCountLowerCutoff = this.edgeMinesMin;
//this.mineCountUpperCutoff = Math.Min(this.edgeMinesMax, this.minesLeft); // can't have more mines than are left
BigInteger soFar = 0;
foreach (ProbabilityLine pl in store) {
if (pl.GetMineCount() >= this.minTotalMines) { // if the mine count for this solution is less than the minimum it can't be valid
BigInteger mult = SolverMain.Calculate(this.minesLeft - pl.GetMineCount(), this.tilesOffEdge); //# of ways the rest of the board can be formed
soFar = soFar + mult * pl.GetSolutionCount();
double perc = Combination.DivideBigIntegerToDouble(soFar, total, 6) * 100;
//Console.WriteLine("Mine count " + pl.GetMineCount() + " has solution count " + pl.GetSolutionCount() + " multiplier " + mult + " running % " + perc);
//Console.WriteLine("Mine count " + pl.GetMineCount() + " has solution count " + pl.GetSolutionCount() + " has running % " + perc);
if (mineValues > 30 && perc < 2.5) {
this.mineCountLowerCutoff = pl.GetMineCount();
}
if (mineValues > 30 && perc > 97.5) {
this.mineCountUpperCutoff = pl.GetMineCount();
break;
}
}
}
information.Write("Significant range " + this.mineCountLowerCutoff + " - " + this.mineCountUpperCutoff);
//this.edgeMinesMaxLeft = this.edgeMinesMax;
//this.edgeMinesMinLeft = this.edgeMinesMin;
return;
// below here are experimental ideas on getting a good guess on very large boards
int midRangeAllMines = (this.mineCountLowerCutoff + this.mineCountUpperCutoff) / 2;
BigInteger[] tally = new BigInteger[boxes.Count];
double[] probability = new double[boxes.Count];
foreach (EdgeStore edgeDetails in edgeStore) {
int sizeRangeEdgeMines = (edgeDetails.data[edgeDetails.data.Count - 1].GetMineCount() - edgeDetails.data[0].GetMineCount()) / 2;
int start = (this.mineCountLowerCutoff - edgeDetails.data[0].GetMineCount() + this.mineCountUpperCutoff - edgeDetails.data[edgeDetails.data.Count - 1].GetMineCount()) / 2;
//int start = midRangeAllMines - sizeRangeEdgeMines;
//BigInteger mult = Combination.Calculate(this.minesLeft - start, this.tilesOffEdge);
BigInteger totalTally = 0;
foreach (ProbabilityLine pl in edgeDetails.data) {
BigInteger mult = Combination.Calculate(this.minesLeft - start - pl.GetMineCount(), this.tilesOffEdge);
totalTally += mult * pl.GetSolutionCount();
for (int i = 0; i < boxes.Count; i++) {
if (edgeDetails.mask[i]) {
BigInteger work = pl.GetMineBoxCount(i) * mult;
tally[i] += work;
}
}
//mult = mult * (this.tilesOffEdge - start) / (start + 1);
//start++;
}
for (int i = 0; i < boxes.Count; i++) {
if (edgeDetails.mask[i]) {
probability[i] = Combination.DivideBigIntegerToDouble(tally[i], totalTally, 6) / boxes[i].GetTiles().Count;
}
}
int minIndex = -1;
for (int i = 0; i < boxes.Count; i++) {
if (edgeDetails.mask[i]) {
if (minIndex == -1 || probability[i] < probability[minIndex]) {
minIndex = i;
}
}
}
if (minIndex != -1) {
information.Write("Best guess is " + boxes[minIndex].GetTiles()[0].AsText() + " with " + (1 - probability[minIndex]));
} else {
information.Write("No Guess found");
}
}
}
// here we expand the localised solution to one across the whole board and
// sum them together to create a definitive probability for each box
private void CalculateBoxProbabilities() {
//long start = DateTime.Now.Ticks;
if (truncatedProbs) {
Console.WriteLine("probability line combining was truncated");
}
information.Write("Solution count multiplier is " + this.solutionCountMultiplier);
BigInteger[] tally = new BigInteger[this.boxes.Count];
// total game tally
BigInteger totalTally = 0;
// outside a box tally
BigInteger outsideTally = 0;
//console.log("There are " + this.heldProbs.length + " different mine counts on the edge");
bool[] emptyBox = new bool[boxes.Count];
for (int i = 0; i < emptyBox.Length; i++) {
emptyBox[i] = true;
}
int linesProcessed = 0;
// calculate how many mines
foreach (ProbabilityLine pl in this.heldProbs) {
//console.log("Mine count is " + pl.mineCount + " with solution count " + pl.solutionCount + " mineBoxCount = " + pl.mineBoxCount);
if (pl.GetMineCount() >= this.minTotalMines) { // if the mine count for this solution is less than the minimum it can't be valid
linesProcessed++;
//console.log("Mines left " + this.minesLeft + " mines on PL " + pl.mineCount + " squares left = " + this.squaresLeft);
BigInteger mult = SolverMain.Calculate(this.minesLeft - pl.GetMineCount(), this.tilesOffEdge); //# of ways the rest of the board can be formed
information.Write("Mines in solution " + pl.GetMineCount() + " solution count " + pl.GetSolutionCount() + " multiplier " + mult);
outsideTally = outsideTally + mult * new BigInteger(this.minesLeft - pl.GetMineCount()) * (pl.GetSolutionCount());
// this is all the possible ways the mines can be placed across the whole game
totalTally = totalTally + mult * (pl.GetSolutionCount());
for (int i = 0; i < emptyBox.Length; i++) {
if (pl.GetMineBoxCount(i) != 0) {
emptyBox[i] = false;
}
}
}
}
// determine how many clear squares there are
if (totalTally > 0) {
for (int i = 0; i < emptyBox.Length; i++) {
if (emptyBox[i]) {
clearCount = clearCount + boxes[i].GetTiles().Count;
}
}
}
this.finalSolutionCount = totalTally * solutionCountMultiplier;
information.Write("Game has " + this.finalSolutionCount + " candidate solutions");
}
public BigInteger GetSolutionCount() {
return this.finalSolutionCount;
}
public int getClearCount() {
return this.clearCount;
}
public int GetSolutionCountMagnitude() {
return (int)Math.Floor(BigInteger.Log10(this.finalSolutionCount));
}
public SolverInfo GetSolverInfo() {
return this.information;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperSolver {
public class SolverAction : GameAction, IComparable<SolverAction> {
public readonly double safeProbability;
public readonly bool isDead;
//public readonly bool isExcluded;
public SolverAction(SolverTile tile, ActionType action, double safeprob) : base(tile.x, tile.y, action) {
this.safeProbability = safeprob;
this.isDead = tile.IsDead();
//this.isExcluded = tile.IsExcluded();
}
public int CompareTo(SolverAction other) {
return safeProbability.CompareTo(other.safeProbability);
}
}
}

View File

@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace MinesweeperSolver {
class SolverActionHeader {
public readonly IList<SolverAction> solverActions;
public readonly IList<SolverAction> deadActions;
public SolverActionHeader() {
List<SolverAction> empty = new List<SolverAction>();
this.solverActions = empty.AsReadOnly();
this.deadActions = empty.AsReadOnly();
}
public SolverActionHeader(List<SolverAction> solverActions, List<SolverAction> deadActions) {
this.solverActions = solverActions.AsReadOnly();
this.deadActions = deadActions.AsReadOnly();
}
}
}

View File

@ -0,0 +1,377 @@
using MinesweeperControl;
using System;
using System.Collections.Generic;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperSolver {
public class SolverInfo {
private static readonly bool EXCLUDE_ON = false;
// contains information about what surrounds a tile
public class AdjacentInfo {
public readonly int mines;
public readonly int hidden;
public readonly int excluded;
public AdjacentInfo(int mines, int hidden, int excluded) {
this.mines = mines;
this.hidden = hidden;
this.excluded = excluded;
}
}
private readonly int key = new Random().Next();
public readonly GameDescription description;
public readonly bool verbose;
private GameStatus gameStatus = GameStatus.NotStarted;
private readonly SolverTile[,] tiles;
private int tilesLeft;
private ProbabilityEngine probabilityEngine = null; // this is the probability engine from the last solver run
private BruteForceAnalysis bfa = null; // this is the brute force analysis from the last solver run
private readonly HashSet<SolverTile> livingWitnesses = new HashSet<SolverTile>(20); // this is a set of tiles which are clear and have hidden neighbours.
private readonly HashSet<SolverTile> knownMines; // this is a set of tiles which are known to be mines.
private readonly HashSet<SolverTile> pendingClears = new HashSet<SolverTile>(); // this is a set of tiles which are clear and haven't yet been clicked.
private readonly HashSet<SolverTile> deadTiles = new HashSet<SolverTile>(); // this is a set of tiles which are known to be dead.
//private readonly HashSet<SolverTile> excludedWitnesses = new HashSet<SolverTile>(); // this is a set of witnesses which no longer influence the solving of the game.
//private readonly HashSet<SolverTile> excludedTiles = new HashSet<SolverTile>(); // this is a set of tiles which no longer influence the solving of the game.
//private int excludedMinesCount = 0; // number of mines which are with the exluded tiles area
private readonly List<SolverTile> newClears = new List<SolverTile>();
public SolverInfo(GameDescription description) : this(description, false) {
}
public SolverInfo(GameDescription description, bool verbose) {
this.description = description;
this.verbose = verbose;
tiles = new SolverTile[description.width, description.height];
// populate the grid with solver tiles showing what we know so far ... which is nothing
for (int x = 0; x < description.width; x++) {
for (int y = 0; y < description.height; y++) {
tiles[x, y] = new SolverTile(key, x, y);
}
}
tilesLeft = description.height * description.width;
knownMines = new HashSet<SolverTile>(description.mines);
}
public void AddInformation(GameResult information) {
//long start = DateTime.Now.Ticks;
gameStatus = information.status;
this.probabilityEngine = null; // previous probability engine invalidated
newClears.Clear();
// the game results tell us when a tile is cleared, flagged or unflagged
foreach (ActionResult ar in information.actionResults) {
if (ar.resultType == ResultType.Cleared) {
tilesLeft--;
tiles[ar.x, ar.y].SetValue(ar.value);
SolverTile tile = tiles[ar.x, ar.y];
newClears.Add(tile);
if (tile.IsDead()) {
deadTiles.Remove(tile);
}
//if (tile.IsExcluded()) {
// excludedTiles.Remove(tile);
//}
//pendingClears.Remove(tile);
} else if (ar.resultType == ResultType.Flagged) {
tiles[ar.x, ar.y].SetFlagged(true);
} else if (ar.resultType == ResultType.Hidden) {
tiles[ar.x, ar.y].SetFlagged(false);
} else if (ar.resultType == ResultType.Exploded) {
SolverTile tile = tiles[ar.x, ar.y];
tile.SetFlagged(false);
MineFound(tile);
this.bfa = null; // can't walk the bfa tree if we've trodden on a mine
}
}
// find and mark tiles as exhausted
//int removed = 0;
//if (newClears.Count > 0) {
foreach (SolverTile tile in livingWitnesses) {
if (AdjacentTileInfo(tile).hidden == 0) {
tile.SetExhausted();
}
}
// remove all exhausted tiles from the list of witnesses
livingWitnesses.RemoveWhere(x => x.IsExhausted());
//}
// add new witnesses which aren't exhausted
foreach (SolverTile newTile in newClears) {
AdjacentInfo adjInfo = AdjacentTileInfo(newTile);
if (adjInfo.hidden != 0) {
if (adjInfo.excluded != adjInfo.hidden) {
livingWitnesses.Add(newTile);
} else {
//excludedWitnesses.Add(newTile);
}
}
}
//Write("There are " + livingWitnesses.Count + " living witnesses (" + removed + " deleted)");
//Write("Adding Information to Solver took " + (DateTime.Now.Ticks - start) + " ticks");
}
public int GetMinesLeft() {
return description.mines - knownMines.Count; // mines left to find = mines in game - mines found
}
public int GetTilesLeft() {
return this.tilesLeft;
}
// sets the tile as a mine, stores it for future use and returns true if it is currently flagged
public bool MineFound(SolverTile tile) {
// if this is already known to be mine then nothing to do
if (tile.IsMine()) {
return tile.IsFlagged();
}
tilesLeft--;
tile.SetAsMine(key);
knownMines.Add(tile);
if (tile.IsDead()) {
deadTiles.Remove(tile); // remove the tile if it was on the dead list
}
//if (tile.IsExcluded()) {
// excludedTiles.Remove(tile); // remove the tile if it was excluded
// excludedMinesCount--;
//}
return tile.IsFlagged();
}
public void SetTileToDead(SolverTile tile) {
if (tile.IsMine()) {
Write("ERROR: Trying to set a mine tile to dead " + tile.AsText());
return;
}
tile.SetDead(key); // mark it
deadTiles.Add(tile); // and add to the set
}
public HashSet<SolverTile> GetDeadTiles() {
return deadTiles;
}
/*
public void ExcludeMines(int n) {
if (!EXCLUDE_ON) {
return;
}
this.excludedMinesCount = this.excludedMinesCount + n;
}
public int GetExcludedMineCount() {
return this.excludedMinesCount;
}
public void ExcludeTile(SolverTile tile) {
if (!EXCLUDE_ON) {
return;
}
tile.SetExcluded();
excludedTiles.Add(tile);
}
public HashSet<SolverTile> GetExcludedTiles() {
return this.excludedTiles;
}
/// <summary>
/// Move a tile from the list of witnesses to the list of excluded witnesses
/// </summary>
public void ExcludeWitness(SolverTile tile) {
if (!EXCLUDE_ON) {
return;
}
tile.SetExcluded();
if (livingWitnesses.Remove(tile)) {
excludedWitnesses.Add(tile);
}
}
public HashSet<SolverTile> GetExcludedWitnesses() {
return this.excludedWitnesses;
}
*/
public void SetBruteForceAnalysis(BruteForceAnalysis bfa) {
this.bfa = bfa;
}
public BruteForceAnalysis GetBruteForceAnalysis() {
return this.bfa;
}
public void SetProbabilityEngine(ProbabilityEngine pe) {
this.probabilityEngine = pe;
}
// returns the probability the tile is safe if known. Or -1 otherwise.
public double GetProbability(int x, int y) {
// if we are out of bounds then nothing to say
if (x < 0 || x >= description.width || y < 0 || y >= description.height) {
return -1;
}
SolverTile tile = tiles[x, y];
// an unflagged mine
if (tile.IsMine() && !tile.IsFlagged()) {
return 0;
}
// otherwise if revealed then nothing to say
if (!tile.IsHidden()) {
return -1;
}
//if (tile.IsExcluded()) {
// return -1;
//}
if (probabilityEngine != null) {
return probabilityEngine.GetProbability(tile);
} else if (pendingClears.Contains(tile)) {
return 1;
} else {
return -1;
}
}
//public bool IsTileDead(SolverTile tile) {
// return deadTiles.Contains(tile);
//}
// allow the gui to see if a tile is dead
//public bool IsTileDead(int x, int y) {
// return IsTileDead(tiles[x, y]);
//}
// Adds the tile to a list of known clears for future use
public void ClearFound(SolverTile tile) {
pendingClears.Add(tile);
}
// returns all the indices adjacent to this index
public List<SolverTile> GetAdjacentTiles(SolverTile tile) {
// have we already calculated the adjacent tiles
List<SolverTile> adjTiles = tile.GetAdjacentTiles();
if (adjTiles != null) {
return adjTiles;
}
adjTiles = new List<SolverTile>();
int first_row = Math.Max(0, tile.y - 1);
int last_row = Math.Min(description.height - 1, tile.y + 1);
int first_col = Math.Max(0, tile.x - 1);
int last_col = Math.Min(description.width - 1, tile.x + 1);
for (int r = first_row; r <= last_row; r++) {
for (int c = first_col; c <= last_col; c++) {
if (r != tile.y || c != tile.x) {
adjTiles.Add(tiles[c, r]);
}
}
}
// remember them for next time
tile.SetAdjacentTiles(adjTiles);
return adjTiles;
}
public AdjacentInfo AdjacentTileInfo(SolverTile tile) {
int hidden = 0;
int mines = 0;
int excluded = 0;
foreach (SolverTile adjTile in GetAdjacentTiles(tile)) {
if (adjTile.IsMine()) {
mines++;
} else if (adjTile.IsHidden() ) {
hidden++;
//if (adjTile.IsExcluded()) {
// excluded++;
//}
}
}
return new AdjacentInfo(mines, hidden, excluded);
}
public GameStatus GetGameStatus() {
return gameStatus;
}
public SolverTile GetTile(int x, int y) {
return tiles[x, y];
}
public HashSet<SolverTile> GetWitnesses() {
return livingWitnesses;
}
public HashSet<SolverTile> GetKnownMines() {
return knownMines;
}
public void Write(string text) {
if (verbose) {
Console.WriteLine(text);
}
}
}
}

View File

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

View File

@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Text;
using static MinesweeperSolver.SolverInfo;
namespace MinesweeperSolver {
public class SolverTile {
public readonly int x;
public readonly int y;
private readonly long key; // the key identifies which SolverInfo created these tiles and locks down some methods.
// details about me
private bool isMine = false;
private bool isFlagged = false;
private bool isHidden = true;
private int value = 0;
private bool isDead = false; // a dead tile is one which provides no information if clicked. Should be avoided unless all tiles are dead.
private bool exhausted = false; // an exhausted tile is where it is cleared and all its neighbours are either cleared or a mine
//private bool newGrowth = true; // a tile is new growth the first time it is revealed. Once it has been analysed by the Probability Engine it stops being new growth.
//private bool excluded = false; // an excluded tile is one which no longer needs to be included in the propability engine since it no longer affects other tiles
private List<SolverTile> adjacentTiles = null;
public SolverTile(long key, int x, int y) {
this.key = key;
this.x = x;
this.y = y;
}
public bool IsMine() {
return isMine;
}
// this should only be called from the SolverInfo class
public void SetAsMine(long key) {
if (key != this.key) {
throw new Exception("Unauthorised access to the SetAsMine method");
}
this.isMine = true;
this.isHidden = false; // no longer hidden we know it is a mine
}
public bool IsFlagged() {
return this.isFlagged;
}
public void SetFlagged(bool flagged) {
isFlagged = flagged;
}
public bool IsHidden() {
return this.isHidden;
}
public void SetValue(int value) {
this.isHidden = false;
this.value = value;
}
public int GetValue() {
return this.value;
}
public bool IsExhausted() {
return this.exhausted;
}
public void SetExhausted() {
this.exhausted = true;
}
// this should only be called from from the SolverInfo class
public void SetDead(long key) {
if (key != this.key) {
throw new Exception("Unauthorised access to the SetDead method");
}
this.isDead = true;
}
//public bool IsNewGrowth() {
// return newGrowth;
//}
//public void SetExamined() {
// this.newGrowth = false;
//}
public bool IsDead() {
return isDead;
}
public List<SolverTile> GetAdjacentTiles() {
return this.adjacentTiles;
}
public void SetAdjacentTiles(List<SolverTile> adjTiles) {
this.adjacentTiles = adjTiles;
}
/*
public void SetExcluded() {
this.excluded = true;
}
public bool IsExcluded() {
return this.excluded;
}
*/
public bool IsEqual(SolverTile tile) {
if (this.x == tile.x && this.y == tile.y) {
return true;
} else {
return false;
}
}
// returns true if the tile provided is adjacent to this tile
public bool IsAdjacent(SolverTile tile) {
var dx = Math.Abs(this.x - tile.x);
if (dx > 1) {
return false;
}
var dy = Math.Abs(this.y - tile.y);
if (dy > 1) {
return false;
}
if (dx == 0 && dy == 0) {
return false;
} else {
return true;
}
/*
// adjacent and not equal
if (dx < 2 && dy < 2 && !(dx == 0 && dy == 0)) {
return true;
} else {
return false;
}
*/
}
//<summary> return the number of hidden tiles shared by these tiles
public int NumberOfSharedHiddenTiles(SolverTile tile) {
// if the locations are too far apart they can't share any of the same squares
if (Math.Abs(tile.x - this.x) > 2 || Math.Abs(tile.y - this.y) > 2) {
return 0;
}
int count = 0;
foreach (SolverTile tile1 in this.GetAdjacentTiles()) {
if (!tile1.IsHidden()) {
continue;
}
foreach (SolverTile tile2 in tile.GetAdjacentTiles()) {
if (!tile2.IsHidden()) {
continue;
}
if (tile1.IsEqual(tile2)) { // if they share a tile then return true
count ++;
}
}
}
// no shared tile found
return count;
}
public string AsText() {
return "(" + x + "," + y + ")";
}
}
}

View File

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text;
namespace MinesweeperSolver {
public class Binomial {
private static readonly double LOG10 = Math.Log(10);
private readonly int max;
private readonly PrimeSieve ps;
private readonly BigInteger[,] binomialLookup;
private readonly int lookupLimit;
public Binomial(int max, int lookup) {
this.max = max;
ps = new PrimeSieve(this.max);
if (lookup < 10) {
lookup = 10;
}
this.lookupLimit = lookup;
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);
//Console.WriteLine("Binomial " + total + " choose " + choose + " is " + binomialLookup[total, choose]);
//} catch (Exception e) {
// throw e;
//}
}
}
}
public BigInteger Generate(int k, int n) {
if (n == 0 && k == 0) {
return BigInteger.One;
}
if (n < 1) {
throw new Exception("Binomial: 1 <= n required, but n was " + n);
}
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] != 0) { // it is zero when it hasn't been built yet
return binomialLookup[n, choose];
} else if (choose < 125) {
return Combination(choose, n);
} else if (n <= max) {
return CombinationLarge(choose, n);
} else {
return CombinationApprox(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 * new BigInteger(squares - i);
bot = bot * new BigInteger(i + 1);
}
BigInteger result = top / bot;
return result;
}
private BigInteger CombinationLarge(int k, int n) {
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;
foreach (int prime in ps.getPrimesIterable(2, n)) {
if (prime > nk) {
result = result * new BigInteger(prime);
continue;
}
if (prime > n2) {
continue;
}
if (prime > rootN) {
if (n % prime < k % prime) {
result = result * new BigInteger(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 * new BigInteger(p);
}
}
return result;
}
// use the stirling approximation for factorials to create an approximate combinatorial
public BigInteger CombinationApprox(int k, int n) {
double logComb = (LogFactorialApprox(n) - LogFactorialApprox(k) - LogFactorialApprox(n - k));
int power = (int) Math.Floor(logComb);
int dp = Math.Min(6, power);
//double value = Math.Exp((logComb - power + dp) * LOG10);
//BigInteger result = new BigInteger(sigDigit) * BigInteger.Pow(new BigInteger(10), power - dp);
// find the significant digits
int sigDigits = (int) Math.Round(Math.Pow(10, logComb - power + dp));
String scientific = "" + sigDigits + "E" + (power - dp);
BigInteger result = BigInteger.Parse(scientific, NumberStyles.AllowExponent);
return result;
}
// returns an approximation for Log(n!)
private double LogFactorialApprox(int n) {
return n * Math.Log10(n) + 0.5d * Math.Log10(2 * Math.PI * n);
}
}
}