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",
"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"
}
}

View File

@ -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),
];
/**

View File

@ -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";

View File

@ -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";

View File

@ -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), <Square[]> [])))
.from(new Set(knowns.reduce((acc, it) => acc.concat(it.neighbors), <Square[]>[])))
.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;
}
}