272 lines
9.0 KiB
TypeScript
272 lines
9.0 KiB
TypeScript
import {range} from "./Common";
|
|
import {Field, Square} from "./Field";
|
|
|
|
|
|
/**
|
|
* A solver for a game of Minesweeper.
|
|
*/
|
|
export class Solver {
|
|
/**
|
|
* Solves the given field as far as the algorithm is able to.
|
|
*
|
|
* @param field the field to solve
|
|
*/
|
|
solve(field: Field) {
|
|
if (!field.started)
|
|
field.getSquareOrElse(Math.floor(field.width / 2), Math.floor(field.height / 2)).uncover();
|
|
|
|
let flagCount = -1;
|
|
let coveredCount = -1;
|
|
while (true) {
|
|
this.bigStep(field);
|
|
|
|
const newFlagCount = field.squareList.filter(it => it.hasFlag).length;
|
|
const newCoveredCount = field.coveredRemaining;
|
|
if (newFlagCount === flagCount && newCoveredCount === coveredCount)
|
|
break;
|
|
|
|
flagCount = newFlagCount;
|
|
coveredCount = newCoveredCount;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Solves the given field given only the information currently available, without considering the information that
|
|
* is gained from the actions performed by this function.
|
|
*
|
|
* @param field the field to solve
|
|
*/
|
|
bigStep(field: Field) {
|
|
if (!field.started || field.won || field.lost) return;
|
|
|
|
const knowns = field.squareList
|
|
.filter(it => !it.isCovered)
|
|
.filter(it => it.getNeighborCount(it => it.isCovered && !it.hasFlag));
|
|
const neighs = Array
|
|
.from(new Set(
|
|
knowns.reduce((neighs, square) => neighs.concat(square.getNeighbors()), <Square[]> [])
|
|
))
|
|
.filter(it => it.isCovered && !it.hasFlag);
|
|
|
|
if (knowns.length === 0 || neighs.length === 0) return;
|
|
|
|
const matrix: number[][] = [];
|
|
// TODO Add row for remaining mines
|
|
knowns.forEach(square => {
|
|
const row = Array(neighs.length).fill(0);
|
|
square.getNeighbors()
|
|
.filter(it => it.isCovered && !it.hasFlag)
|
|
.forEach(it => row[neighs.indexOf(it)] = 1);
|
|
|
|
row.push(square.getNeighborCount(it => it.hasMine) - square.getNeighborCount(it => it.hasFlag));
|
|
matrix.push(row);
|
|
});
|
|
|
|
const system = new Matrix(matrix).solveBinary();
|
|
|
|
system.forEach((it, i) => {
|
|
const square = neighs[i];
|
|
if (it === 0) square.uncover();
|
|
else if (it === 1) square.flag();
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* A matrix of numbers.
|
|
*/
|
|
export class Matrix {
|
|
private readonly cells: number[][];
|
|
private readonly rowCount: number;
|
|
private readonly colCount: number;
|
|
|
|
|
|
/**
|
|
* Constructs a new matrix from the given numbers.
|
|
*
|
|
* @param cells an array of rows of numbers
|
|
*/
|
|
constructor(cells: number[][]) {
|
|
if (cells.length === 0) throw new Error("Matrix must have at least 1 row.");
|
|
if (cells[0].length === 0) throw new Error("Matrix must have at least 1 column.");
|
|
|
|
this.cells = cells;
|
|
this.rowCount = this.cells.length;
|
|
this.colCount = this.cells[0].length;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the `row`th row of numbers.
|
|
*
|
|
* @param row the index of the row to return
|
|
* @returns the `row`th row of numbers
|
|
*/
|
|
getRow(row: number): number[] {
|
|
if (row < 0 || row >= this.rowCount)
|
|
throw new Error(`Row must be in range [0, ${this.rowCount}) but was ${row}.`);
|
|
|
|
return this.cells[row];
|
|
}
|
|
|
|
/**
|
|
* Returns the `col`th column of numbers.
|
|
*
|
|
* @param col the index of the column to return
|
|
* @returns the `col`th column of numbers
|
|
*/
|
|
getCol(col: number): number[] {
|
|
if (col < 0 || col >= this.colCount)
|
|
throw new Error(`Col must be in range [0, ${this.colCount}) but was ${col}.`);
|
|
|
|
return this.cells.map(row => row[col]);
|
|
}
|
|
|
|
/**
|
|
* Returns the `col`th number in the `row`th row.
|
|
*
|
|
* @param row the index of the row to find the number in
|
|
* @param col the index of the column to find the number in
|
|
* @returns the `col`th number in the `row`th row
|
|
*/
|
|
getCell(row: number, col: number): number {
|
|
if (row < 0 || row >= this.rowCount)
|
|
throw new Error(`Row must be in range [0, ${this.rowCount}) but was ${row}.`);
|
|
if (col < 0 || col >= this.colCount)
|
|
throw new Error(`Row must be in range [0, ${this.colCount}) but was ${col}.`);
|
|
|
|
return this.cells[row][col];
|
|
}
|
|
|
|
|
|
/**
|
|
* Transforms this matrix into its row-reduced echelon form using Gauss-Jordan elimination.
|
|
*/
|
|
rref(): void {
|
|
let pivot = 0;
|
|
for (let row = 0; row < this.rowCount; row++) {
|
|
// Find pivot
|
|
while (pivot < this.colCount && this.getCol(pivot).slice(row).every(it => it === 0)) pivot++;
|
|
if (pivot >= this.colCount) return;
|
|
|
|
// Set pivot to non-zero
|
|
if (this.getCell(row, pivot) === 0)
|
|
this.swap(row, this.getCol(pivot).slice(row + 1).findIndex(it => it !== 0) + row + 1);
|
|
// Set pivot to 1
|
|
this.multiply(row, 1 / this.getCell(row, pivot));
|
|
|
|
// Set all other cells in this column to 0
|
|
for (let row2 = 0; row2 < this.rowCount; row2++) {
|
|
if (row2 === row) continue;
|
|
this.add(row2, row, -this.getCell(row2, pivot));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interprets this matrix as an augmented matrix and returns for each variable the value or `undefined` if its value
|
|
* could not be determined.
|
|
*
|
|
* This function invokes `#rref`, so this matrix will change as a result.
|
|
*
|
|
* @returns the value of each variable, and `undefined` for each variable that could not be determined uniquely
|
|
*/
|
|
solve(): (number | undefined)[] {
|
|
this.rref();
|
|
|
|
return range(this.colCount - 1)
|
|
.map(it => {
|
|
const row = this.getRow(this.getCol(it).findIndex(it => it === 1));
|
|
if (row.slice(0, it).every(it => it === 0) && row.slice(it + 1, -1).every(it => it === 0))
|
|
return row.slice(-1)[0];
|
|
|
|
return undefined;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Same as `#solve`, except that it assumes that every variable is an integer in the range [0, 1].
|
|
*
|
|
* @returns the value of each variable, and `undefined` for each variable that could not be determined uniquely
|
|
*/
|
|
solveBinary(): (number | undefined)[] {
|
|
const resultsA = this.solveBinarySub(); // This check effectively auto-chords and auto-flags
|
|
const resultsB = this.solve();
|
|
const resultsC = this.solveBinarySub();
|
|
|
|
return range(this.colCount - 1, 0)
|
|
.map((_, i) => {
|
|
if (resultsA[i] !== undefined) return resultsA[i];
|
|
else if (resultsB[i] !== undefined) return resultsB[i];
|
|
else return resultsC[i];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper function for `#solveBinary` that tries to solve for variables in the range [0, 1] in the current matrix
|
|
* without applying transformations.
|
|
*
|
|
* @returns the value of each variable, and `undefined` for each variable that could not be determined uniquely
|
|
*/
|
|
solveBinarySub(): (number | undefined)[] {
|
|
const results = Array(this.colCount - 1).fill(undefined);
|
|
this.cells.forEach(row => {
|
|
// ax = b
|
|
const a = row.slice(0, -1);
|
|
const b = row.slice(-1)[0];
|
|
|
|
const negSum = a.filter(it => it < 0).reduce((sum, cell) => sum + cell, 0);
|
|
const posSum = a.filter(it => it > 0).reduce((sum, cell) => sum + cell, 0);
|
|
|
|
if (b === negSum) {
|
|
a.forEach((it, i) => {
|
|
if (it < 0) results[i] = 1;
|
|
if (it > 0) results[i] = 0;
|
|
});
|
|
} else if (b === posSum) {
|
|
a.forEach((it, i) => {
|
|
if (it < 0) results[i] = 0;
|
|
if (it > 0) results[i] = 1;
|
|
});
|
|
}
|
|
});
|
|
return results;
|
|
}
|
|
|
|
|
|
/**
|
|
* Swaps the rows at the given indices.
|
|
*
|
|
* @param rowA the index of the row to swap
|
|
* @param rowB the index of the other row to swap
|
|
*/
|
|
swap(rowA: number, rowB: number) {
|
|
[this.cells[rowA], this.cells[rowB]] = [this.cells[rowB], this.cells[rowA]];
|
|
}
|
|
|
|
/**
|
|
* Multiplies all numbers in the `row`th number by `factor`.
|
|
*
|
|
* @param row the index of the row to multiply
|
|
* @param factor the factory to multiply each number with
|
|
*/
|
|
multiply(row: number, factor: number) {
|
|
this.cells[row] = this.cells[row].map(it => it * factor);
|
|
}
|
|
|
|
/**
|
|
* Adds `factor` multiples of the `rowB`th row to the `rowA`th row.
|
|
*
|
|
* Effectively, sets `A = A + B * factor`.
|
|
*
|
|
* @param rowA the index of the row to add to
|
|
* @param rowB the index of the row to add a multiple of
|
|
* @param factor the factor to multiply each added number with
|
|
*/
|
|
add(rowA: number, rowB: number, factor: number) {
|
|
this.cells[rowA] =
|
|
this.cells[rowA].map((it, i) => this.cells[rowA][i] + this.cells[rowB][i] * factor);
|
|
}
|
|
}
|