diff --git a/package-lock.json b/package-lock.json index dca71e4..7ebbffb 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 9f2af99..8fbfd1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minesweeper", - "version": "0.85.3", + "version": "0.86.0", "description": "Just Minesweeper!", "author": "Florine W. Dekker", "browser": "dist/bundle.js", @@ -17,7 +17,8 @@ }, "dependencies": { "alea": "^1.0.1", - "canvas-confetti": "^1.6.0" + "canvas-confetti": "^1.6.0", + "vectorious": "^6.1.4" }, "devDependencies": { "grunt": "^1.5.3", @@ -28,10 +29,10 @@ "grunt-focus": "^1.0.0", "grunt-text-replace": "^0.4.0", "grunt-webpack": "^5.0.0", - "ts-loader": "^9.4.1", + "ts-loader": "^9.4.2", "ts-node": "^10.9.1", - "typescript": "^4.9.3", + "typescript": "^4.9.4", "webpack": "^5.75.0", - "webpack-cli": "^5.0.0" + "webpack-cli": "^5.0.1" } } diff --git a/src/main/js/Difficulty.ts b/src/main/js/Difficulty.ts index 319f2b9..eb938a6 100644 --- a/src/main/js/Difficulty.ts +++ b/src/main/js/Difficulty.ts @@ -38,7 +38,8 @@ export const difficulties: Difficulty[] = [ new Difficulty("Beginner", "9x9, 10 mines", 9, 9, 10, true), new Difficulty("Intermediate", "16x16, 40 mines", 16, 16, 40, true), new Difficulty("Expert", "30x16, 99 mines", 30, 16, 99, true), - new Difficulty("Custom", null, 0, 0, 0, false) + new Difficulty("Insane", "30x16, 170 mines", 30, 16, 170, true), + new Difficulty("Custom", null, 0, 0, 0, false), ]; /** diff --git a/src/main/js/Display.ts b/src/main/js/Display.ts index 5b3aabe..66023c7 100644 --- a/src/main/js/Display.ts +++ b/src/main/js/Display.ts @@ -1,5 +1,6 @@ // @ts-ignore import confetti from "canvas-confetti"; + import {formatTime, range} from "./Common"; import {Field, Square} from "./Field"; import {Preferences} from "./Preferences"; diff --git a/src/main/js/Game.ts b/src/main/js/Game.ts index 1b08aba..a64a973 100644 --- a/src/main/js/Game.ts +++ b/src/main/js/Game.ts @@ -1,7 +1,7 @@ const {$, stringToHtml} = (window as any).fwdekker; - // @ts-ignore import alea from "alea"; + import {stringToHash} from "./Common"; import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty"; import {Display} from "./Display"; diff --git a/src/main/js/Solver.ts b/src/main/js/Solver.ts index 1a2fc54..1499afa 100644 --- a/src/main/js/Solver.ts +++ b/src/main/js/Solver.ts @@ -1,3 +1,6 @@ +// @ts-ignore +import {array} from "vectorious"; + import {range} from "./Common"; import {Field, Square} from "./Field"; @@ -199,7 +202,7 @@ export class Solver { let unknowns: Square[]; if (adjacentSquaresOnly) unknowns = Array - .from(new Set(knowns.reduce((acc, it) => acc.concat(it.neighbors), []))) + .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 @@ -261,7 +264,7 @@ export class Solver { * A matrix of numbers. */ export class Matrix { - private readonly cells: number[][]; + private readonly matrix: array; private readonly rowCount: number; private readonly colCount: number; @@ -275,75 +278,36 @@ export class Matrix { 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]; + this.matrix = array(cells); + this.rowCount = this.matrix.shape[0]; + this.colCount = this.matrix.shape[1]; } /** * Transforms this matrix into its row-reduced echelon form using Gauss-Jordan elimination. */ - rref(): void { + private rref(): void { + const shape = this.matrix.shape; + let pivot = 0; - for (let row = 0; row < this.rowCount; row++) { + for (let row = 0; row < this.matrix.shape[1]; row++) { // Find pivot - while (pivot < this.colCount && this.getCol(pivot).slice(row).every(it => it === 0)) pivot++; + while (pivot < this.colCount && this.rowWhereColSatisfies(row, pivot, it => it !== 0) == null) 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)); + // Swap with any lower row with non-zero in pivot column + if (this.matrix.get(row, pivot) === 0) { + const row2 = this.rowWhereColSatisfies(row + 1, pivot, it => it !== 0)!; + this.matrix.swap(row, row2); + } + // Scale row so pivot equals 1 + this.matrix.slice(row, row + 1).scale(1 / this.matrix.get(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)); + this.matrix.row_add(row2, row, -this.matrix.get(row2, pivot)); } } } @@ -356,17 +320,17 @@ export class Matrix { * * @returns the value of each variable, and `undefined` for each variable that could not be determined uniquely */ - solve(): (number | undefined)[] { + private 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; + .map(column => { + const rowPivotIndex = this.rowWhereColSatisfies(0, column, it => it === 1); + if (rowPivotIndex == null) 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]; + const row = this.matrix.slice(rowPivotIndex, rowPivotIndex + 1); + if (row.map((it: number) => it === 0).sum() >= this.colCount - 1) + return row.get(row.length - 1); return undefined; }); @@ -392,62 +356,37 @@ export class Matrix { */ private solveBinarySub(): (number | undefined)[] { const results = Array(this.colCount - 1).fill(undefined); - this.cells.forEach(row => { + for (let row = 0; row < this.rowCount; row++) { // ax = b - const a = row.slice(0, -1); - const b = row.slice(-1)[0]; + const a = this.matrix.slice(0, this.colCount - 1); + const b = this.matrix.get(row, this.colCount - 1); - 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); + const sign = a.copy().sign(); + const negSum = -sign.copy().map((it: number) => it === -1).product(a).sum(); + const posSum = sign.copy().map((it: number) => it === 1).product(a).sum(); if (b === negSum) { - a.forEach((it, i) => { + a.forEach((it: number, i: number) => { if (it < 0) results[i] = 1; if (it > 0) results[i] = 0; }); } else if (b === posSum) { - a.forEach((it, i) => { + a.forEach((it: number, i: number) => { 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]]; - } + private rowWhereColSatisfies(rowStart: number = 0, column: number, criterion: (cell: number) => boolean): number | null { + for (let row = rowStart; row < this.rowCount; row++) + if (criterion(this.matrix.get(row, column))) + return row; - /** - * 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); + return null; } }