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. * * The `#startSequence` function on the field must NOT be called before invoking this function. * * @param field the field to solve */ static solve(field: Field): void { if (field.hasWon || field.hasLost) return; if (field.hasStarted && !this.step(field.copy())) return; if (!field.hasStarted) { field.isAutoSolving = true; field.runUndoably(() => { const target = {x: Math.floor(field.width / 2), y: Math.floor(field.height / 2)}; const targetSquare = field.getSquareOrElse(target, undefined)!; if (targetSquare.hasFlag) field.toggleFlag(target); if (targetSquare.hasMark) field.toggleMark(target); field.uncover(target); }); field.isAutoSolving = false; } field.isAutoSolving = true; field.runUndoably(() => { field.squareList.filter(it => it.hasFlag).forEach(it => field.toggleFlag(it.coords)); field.squareList.filter(it => it.hasMark).forEach(it => field.toggleMark(it.coords)); while (this.step(field)) { // Repeat until `step` returns false } }); field.isAutoSolving = false; } /** * Returns `true` if and only if this solver can solve the given field. * * This function does not change anything in the given field; solvability is checked on a copy of the field. * * If the given field has not started and no initial square is given, the solver will start solving at an arbitrary * position. * * @param field the field to check for solvability * @param initialSquare the initial coordinates to click at */ static canSolve(field: Field, initialSquare: { x: number, y: number } | undefined = undefined): boolean { const copy = field.copy(); if (initialSquare !== undefined) copy.runUndoably(() => copy.uncover(initialSquare)); this.solve(copy); return copy.hasWon; } /** * Returns a suggestion for a next move based on the current state of the field. * * @param field the field to suggest a move for * @returns a suggestion for a next move based on the current state of the field */ static getHint(field: Field): Square | null { if (!field.hasStarted || field.hasWon || field.hasLost) return null; const knowns = Solver.getKnowns(field); const candidate = knowns.find(square => // Can chord square.getNeighborCount(it => it.hasFlag) === square.getNeighborCount(it => it.hasMine) || // Can flag square.getNeighborCount(it => it.isCovered && !it.hasFlag) === (square.getNeighborCount(it => it.hasMine) - square.getNeighborCount(it => it.hasFlag)) ); if (candidate !== undefined) return candidate; for (let i = 0; i < knowns.length; i++) { const square = knowns[i]; const solution = this.matrixSolve(field, square.neighbors.filter(it => !it.isCovered).concat(square), true); const candidate = solution.find(it => it !== undefined); if (candidate !== undefined) return candidate[1]; } const solution = this.matrixSolve(field, knowns, false); const candidate2 = solution.find(it => it !== undefined); if (candidate2 !== undefined) return candidate2[1]; return null; } /** * Solves in one step through the field. * * @param field the field to solve one step in * @returns `true` if a step could be solved * @private */ private static step(field: Field): boolean { let flagCount = field.flagCount; let coveredCount = field.coveredNonMineCount; if (field.hasWon || field.hasLost) return false; this.stepSingleSquares(field); if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount) return true; this.stepNeighboringSquares(field); if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount) return true; this.stepAllSquares(field); // noinspection RedundantIfStatementJS // Makes it easier to add more steps if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount) return true; return false; } /** * Solves the field as much as by considering just one square at a time and looking for trivial solutions. * * This function is very fast but only finds trivial moves such as a square that can be chorded or a square of which * all neighbors can be flagged. * * @param field the field to solve * @private */ private static stepSingleSquares(field: Field): void { Solver.getKnowns(field) .forEach(square => { field.chord(square); if (square.getNeighborCount(it => it.isCovered) === square.getNeighborCount(it => it.hasMine)) square.neighbors.filter(it => !it.hasFlag).forEach(it => field.toggleFlag(it)); }); } /** * Solves the field as much as possible by considering only one uncovered square and its uncovered neighbors at a * time. * * This function is slower than `#stepSingleSquares` but finds some more advanced moves by effectively considering * two squares at time. Meanwhile, this function does not look at the bigger picture so it cannot infer some more * complicated moves. On the other hand, for some reason this function finds some edge cases that `#stepAllSquares` * overlooks. * * @param field the field to solve * @private */ private static stepNeighboringSquares(field: Field): void { const knowns = Solver.getKnowns(field); knowns.forEach(known => { Solver.applySolution( field, this.matrixSolve(field, known.neighbors.filter(it => !it.isCovered).concat(known), true) ); }); } /** * Solves the field as much as possible by looking at all uncovered squares and the remaining number of mines. * * Because this function considers all squares in the field, it is very slow. Then again, it finds a lot of steps * as well. * * @param field the field to solve * @private */ private static stepAllSquares(field: Field): void { if (!field.hasStarted || field.hasWon || field.hasLost) return; const knowns = Solver.getKnowns(field); Solver.applySolution( field, this.matrixSolve(field, knowns, false) ); } /** * Solves as much as possible from the field assuming knowledge of the uncovered squares in `known`. * * @param field the field to solve in * @param knowns the uncovered squares that the solver should consider * @param adjacentSquaresOnly `true` if the solver should only look at the squares adjacent to `known` and not at * all squares in the field. Enabling this option increases complexity, but may uncover some edge cases * @returns the solution that has been found * @private */ private static matrixSolve(field: Field, knowns: Square[], adjacentSquaresOnly: boolean): Solution { if (knowns.length === 0) return []; let unknowns: Square[]; if (adjacentSquaresOnly) unknowns = Array .from(new Set(knowns.reduce((acc, it) => acc.concat(it.neighbors), []))) .filter(it => it.isCovered && !it.hasFlag && knowns.indexOf(it) < 0); else unknowns = field.squareList .filter(it => it.isCovered && !it.hasFlag && knowns.indexOf(it) < 0); if (unknowns.length === 0) return []; const matrix: number[][] = []; knowns.forEach(square => { const row = Array(unknowns.length).fill(0); square.neighbors .filter(it => it.isCovered && !it.hasFlag) .forEach(it => row[unknowns.indexOf(it)] = 1); row.push(square.getNeighborCount(it => it.hasMine) - square.getNeighborCount(it => it.hasFlag)); matrix.push(row); }); if (!adjacentSquaresOnly) matrix.push(Array(unknowns.length).fill(1).concat(field.mineCount - field.flagCount)); return (new Matrix(matrix)) .solveBinary() .map((it, i) => it === undefined ? undefined : [it, unknowns[i]]); } /** * Returns all uncovered squares that have at least one covered unflagged neighbor. * * @param field the field to find the known squares in * @returns all uncovered squares that have at least one covered unflagged neighbor * @private */ private static getKnowns(field: Field): Square[] { return field.squareList .filter(it => !it.isCovered) .filter(it => it.getNeighborCount(it => it.isCovered && !it.hasFlag) > 0); } /** * Applies the given solution to the field. * * @param field the field to apply the solution to * @param solution the solution to apply * @private */ private static applySolution(field: Field, solution: Solution): void { solution.forEach(target => { if (target === undefined) return; const [solution, square] = target; if (solution === 0) field.uncover(square.coords); else if (solution === 1) field.toggleFlag(square.coords); }); } } /** * 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 rowPivotIndex = this.getCol(it).findIndex(it => it === 1); if (rowPivotIndex < 0) return undefined; const row = this.getRow(rowPivotIndex); 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.solve(); const resultsB = this.solveBinarySub(); return resultsA.map((it, i) => it ?? resultsB[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 * @private */ private 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); } } /** * A partial solution to a field. * * Each element of the array describes an instruction to apply to the field. If the instruction is `undefined`, then * nothing should be done. Otherwise, the tuple describes the instruction: If the number is 0, the associated square * should be uncovered because it definitely does not contain a mine. Otherwise, the number is 1 and the associated * square should be flagged because it definitely contains a mine. */ type Solution = ([number, Square] | undefined)[]