Try out vectorious for matrix operations

See also #60.
This commit is contained in:
Florine W. Dekker 2022-12-20 19:23:54 +01:00
parent bfbbda2624
commit 4d6b615513
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
6 changed files with 52 additions and 110 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{ {
"name": "minesweeper", "name": "minesweeper",
"version": "0.85.3", "version": "0.86.0",
"description": "Just Minesweeper!", "description": "Just Minesweeper!",
"author": "Florine W. Dekker", "author": "Florine W. Dekker",
"browser": "dist/bundle.js", "browser": "dist/bundle.js",
@ -17,7 +17,8 @@
}, },
"dependencies": { "dependencies": {
"alea": "^1.0.1", "alea": "^1.0.1",
"canvas-confetti": "^1.6.0" "canvas-confetti": "^1.6.0",
"vectorious": "^6.1.4"
}, },
"devDependencies": { "devDependencies": {
"grunt": "^1.5.3", "grunt": "^1.5.3",
@ -28,10 +29,10 @@
"grunt-focus": "^1.0.0", "grunt-focus": "^1.0.0",
"grunt-text-replace": "^0.4.0", "grunt-text-replace": "^0.4.0",
"grunt-webpack": "^5.0.0", "grunt-webpack": "^5.0.0",
"ts-loader": "^9.4.1", "ts-loader": "^9.4.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.3", "typescript": "^4.9.4",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-cli": "^5.0.0" "webpack-cli": "^5.0.1"
} }
} }

View File

@ -38,7 +38,8 @@ export const difficulties: Difficulty[] = [
new Difficulty("Beginner", "9x9, 10 mines", 9, 9, 10, true), new Difficulty("Beginner", "9x9, 10 mines", 9, 9, 10, true),
new Difficulty("Intermediate", "16x16, 40 mines", 16, 16, 40, true), new Difficulty("Intermediate", "16x16, 40 mines", 16, 16, 40, true),
new Difficulty("Expert", "30x16, 99 mines", 30, 16, 99, 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),
]; ];
/** /**

View File

@ -1,5 +1,6 @@
// @ts-ignore // @ts-ignore
import confetti from "canvas-confetti"; import confetti from "canvas-confetti";
import {formatTime, range} from "./Common"; import {formatTime, range} from "./Common";
import {Field, Square} from "./Field"; import {Field, Square} from "./Field";
import {Preferences} from "./Preferences"; import {Preferences} from "./Preferences";

View File

@ -1,7 +1,7 @@
const {$, stringToHtml} = (window as any).fwdekker; const {$, stringToHtml} = (window as any).fwdekker;
// @ts-ignore // @ts-ignore
import alea from "alea"; import alea from "alea";
import {stringToHash} from "./Common"; import {stringToHash} from "./Common";
import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty"; import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty";
import {Display} from "./Display"; import {Display} from "./Display";

View File

@ -1,3 +1,6 @@
// @ts-ignore
import {array} from "vectorious";
import {range} from "./Common"; import {range} from "./Common";
import {Field, Square} from "./Field"; import {Field, Square} from "./Field";
@ -199,7 +202,7 @@ export class Solver {
let unknowns: Square[]; let unknowns: Square[];
if (adjacentSquaresOnly) if (adjacentSquaresOnly)
unknowns = Array unknowns = Array
.from(new Set(knowns.reduce((acc, it) => acc.concat(it.neighbors), <Square[]> []))) .from(new Set(knowns.reduce((acc, it) => acc.concat(it.neighbors), <Square[]>[])))
.filter(it => it.isCovered && !it.hasFlag && knowns.indexOf(it) < 0); .filter(it => it.isCovered && !it.hasFlag && knowns.indexOf(it) < 0);
else else
unknowns = field.squareList unknowns = field.squareList
@ -261,7 +264,7 @@ export class Solver {
* A matrix of numbers. * A matrix of numbers.
*/ */
export class Matrix { export class Matrix {
private readonly cells: number[][]; private readonly matrix: array;
private readonly rowCount: number; private readonly rowCount: number;
private readonly colCount: 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.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."); if (cells[0].length === 0) throw new Error("Matrix must have at least 1 column.");
this.cells = cells; this.matrix = array(cells);
this.rowCount = this.cells.length; this.rowCount = this.matrix.shape[0];
this.colCount = this.cells[0].length; this.colCount = this.matrix.shape[1];
}
/**
* 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. * 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; let pivot = 0;
for (let row = 0; row < this.rowCount; row++) { for (let row = 0; row < this.matrix.shape[1]; row++) {
// Find pivot // 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; if (pivot >= this.colCount) return;
// Set pivot to non-zero // Swap with any lower row with non-zero in pivot column
if (this.getCell(row, pivot) === 0) if (this.matrix.get(row, pivot) === 0) {
this.swap(row, this.getCol(pivot).slice(row + 1).findIndex(it => it !== 0) + row + 1); const row2 = this.rowWhereColSatisfies(row + 1, pivot, it => it !== 0)!;
// Set pivot to 1 this.matrix.swap(row, row2);
this.multiply(row, 1 / this.getCell(row, pivot)); }
// 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 // Set all other cells in this column to 0
for (let row2 = 0; row2 < this.rowCount; row2++) { for (let row2 = 0; row2 < this.rowCount; row2++) {
if (row2 === row) continue; 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 * @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(); this.rref();
return range(this.colCount - 1) return range(this.colCount - 1)
.map(it => { .map(column => {
const rowPivotIndex = this.getCol(it).findIndex(it => it === 1); const rowPivotIndex = this.rowWhereColSatisfies(0, column, it => it === 1);
if (rowPivotIndex < 0) return undefined; if (rowPivotIndex == null) return undefined;
const row = this.getRow(rowPivotIndex); const row = this.matrix.slice(rowPivotIndex, rowPivotIndex + 1);
if (row.slice(0, it).every(it => it === 0) && row.slice(it + 1, -1).every(it => it === 0)) if (row.map((it: number) => it === 0).sum() >= this.colCount - 1)
return row.slice(-1)[0]; return row.get(row.length - 1);
return undefined; return undefined;
}); });
@ -392,62 +356,37 @@ export class Matrix {
*/ */
private solveBinarySub(): (number | undefined)[] { private solveBinarySub(): (number | undefined)[] {
const results = Array(this.colCount - 1).fill(undefined); const results = Array(this.colCount - 1).fill(undefined);
this.cells.forEach(row => { for (let row = 0; row < this.rowCount; row++) {
// ax = b // ax = b
const a = row.slice(0, -1); const a = this.matrix.slice(0, this.colCount - 1);
const b = row.slice(-1)[0]; const b = this.matrix.get(row, this.colCount - 1);
const negSum = a.filter(it => it < 0).reduce((sum, cell) => sum + cell, 0); const sign = a.copy().sign();
const posSum = a.filter(it => it > 0).reduce((sum, cell) => sum + cell, 0); 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) { if (b === negSum) {
a.forEach((it, i) => { a.forEach((it: number, i: number) => {
if (it < 0) results[i] = 1; if (it < 0) results[i] = 1;
if (it > 0) results[i] = 0; if (it > 0) results[i] = 0;
}); });
} else if (b === posSum) { } 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] = 0;
if (it > 0) results[i] = 1; if (it > 0) results[i] = 1;
}); });
} }
}); }
return results; return results;
} }
/** private rowWhereColSatisfies(rowStart: number = 0, column: number, criterion: (cell: number) => boolean): number | null {
* Swaps the rows at the given indices. for (let row = rowStart; row < this.rowCount; row++)
* if (criterion(this.matrix.get(row, column)))
* @param rowA the index of the row to swap return row;
* @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]];
}
/** return null;
* 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);
} }
} }