parent
bfbbda2624
commit
4d6b615513
Binary file not shown.
11
package.json
11
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue