Files
BH2023-Minesweeper/references/Minesweeper2/MinesweeperSolver/ProbabilityEngine.cs
2023-09-28 20:24:18 +08:00

2056 lines
77 KiB
C#

using System;
using System.Collections.Generic;
using System.Numerics;
using System.Security.Permissions;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperSolver {
public class ProbabilityEngine {
public enum Outcome { COMPLETED, DEAD_EDGE, LOCAL_CLEARS, ISOLATED_EDGE};
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 Outcome outcome;
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
// details about independent witnesses
private List<BoxWitness> independentWitnesses = new List<BoxWitness>();
private List<BoxWitness> dependentWitnesses = new List<BoxWitness>();
private int independentMines;
private BigInteger independentIterations = 1;
private int independentTiles = 0;
// local clears which means we can stop the probability engine early
private readonly List<SolverTile> localClears = new List<SolverTile>();
private readonly List<SolverTile> minesFound = new List<SolverTile>();
private readonly List<SolverTile> deadEdge = new List<SolverTile>();
private BigInteger deadEdgeSolutionCount;
private Cruncher isolatedEdgeCruncher;
private int minesLeft;
private int tilesLeft;
private int tilesOffEdge;
private int minTotalMines;
private 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 double bestProbability = 0;
private double bestEdgeProbability = 0;
private double offEdgeProbability = 0;
private BigInteger finalSolutionCount = 0;
private BigInteger solutionCountMultiplier = 1;
private bool truncatedProbs = false; // this gets set when the number of held probabilies exceeds the permitted threshold
private readonly SolverInfo information;
public ProbabilityEngine(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 array showing which boxes have been procesed this iteration - none have to start with
//for (var i = 0; i < this.boxes.length; i++) {
// this.mask.push(false);
//}
// look for places which could be dead
GetCandidateDeadLocations();
// 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) {
//console.log("Probability engine processing witness " + nextWitness.boxWitness.tile.asText());
// 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);
//if (this.workingProbs.length > 10) {
// console.log("Items in the working array = " + this.workingProbs.length);
//}
nextWitness = FindNextWitness(nextWitness);
}
// if we have completed the whole thing then calculate the probabilities
if (this.outcome == Outcome.COMPLETED) {
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);
//for (var j = 0; j < result.length; j++) {
// newProbs.push(result[j]);
//}
}
}
//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("Probability Engine 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);
//for (var j = 0; j < r1.length; j++) {
// result.push(r1[j]);
//}
//result.push(distributeMissingMines(npl, nw, missingMines - i, index + 1));
}
return result;
}
// create a new probability line by taking the old and adding the mines to the new Box
private ProbabilityLine ExtendProbabilityLine(ProbabilityLine pl, Box newBox, int mines) {
//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.solutionCount = pl.solutionCount;
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 = new List<ProbabilityLine>(this.workingProbs);
EdgeStore edge = new EdgeStore(crunched, this.mask);
edgeStore.Add(edge); ; // 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
// try and determine if every tile on this edge is dead
if (crunched.Count == 1) { // if the edge has the same number of mines for every possible solution
bool edgeDead = true;
deadEdgeSolutionCount = crunched[0].GetSolutionCount();
for (int i = 0; i < this.mask.Length; i++) {
if (this.mask[i]) { // if any of the boxes on this edge is not dead then the edge isn't dead
foreach (SolverTile tile in boxes[i].GetTiles()) {
if (!tile.IsDead()) {
edgeDead = false;
break;
}
}
if (!edgeDead) {
break;
}
}
}
// if the edge is dead add all the tiles in to a list to be used later
if (edgeDead) {
information.Write("Dead edge found with " + deadEdgeSolutionCount + " solutions");
for (int i = 0; i < this.mask.Length; i++) {
if (this.mask[i]) {
deadEdge.AddRange(boxes[i].GetTiles());
}
}
} else {
// no point checking if it is too large to process
if (deadEdgeSolutionCount <= SolverMain.MAX_BFDA_SOLUTIONS) {
CheckEdgeIsIsolated(crunched[0]);
}
}
}
this.mask = new bool[boxes.Count];
}
// an edge is isolated if every tile on it is completely surrounded by boxes also on the same edge
private bool CheckEdgeIsIsolated(ProbabilityLine probabilityLine) {
HashSet<SolverTile> edgeTiles = new HashSet<SolverTile>();
HashSet<BoxWitness> edgeWitnesses = new HashSet<BoxWitness>();
bool everything = true;
// load each tile on this edge into a set
for (int i = 0; i < this.mask.Length; i++) {
if (this.mask[i]) {
edgeTiles.UnionWith(boxes[i].GetTiles());
edgeWitnesses.UnionWith(boxes[i].GetBoxWitnesses());
} else {
everything = false;
}
}
// if this edge is everything then it isn't an isolated edge
if (everything) {
return false;
}
// check whether every tile adjacent to the tiles on the edge is itself on the edge
List<SolverTile> adjTiles = new List<SolverTile>();
for (int i = 0; i < this.mask.Length; i++) {
if (this.mask[i]) {
foreach (SolverTile tile in boxes[i].GetTiles()) {
adjTiles.Clear();
foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) {
if (adjTile.IsHidden()) {
if (!edgeTiles.Contains(adjTile)) {
return false;
}
}
}
}
}
}
information.Write("*** Isolated Edge found ***");
List<SolverTile> tiles = new List<SolverTile>(edgeTiles);
List<BoxWitness> witnesses = new List<BoxWitness>(edgeWitnesses);
int mines = probabilityLine.GetMineCount();
//SequentialIterator iterator = new SequentialIterator(mines, tiles.Count);
WitnessWebIterator iterator = new WitnessWebIterator(information, null, witnesses, tiles, mines, tiles.Count, -1);
BruteForceAnalysis bfa = new BruteForceAnalysis(information, tiles, 1000000, null);
Cruncher cruncher = new Cruncher(information, iterator, witnesses, bfa);
this.isolatedEdgeCruncher = cruncher;
return true;
}
// 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);
//npl.hashCount[i] = epl.hashCount[i].add(pl.hashCount[i]);
}
/*
newpl.SetSolutionCount(pl.GetSolutionCount() * epl.GetSolutionCount());
for (int k = 0; k < this.boxes.Count; k++) {
BigInteger w1 = pl.GetMineBoxCount(k) * epl.GetSolutionCount();
BigInteger w2 = epl.GetMineBoxCount(k) * pl.GetSolutionCount();
newpl.SetMineBoxCount(k, w1 + w2);
//npl.hashCount[i] = epl.hashCount[i].add(pl.hashCount[i]);
}
*/
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) {
//if (pl.GetMineCount() > startMC + 9) {
// //Console.WriteLine("Start mc = " + startMC + ", broke on " + pl.GetMineCount());
// truncatedProbs = true;
// break;
//}
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));
//npl.hashCount[i] = npl.hashCount[i].add(pl.hashCount[i]);
}
}
this.heldProbs.Add(npl);
//if (this.heldProbs.length > 10) {
// console.log("Items in the held array = " + this.heldProbs.length);
//}
/*
for (Box b: boxes) {
System.out.print(b.getSquares().size() + " ");
}
System.out.println("");
for (ProbabilityLine pl: heldProbs) {
System.out.print("Mines = " + pl.mineCount + " solutions = " + pl.solutionCount + " boxes: ");
for (int i=0; i < pl.mineBoxCount.length; i++) {
System.out.print(" " + pl.mineBoxCount[i]);
}
System.out.println("");
}
*/
}
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");
//SolverMain.Write(target.Count + " Probability Lines compressed to " + result.Count);
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);
//if (pl.mineBoxCount[i].signum() == 0) {
// npl.hashCount[i] = npl.hashCount[i].subtract(pl.hash.multiply(BigInteger.valueOf(boxes.get(i).getSquares().size()))); // treat no mines as -1 rather than zero
//} else {
// npl.hashCount[i] = npl.hashCount[i].add(pl.mineBoxCount[i].multiply(pl.hash));
//}
}
}
}
// 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 with " + workingProbs.Count + " solution lines");
}
//Console.WriteLine("Edge has " + this.workingProbs.Count + " probability lines");
//if we are down here then there is no witness which is on the boundary, so we have processed a complete set of independent witnesses
//information.Write("Considering " + this.workingProbs.Count + " probability lines");
//foreach (ProbabilityLine wp in this.workingProbs) {
// information.Write("Probability line has " + wp.GetMineCount() + " mines and " + wp.GetSolutionCount() + " solutions");
//}
// see if any of the tiles on this independent edge are dead
CheckCandidateDeadLocations();
// reduce the edge by sorting and consolidating by mine count
this.workingProbs = CrunchByMineCount(this.workingProbs);
information.Write("Independent edge reduced to " + workingProbs.Count + " solution lines");
// look to see if this sub-section of the edge has any certain clears
for (int i = 0; i < this.mask.Length; i++) {
if (this.mask[i]) {
bool isClear = true;
foreach (ProbabilityLine wp in this.workingProbs) {
if (wp.GetMineBoxCount(i) != BigInteger.Zero) {
//information.Write("Box " + i + " has non-zero mine box count " + wp.GetMineBoxCount(i));
isClear = false;
break;
}
}
if (isClear) {
// if the box is locally clear then store the tiles in it
foreach (SolverTile tile in this.boxes[i].GetTiles()) {
information.Write(tile.AsText() + " has been determined locally to be clear");
this.localClears.Add(tile);
}
}
bool isFlag = true;
foreach (ProbabilityLine wp in this.workingProbs) {
if (wp.GetMineBoxCount(i) != wp.GetSolutionCount() * this.boxes[i].GetTiles().Count) {
isFlag = false;
break;
}
}
if (isFlag) {
foreach (SolverTile tile in this.boxes[i].GetTiles()) {
information.Write(tile.AsText() + " has been determined locally to be a mine");
minesFound.Add(tile);
//information.MineFound(tile);
}
}
}
}
// if we have found some local clears then stop and use these
if (this.localClears.Count > 0) {
this.outcome = Outcome.LOCAL_CLEARS;
information.Write(this.localClears.Count + " Local clears have been found");
return null;
}
//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();
if (deadEdge.Count != 0) {
this.outcome = Outcome.DEAD_EDGE;
return null;
}
if (this.isolatedEdgeCruncher != null) {
this.outcome = Outcome.ISOLATED_EDGE;
return null;
}
// 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) {
// if we have found some local clears then stop and use these
//if (this.localClears.Count > 0) {
// information.Write("In total " + this.localClears.Count + " Local clears have been found");
// return 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");
this.outcome = Outcome.COMPLETED;
}
// 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");
}
}
}
// check the candidate dead locations with the information we have
private void CheckCandidateDeadLocations() {
bool completeScan;
if (this.tilesOffEdge == 0) {
completeScan = true; // this indicates that every box has been considered in one sweep (only 1 independent edge)
for (int i = 0; i < this.mask.Length; i++) {
if (!this.mask[i]) {
completeScan = false;
break;
}
}
if (completeScan) {
information.Write("This is a complete scan");
} else {
information.Write("This is not a complete scan");
}
} else {
completeScan = false;
information.Write("This is not a complete scan because there are squares off the edge");
}
foreach (DeadCandidate dc in this.deadCandidates) {
if (dc.IsAlive()) { // if this location isn't dead then no need to check any more
continue;
}
// only do the check if all the boxes have been analysed in this probability iteration
int boxesInScope = 0;
foreach (Box b in dc.GetGoodBoxes()) {
if (this.mask[b.GetUID()]) {
boxesInScope++;
}
}
foreach (Box b in dc.GetBadBoxes()) {
if (this.mask[b.GetUID()]) {
boxesInScope++;
}
}
if (boxesInScope == 0) {
continue;
} else if (boxesInScope != dc.GetGoodBoxes().Count + dc.GetBadBoxes().Count) {
information.Write("Location " + dc.GetCandidate().AsText() + " has some boxes in scope and some out of scope so assumed alive");
dc.SetAlive();
continue;
}
bool okay = true;
int mineCount = 0;
foreach (ProbabilityLine pl in this.workingProbs) {
if (completeScan && pl.GetMineCount() != this.minesLeft) {
continue;
}
// ignore probability lines where the candidate is a mine
if (pl.GetMineBoxCount(dc.GetMyBox().GetUID()) == dc.GetMyBox().GetTiles().Count) {
mineCount++;
continue;
}
// all the bad boxes must be zero
foreach (Box b in dc.GetBadBoxes()) {
int requiredMines;
if (b.GetUID() == dc.GetMyBox().GetUID()) {
requiredMines = b.GetTiles().Count - 1;
} else {
requiredMines = b.GetTiles().Count;
}
if (pl.GetMineBoxCount(b.GetUID()) != 0 && pl.GetMineBoxCount(b.GetUID()) != requiredMines) {
//if (pl.GetMineBoxCount(b.GetUID()) != 0) {
information.Write("Location " + dc.GetCandidate().AsText() + " is not dead because a bad box is neither empty nor full of mines");
okay = false;
break;
}
}
// if we aren't okay then no need to continue
if (!okay) {
break;
}
BigInteger tally = 0;
// the number of mines in the good boxes must always be the same
foreach (Box b in dc.GetGoodBoxes()) {
tally = tally + pl.GetMineBoxCount(b.GetUID());
}
//boardState.display("Location " + dc.candidate.display() + " has mine tally " + tally);
if (!dc.CheckTotalOkay(tally)) {
information.Write("Location " + dc.GetCandidate().AsText() + " is not dead because the sum of mines in good boxes is not constant. Was "
+ dc.GetTotal() + " now " + tally + ". Mines in probability line " + pl.GetMineCount());
okay = false;
break;
}
}
// if a check failed or this tile is a mine for every solution then it is alive
if (!okay || mineCount == this.workingProbs.Count) {
dc.SetAlive();
} else {
information.Write(dc.GetCandidate().AsText() + " is dead");
//this.deadTiles.Add(dc.GetCandidate());
information.SetTileToDead(dc.GetCandidate()); // set the tile as dead. A dead tile can never be resurrected.
}
}
}
// find a list of locations which could be dead
private void GetCandidateDeadLocations() {
// for each square on the edge
foreach (SolverTile tile in this.witnessed) {
// if we have already decided this tile is dead not much more to do
if (tile.IsDead()) {
continue;
}
List<Box> adjBoxes = GetAdjacentBoxes(tile);
if (adjBoxes == null) { // this happens when the square isn't fully surrounded by boxes
continue;
}
DeadCandidate dc = new DeadCandidate(tile, GetBox(tile));
foreach (Box box in adjBoxes) {
bool good = true;
foreach (SolverTile square in box.GetTiles()) {
if (!square.IsAdjacent(tile) && !square.IsEqual(tile)) {
good = false;
break;
}
}
if (good) {
dc.AddToGoodBoxes(box);
} else {
dc.AddToBadBoxes(box);
}
}
if (dc.GetGoodBoxes().Count == 0 && dc.GetBadBoxes().Count == 0) {
information.SetTileToDead(tile); // set the tile as dead. A dead tile can never be resurrected.
} else {
this.deadCandidates.Add(dc);
}
}
foreach (DeadCandidate dc in this.deadCandidates) {
information.Write(dc.GetCandidate().AsText() + " is candidate dead with " + dc.GetGoodBoxes().Count + " good boxes and " + dc.GetBadBoxes().Count + " bad boxes");
}
}
// get the box containing this tile
private Box GetBox(SolverTile tile) {
foreach (Box b in this.boxes) {
if (b.Contains(tile)) {
return b;
}
}
return null;
}
// return all the boxes adjacent to this tile
private List<Box> GetAdjacentBoxes(SolverTile loc) {
List<Box> result = new List<Box>();
// get each adjacent location
foreach (SolverTile adjLoc in information.GetAdjacentTiles(loc)) {
// we only want adjacent tile which are hidden (i.e. not clear and not mines)
if (!adjLoc.IsHidden()) {
continue;
}
// lookup the box this adjacent tile is in
boxLookup.TryGetValue(adjLoc, out Box box);
// if a box can't be found for the adjacent tile then the candidate can't be dead
if (box == null) {
return null;
}
// make sure we only include the box once
bool alreadyIncluded = false;
foreach (Box box1 in result) {
if (box.GetUID() == box1.GetUID()) {
alreadyIncluded = true;
break;
}
}
// if not add it
if (!alreadyIncluded) {
result.Add(box);
}
}
return result;
}
// determine a set of independent witnesses which can be used to brute force the solution space more efficiently then a basic 'pick r from n'
public void GenerateIndependentWitnesses() {
this.independentTiles = 0;
// find a set of witnesses which don't share any squares (there can be many of these, but we just want one to use with the brute force iterator)
foreach (BoxWitness w in this.prunedWitnesses) {
//console.log("Checking witness " + w.tile.asText() + " for independence");
//if (w.GetTile().IsExcluded()) {
// //information.Write("Witness " + w.GetTile().AsText() + " is excluded");
// continue; // don't process excluded witnesses
//}
bool okay = true;
foreach (BoxWitness iw in this.independentWitnesses) {
if (w.Overlap(iw)) {
okay = false;
break;
}
}
// split the witnesses into dependent ones and independent ones
if (okay) {
information.Write(w.GetTile().AsText() + " is independent witness");
this.independentTiles = this.independentTiles + w.GetAdjacentTiles().Count;
this.independentIterations = this.independentIterations * SolverMain.Calculate(w.GetMinesToFind(), w.GetAdjacentTiles().Count);
this.independentMines = this.independentMines + w.GetMinesToFind();
this.independentWitnesses.Add(w);
} else {
this.dependentWitnesses.Add(w);
}
}
information.Write("Calculated " + this.independentWitnesses.Count + " independent witnesses and " + this.dependentWitnesses.Count + " dependent witnesses");
}
// 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");
int lowesti = 0;
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 j = 0; j < tally.Length; j++) {
//tally[j] = tally[j] + mult * pl.GetMineBoxCount(j);
tally[j] = tally[j] + (mult * pl.GetMineBoxCount(j)) / new BigInteger(this.boxes[j].GetTiles().Count);
//hashTally[i] = hashTally[i].add(pl.hashCount[i]);
}
if (tally.Length > 0) {
lowesti = 0;
for (int j = 1; j < tally.Length; j++) {
if (tally[j] < tally[lowesti]) {
lowesti = j;
}
}
//Console.WriteLine("Lowest mine count after first " + linesProcessed + " probability lines is at " + boxes[lowesti].GetTiles()[0].AsText());
}
}
}
// for each box calcaulate a probability
minesFound.Clear(); // clear down any mines we have found locally as we'll find them again here
for (int i = 0; i < this.boxes.Count; i++) {
if (totalTally != 0) {
if (tally[i] == totalTally) { // a mine
information.Write("Box " + i + " contains all mines");
foreach (SolverTile tile in boxes[i].GetTiles()) {
minesFound.Add(tile);
//information.MineFound(tile);
}
boxes[i].SetProbability(0);
} else {
//this.boxProb[i] = 1 - Combination.DivideBigIntegerToDouble(tally[i], totalTally, 6);
boxes[i].SetProbability(1 - Combination.DivideBigIntegerToDouble(tally[i], totalTally, 6));
}
} else {
information.Write("total Tally is zero!");
boxes[i].SetProbability(0);
//this.boxProb[i] = 0;
}
}
/*
for (int i = 0; i < hashTally.length; i++) {
//solver.display(boxes.get(i).getSquares().size() + " " + boxes.get(i).getSquares().get(0).display() + " " + hashTally[i].toString());
for (int j = i + 1; j < hashTally.length; j++) {
//BigInteger hash1 = hashTally[i].divide(BigInteger.valueOf(boxes.get(i).getSquares().size()));
//BigInteger hash2 = hashTally[j].divide(BigInteger.valueOf(boxes.get(j).getSquares().size()));
if (hashTally[i].compareTo(hashTally[j]) == 0 && boxes.get(i).getSquares().size() == 1 && boxes.get(j).getSquares().size() == 1) {
//if (hash1.compareTo(hash2) == 0) {
addLinkedLocation(linkedLocations, boxes.get(i), boxes.get(j));
addLinkedLocation(linkedLocations, boxes.get(j), boxes.get(i));
//solver.display("Box " + boxes.get(i).getSquares().get(0).display() + " is linked to Box " + boxes.get(j).getSquares().get(0).display() + " prob " + boxProb[i]);
}
// if one hasTally is the negative of the other then i flag <=> j clear
if (hashTally[i].compareTo(hashTally[j].negate()) == 0 && boxes.get(i).getSquares().size() == 1 && boxes.get(j).getSquares().size() == 1) {
//if (hash1.compareTo(hash2.negate()) == 0) {
//solver.display("Box " + boxes.get(i).getSquares().get(0).display() + " is contra linked to Box " + boxes.get(j).getSquares().get(0).display() + " prob " + boxProb[i] + " " + boxProb[j]);
addLinkedLocation(contraLinkedLocations, boxes.get(i), boxes.get(j));
addLinkedLocation(contraLinkedLocations, boxes.get(j), boxes.get(i));
}
}
}
*/
// sort so that the locations with the most links are at the top
//Collections.sort(linkedLocations, LinkedLocation.SORT_BY_LINKS_DESC);
// avoid divide by zero
if (this.tilesOffEdge != 0 && totalTally != 0) {
//offEdgeProbability = 1 - outsideTally / (totalTally * BigInt(this.squaresLeft));
this.offEdgeProbability = 1 - Combination.DivideBigIntegerToDouble(outsideTally, totalTally * new BigInteger(this.tilesOffEdge), 6);
} else {
this.offEdgeProbability = 0;
}
this.finalSolutionCount = totalTally * solutionCountMultiplier;
// see if we can find a guess which is better than outside the boxes
double hwm = this.offEdgeProbability;
//offEdgeBest = true;
foreach (Box b in this.boxes) {
// a box is dead if all its tiles are dead
bool boxLiving = false;
foreach (SolverTile tile in b.GetTiles()) {
if (!tile.IsDead()) {
boxLiving = true;
break;
}
}
//double prob = this.boxProb[b.GetUID()];
double prob = b.GetProbability();
if (boxLiving || prob == 1) { // if living or 100% safe then consider this probability
if (this.bestEdgeProbability <= prob) {
this.bestEdgeProbability = prob;
}
}
}
boxes.Sort(Box.CompareByProbabilityDescending);
this.bestProbability = Math.Max(this.bestEdgeProbability, this.offEdgeProbability);
information.Write("Best edge probability is " + this.bestEdgeProbability);
information.Write("Off edge probability is " + this.offEdgeProbability);
information.Write("Best probability is " + this.bestProbability);
information.Write("Game has " + this.finalSolutionCount + " candidate solutions");
//Console.WriteLine("Calculate probability took " + (DateTime.Now.Ticks - start) + " ticks");
}
public List<SolverAction> GetBestCandidates(double freshhold) {
List<SolverAction> best = new List<SolverAction>();
//solver.display("Squares left " + this.squaresLeft + " squares analysed " + web.getSquares().size());
// if the outside probability is the best then return an empty list
double test;
//if (offEdgeBest) {
// solver.display("Best probability is off the edge " + bestProbability + " but will look for options on the edge only slightly worse");
// //test = bestProbability.multiply(Solver.EDGE_TOLERENCE);
// test = bestProbability.multiply(freshhold);
//} else
if (this.bestProbability == 1) { // if we have a probability of one then don't allow lesser probs to get a look in
test = this.bestProbability;
} else {
test = this.bestProbability * freshhold;
}
information.Write("Best probability is " + this.bestProbability + " freshhold is " + test);
foreach (Box b in boxes) {
information.Write("Box has probability " + b.GetProbability());
if (b.GetProbability() >= test) {
foreach (SolverTile tile in b.GetTiles()) {
if (!tile.IsDead() || b.GetProbability() == 1) { // if the tile isn't dead or is certainly safe use it
if (b.GetProbability() == 0) { // edge case here when the only non-dead tiles left happen to be mines
information.MineFound(tile);
best.Add(new SolverAction(tile, ActionType.Flag, b.GetProbability()));
} else {
best.Add(new SolverAction(tile, ActionType.Clear, b.GetProbability()));
}
}
}
} else {
break;
}
}
/*
for (int i = 0; i < this.boxProb.Length; i++) {
if (this.boxProb[i] >= test) {
foreach (SolverTile tile in this.boxes[i].GetTiles()) {
if (!tile.IsDead()) {
if (this.boxProb[i] == 0) { // edge case here when the only non-dead tiles left happen to be mines
information.MineFound(tile);
best.Add(new SolverAction(tile, ActionType.Flag, this.boxProb[i]));
} else {
best.Add(new SolverAction(tile, ActionType.Clear, this.boxProb[i]));
}
}
}
}
}
*/
// sort in to best order
best.Sort();
return best;
}
public Outcome GetOutcome() {
return this.outcome;
}
// probability that any square off the edge is a mine
public double GetOffEdgeProbability() {
return this.offEdgeProbability;
}
public BigInteger GetSolutionCount() {
return this.finalSolutionCount;
}
public int GetSolutionCountMagnitude() {
return (int)Math.Floor(BigInteger.Log10(this.finalSolutionCount));
}
// returns the propability of this tile being safe
public double GetProbability(SolverTile tile) {
boxLookup.TryGetValue(tile, out Box box);
if (box == null) {
return GetOffEdgeProbability();
} else {
return box.GetProbability();
}
}
public double GetBestEdgeProbability() {
return this.bestEdgeProbability;
}
// returns a list of tiles that are clears found by local analysis
public List<SolverTile> GetLocalClears() {
return this.localClears;
}
// returns a list of tiles which are mines
public List<SolverTile> GetMinesFound() {
return this.minesFound;
}
public bool getTruncated() {
return this.truncatedProbs;
}
public List<BoxWitness> GetIndependentWitnesses() {
return this.independentWitnesses;
}
public List<BoxWitness> GetDependentWitnesses() {
return this.dependentWitnesses;
}
public BigInteger GetIndependentIterations() {
return this.independentIterations;
}
public int GetIndependentTiles() {
return this.independentTiles;
}
public int GetIndependentMines() {
return this.independentMines;
}
public SolverInfo GetSolverInfo() {
return this.information;
}
/*
* returns the dead edge if there is one
*/
public List<SolverTile> GetDeadEdge() {
return deadEdge;
}
public BigInteger GetDeadEdgeSolutionCount() {
return this.deadEdgeSolutionCount;
}
/*
* returns the isolated edge if there is one
*/
public Cruncher getIsolatedEdgeCruncher() {
return isolatedEdgeCruncher;
}
}
/*
* Used to hold a solution
*/
public class ProbabilityLine : IComparable<ProbabilityLine> {
private int mineCount = 0;
private BigInteger solutionCount;
private BigInteger[] mineBoxCount;
public ProbabilityLine(int boxCount) {
this.mineCount = 0;
this.solutionCount = 0;
this.mineBoxCount = new BigInteger[boxCount];
}
public void SetSolutionCount(BigInteger solutionCount) {
this.solutionCount = solutionCount;
}
public BigInteger GetSolutionCount() {
return this.solutionCount;
}
public BigInteger GetMineBoxCount(int i) {
return mineBoxCount[i];
}
public void SetMineBoxCount(int i, BigInteger n) {
this.mineBoxCount[i] = n;
}
public int GetMineCount() {
return this.mineCount;
}
public void SetMineCount(int count) {
this.mineCount = count;
}
public void CopyMineBoxCount(ProbabilityLine pl) {
Array.Copy(pl.mineBoxCount, this.mineBoxCount, this.mineBoxCount.Length);
}
// sort by the number of mines in the solution
public int CompareTo(ProbabilityLine o) {
return this.mineCount - o.mineCount;
}
}
public class EdgeStore {
public readonly List<ProbabilityLine> data;
public bool[] mask;
public EdgeStore(List<ProbabilityLine> data, bool[] mask) {
this.data = data;
this.mask = mask;
}
public static int SortByLineCount(EdgeStore a, EdgeStore b) {
return a.data.Count - b.data.Count;
}
}
// used to hold what we need to analyse next
public class NextWitness {
private BoxWitness boxWitness;
private List<Box> oldBoxes = new List<Box>();
private List<Box> newBoxes = new List<Box>();
public NextWitness(BoxWitness boxWitness) {
this.boxWitness = boxWitness;
//boxWitness.GetTile().SetExamined(); // set this tile as now longer new growth
foreach (Box box in this.boxWitness.GetBoxes()) {
if (box.IsProcessed()) {
this.oldBoxes.Add(box);
} else {
this.newBoxes.Add(box);
}
}
}
public BoxWitness GetBoxWitness() {
return this.boxWitness;
}
public List<Box> GetNewBoxes() {
return this.newBoxes;
}
public List<Box> GetOldBoxes() {
return this.oldBoxes;
}
}
// holds a witness and all the Boxes adjacent to it
public class BoxWitness {
private readonly SolverTile tile;
private readonly List<Box> boxes = new List<Box>(); // adjacent boxes being witnessed
private readonly List<SolverTile> tiles = new List<SolverTile>(); // adjacent tiles being witnessed
private bool processed = false;
private readonly int minesToFind;
public BoxWitness(SolverInfo information, SolverTile tile) {
this.tile = tile;
this.minesToFind = tile.GetValue();
List<SolverTile> adjTiles = information.GetAdjacentTiles(tile);
// determine how many mines are left to find and store adjacent tiles
foreach (SolverTile adjTile in adjTiles) {
if (adjTile.IsMine()) {
this.minesToFind--;
} else if (adjTile.IsHidden()) {
this.tiles.Add(adjTile);
}
}
}
public bool IsProcessed() {
return processed;
}
public void SetProcessed(bool processed) {
this.processed = processed;
}
//public bool HasNewGrowth() {
// return tile.IsNewGrowth();
//}
// gets the tile which is the witness
public SolverTile GetTile() {
return tile;
}
public List<SolverTile> GetAdjacentTiles() {
return this.tiles;
}
// returns the boxes that are witnessed by this BoxWitness
public List<Box> GetBoxes() {
return this.boxes;
}
public int GetMinesToFind() {
return this.minesToFind;
}
public bool Overlap(BoxWitness boxWitness) {
// if the locations are too far apart they can't share any of the same squares
if (Math.Abs(boxWitness.tile.x - this.tile.x) > 2 || Math.Abs(boxWitness.tile.y - this.tile.y) > 2) {
return false;
}
foreach (SolverTile tile1 in boxWitness.tiles) {
foreach (SolverTile tile2 in this.tiles) {
if (tile1.IsEqual(tile2)) { // if they share a tile then return true
return true;
}
}
}
// no shared tile found
return false;
}
// if two witnesses have the same Squares around them they are equivalent
public bool Equivalent(BoxWitness boxWitness) {
// if the number of squares is different then they can't be equivalent
if (this.tiles.Count != boxWitness.tiles.Count) {
return false;
}
// if the locations are too far apart they can't share the same squares
if (Math.Abs(boxWitness.tile.x - this.tile.x) > 2 || Math.Abs(boxWitness.tile.y - this.tile.y) > 2) {
return false;
}
foreach (SolverTile l1 in this.tiles) {
bool found = false;
foreach (SolverTile l2 in boxWitness.tiles) {
if (l1.IsEqual(l2)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
// add an adjacdent box
public void AddBox(Box box) {
this.boxes.Add(box);
}
}
// information about the boxes surrounding a dead candidate
public class DeadCandidate {
private readonly SolverTile candidate;
private readonly Box myBox;
private bool isAlive = false;
private List<Box> goodBoxes = new List<Box>();
private List<Box> badBoxes = new List<Box>();
private bool firstCheck = true;
private BigInteger total;
public DeadCandidate(SolverTile tile, Box myBox) {
this.candidate = tile;
this.myBox = myBox;
}
public SolverTile GetCandidate() {
return this.candidate;
}
public bool IsAlive() {
return this.isAlive;
}
public void SetAlive() {
this.isAlive = true;
}
public BigInteger GetTotal() {
return this.total;
}
public Box GetMyBox() {
return this.myBox;
}
// checks that the total is the same as previous totals
public bool CheckTotalOkay(BigInteger total) {
if (firstCheck) {
this.total = total;
firstCheck = false;
return true;
} else {
if (this.total != total) {
return false;
} else {
return true;
}
}
}
public List<Box> GetGoodBoxes() {
return this.goodBoxes;
}
public void AddToGoodBoxes(Box b) {
this.goodBoxes.Add(b);
}
public List<Box> GetBadBoxes() {
return this.badBoxes;
}
public void AddToBadBoxes(Box b) {
this.badBoxes.Add(b);
}
}
// a box is a group of tiles which share the same witnesses
public class Box {
private bool processed = false;
private readonly int uid;
private int minMines;
private int maxMines;
//private bool dead = true; // a box is dead if all its tiles are dead
private double safeProbability;
private readonly List<BoxWitness> boxWitnesses = new List<BoxWitness>();
private readonly List<SolverTile> tiles = new List<SolverTile>();
public Box(List<BoxWitness> boxWitnesses, SolverTile tile, int uid) {
this.uid = uid;
this.tiles.Add(tile);
//if (!tile.IsDead()) {
// dead = false;
//}
foreach (BoxWitness bw in boxWitnesses) {
if (tile.IsAdjacent(bw.GetTile())) {
this.boxWitnesses.Add(bw);
bw.AddBox(this);
}
}
//console.log("Box created for tile " + tile.asText() + " with " + this.boxWitnesses.length + " witnesses");
}
public bool IsProcessed() {
return this.processed;
}
public void SetProcessed(bool processed) {
this.processed = processed;
}
public int GetMinMines() {
return this.minMines;
}
public int GetMaxMines() {
return this.maxMines;
}
public int GetUID() {
return this.uid;
}
public List<SolverTile> GetTiles() {
return this.tiles;
}
public List<BoxWitness> GetBoxWitnesses() {
return this.boxWitnesses;
}
public double GetProbability() {
return this.safeProbability;
}
public void SetProbability(double probability) {
this.safeProbability = probability;
}
//public bool IsDead() {
// return this.dead;
//}
// if the tiles surrounding witnesses equal the boxes then it fits
public bool Fits(SolverTile tile, int count) {
// a tile can't share the same witnesses for this box if they have different numbers
if (count != this.boxWitnesses.Count) {
return false;
}
foreach (BoxWitness bw in this.boxWitnesses) {
if (!bw.GetTile().IsAdjacent(tile)) {
return false;
}
}
//console.log("Tile " + tile.asText() + " fits in box with tile " + this.tiles[0].asText());
return true;
}
/*
* Once all the squares have been added we can do some calculations
*/
public void Calculate(int minesLeft) {
this.maxMines = Math.Min(this.tiles.Count, minesLeft); // can't have more mines then there are tiles to put them in or mines left to discover
this.minMines = 0;
foreach (BoxWitness bw in this.boxWitnesses) {
if (bw.GetMinesToFind() < this.maxMines) { // can't have more mines than the lowest constraint
this.maxMines = bw.GetMinesToFind();
}
}
}
// add a new tile to the box
public void Add(SolverTile tile) {
this.tiles.Add(tile);
//if (!tile.IsDead()) {
// dead = false;
//}
}
public bool Contains(SolverTile tile) {
// return true if the given tile is in this box
foreach (SolverTile tile1 in this.tiles) {
if (tile1.IsEqual(tile)) {
return true;
}
}
return false;
}
public static int CompareByProbabilityDescending(Box x, Box y) {
return -x.safeProbability.CompareTo(y.safeProbability);
}
}
}