Clean up code using advanced linter feedback

This commit is contained in:
Florine W. Dekker 2024-05-10 17:00:53 +02:00
parent fcd0207aa3
commit 3f82aed17b
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
15 changed files with 193 additions and 142 deletions

View File

@ -5,12 +5,42 @@ import tseslint from 'typescript-eslint';
// noinspection JSUnusedGlobalSymbols
export default tseslint.config(
// Configure parser for type checking
{
languageOptions: {
parserOptions: {
project: ["tsconfig.json"],
},
},
},
// Basic
eslint.configs.recommended,
...tseslint.configs.recommended,
{"ignores": ["Gruntfile.js", "dist/**"]},
// Strict TS checks
...tseslint.configs.strictTypeChecked,
{
"rules": {
"@typescript-eslint/no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
"@typescript-eslint/no-confusing-void-expression": ["error", {ignoreArrowShorthand: true}],
"@typescript-eslint/no-extraneous-class": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unnecessary-condition": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unused-vars": ["error", {argsIgnorePattern: "_"}],
"@typescript-eslint/restrict-template-expressions": ["error", {allowBoolean: true, allowNumber: true}],
"no-unused-vars": "off",
},
}
},
// Stylistic TS checks
...tseslint.configs.stylisticTypeChecked,
{
rules: {
"@typescript-eslint/no-inferrable-types": "off",
},
},
);

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "minesweeper",
"version": "0.86.10",
"version": "0.86.11",
"description": "Just Minesweeper!",
"author": "Florine W. Dekker",
"browser": "dist/bundle.js",
@ -14,7 +14,7 @@
"dev": "grunt dev",
"dev:server": "grunt dev:server",
"deploy": "grunt deploy",
"lint": "npx eslint ."
"lint": "npx eslint src/main/js/"
},
"dependencies": {
"alea": "^1.0.1",
@ -23,6 +23,7 @@
"devDependencies": {
"@eslint/js": "^9.2.0",
"@types/canvas-confetti": "^1.6.4",
"@typescript-eslint/parser": "^7.8.0",
"eslint": "^8.57.0",
"grunt": "^1.6.1",
"grunt-cli": "^1.4.3",

View File

@ -1,4 +1,4 @@
import {noop} from "./Common";
import {noop, req} from "./Common";
/**
@ -126,8 +126,7 @@ export class ActionHistory {
* @param action the action to add
*/
addAction(action: Action): void {
if (this.depth === 0)
throw new Error("Cannot add action if there is no uncommitted sequence.");
req(this.depth > 0, () => "Cannot add action if there is no uncommitted sequence.");
this.currentSequence!.addAction(action);
}
@ -141,8 +140,7 @@ export class ActionHistory {
* @returns `true` if and only if the sequence was actually committed
*/
commitSequence(): boolean {
if (this.depth === 0)
throw new Error("Cannot commit sequence if there is no uncommitted sequence.");
req(this.depth > 0, () => "Cannot commit sequence if there is no uncommitted sequence.");
this.depth--;
if (this.depth === 0 && this.currentSequence!.isEmpty()) {
@ -171,8 +169,7 @@ export class ActionHistory {
* @returns the actual amount of sequences that have been undone
*/
undo(amount: number = this.sequenceIndex + 1): number {
if (this.depth > 0)
throw new Error("Cannot undo sequences while there is an uncommitted sequence.");
req(this.depth === 0, () => "Cannot undo sequences while there is an uncommitted sequence.");
amount = Math.min(amount, this.sequenceIndex + 1);
let i;
@ -203,8 +200,7 @@ export class ActionHistory {
* @returns the actual amount of sequences that have been redone
*/
redo(amount: number = this.sequences.length - (this.sequenceIndex + 1)): number {
if (this.depth !== 0)
throw new Error("Cannot redo sequences while there is an uncommitted sequence.");
req(this.depth === 0, () => "Cannot redo sequences while there is an uncommitted sequence.");
amount = Math.min(amount, this.sequences.length - (this.sequenceIndex + 1));
for (let i = 0; i < amount; i++) {

View File

@ -1,3 +1,60 @@
/**
* A no-op, which does absolutely nothing.
*/
export function noop(): void {
// Do nothing
}
/**
* Throws an error.
*
* @param message the error message
*/
export function error(message: string): never {
throw new Error(message);
}
/**
* Requires that `condition` is `true`, and throws an error with `message` otherwise.
*
* @param condition the condition to require
* @param message a function that returns the error message if `condition` is `false`
*/
export function req(condition: boolean, message: () => string): asserts condition {
if (!condition) error(message());
}
/**
* Requires that `value` is neither `null` nor `undefined`, and throws an error with `message` otherwise.
*
* @param value the value that is asserted to not be nullish
* @param message a function that returns the error message if `value` is nullish
*/
export function reqNotNullish<T>(
value: T,
message: () => string = () => "Value should be non-nullable, was was nullable."
): asserts value is NonNullable<T> {
req(value !== null && value !== undefined, message);
}
/**
* Creates an array of `size` consecutive integers starting at `startAt`.
*
* If `startAt` is not given, this creates an array of consecutive integers from 0 (inclusive) until `size` (exclusive).
*
* Taken from https://stackoverflow.com/a/10050831 (CC BY-SA 4.0).
*
* @param length the number of consecutive integers to put in the array
* @param beginAt the first integer to return
* @returns the array of consecutive integers
*/
export function range(length: number, beginAt: number = 0): number[] {
return [...Array(length).keys()].map(i => i + beginAt);
}
/**
* Slices `array` into chunks of `chunkSize` elements each.
*
@ -23,41 +80,19 @@ export function chunkifyArray<T>(array: T[], chunkSize: number): T[][] {
* @returns the formatted time
*/
export function formatTime(seconds: number, minutes: boolean, hours: boolean): string {
if (!minutes && hours) throw new Error("Cannot format time with hours but without minutes.");
if (!minutes && !hours) return "" + seconds;
req(minutes || !hours, () => "Cannot format time with hours but without minutes.");
if (!minutes && !hours) return seconds.toString();
const secondsString = ("" + (seconds % 60)).padStart(2, '0');
const secondsString = (seconds % 60).toString().padStart(2, '0');
const minutesString = hours
? ("" + Math.floor((seconds % 3600) / 60)).padStart(2, '0')
: ("" + Math.floor(seconds / 60));
? Math.floor((seconds % 3600) / 60).toString().padStart(2, '0')
: Math.floor(seconds / 60).toString();
if (!hours) return `${minutesString}:${secondsString}`;
const hoursString = Math.floor(seconds / 3600);
return `${hoursString}:${minutesString}:${secondsString}`;
}
/**
* A no-op, which does absolutely nothing.
*/
export function noop(): void {
// Do nothing
}
/**
* Creates an array of `size` consecutive integers starting at `startAt`.
*
* If `startAt` is not given, this creates an array of consecutive integers from 0 (inclusive) until `size` (exclusive).
*
* Taken from https://stackoverflow.com/a/10050831 (CC BY-SA 4.0).
*
* @param length the number of consecutive integers to put in the array
* @param beginAt the first integer to return
* @returns the array of consecutive integers
*/
export function range(length: number, beginAt: number = 0): number[] {
return [...Array(length).keys()].map(i => i + beginAt);
}
/**
* Hashes the given string.
*

View File

@ -1,5 +1,5 @@
import confetti from "canvas-confetti";
import {formatTime, range} from "./Common";
import {error, formatTime, range, reqNotNullish} from "./Common";
import {Coords, Field, Square} from "./Field";
import {Preferences} from "./Preferences";
@ -100,7 +100,7 @@ export class Display {
this.digitSymbols = range(10).map(digit => {
const canvas = createCanvas(this.scale, this.scale);
ctx = canvas.getContext("2d")!;
if (digit !== 0) fillText("" + digit, "Courier New", digitColors[digit]);
if (digit !== 0) fillText(digit.toString(), "Courier New", digitColors[digit]);
return canvas;
});
@ -177,8 +177,10 @@ export class Display {
* @returns the square grid coordinates corresponding to the given client coordinates
*/
posToSquare(pos: Coords): Coords {
reqNotNullish(this.field);
const rect = this.canvas.getBoundingClientRect();
const scaling = (rect.width - 2 * this.canvas.clientLeft) / (this.field!.width * this.scale);
const scaling = (rect.width - 2 * this.canvas.clientLeft) / (this.field.width * this.scale);
return {
x: Math.floor((pos.x - rect.left - this.canvas.clientLeft) / (this.scale * scaling)),
@ -279,14 +281,12 @@ export class Display {
return true;
if (isUncovering && this.mouseSquare === it)
return it.hasFlag || it.hasMark;
if (isChording && this.mouseSquare.neighbors.indexOf(it) >= 0)
if (isChording && this.mouseSquare.neighbors.includes(it))
return it.hasFlag || it.hasMark;
return true;
})
.forEach(square => {
ctx.drawImage(this.coverSymbol!, square.coords.x * this.scale, square.coords.y * this.scale);
});
.forEach(it => ctx.drawImage(this.coverSymbol!, it.coords.x * this.scale, it.coords.y * this.scale));
ctx.restore();
}
@ -305,26 +305,24 @@ export class Display {
if (this.hintSquare != null) {
ctx.save();
ctx.scale(this.scale, this.scale);
ctx.fillStyle = this.hintColor;
ctx.fillRect(
this.hintSquare.coords.x * this.scale, this.hintSquare.coords.y * this.scale,
this.scale, this.scale
);
ctx.fillRect(this.hintSquare.coords.x, this.hintSquare.coords.y, 1, 1);
ctx.restore();
showsHint = true;
}
if (!showsHint && this.preferences.showTooManyFlagsHints) {
ctx.save();
ctx.fillStyle = this.errorColor;
madeMistakes = madeMistakes || this.field.squareList
const mistakes = this.field.squareList
.filter(it => !it.isCovered)
.filter(it => it.getNeighborCount(it => it.hasMine) < it.getNeighborCount(it => it.hasFlag))
.map(square => {
ctx.fillRect(square.coords.x * this.scale, square.coords.y * this.scale, this.scale, this.scale);
})
.length > 0;
.filter(it => it.getNeighborCount(it => it.hasMine) < it.getNeighborCount(it => it.hasFlag));
madeMistakes = mistakes.length > 0;
ctx.save();
ctx.scale(this.scale, this.scale);
ctx.fillStyle = this.errorColor;
mistakes.forEach(square => ctx.fillRect(square.coords.x, square.coords.y, 1, 1));
ctx.restore();
}
@ -333,19 +331,20 @@ export class Display {
(this.preferences.showChordableHints || this.preferences.showAllNeighborsAreMinesHints)
) {
ctx.save();
ctx.scale(this.scale, this.scale);
this.field.squareList
.filter(it => !it.isCovered)
.filter(it => {
.forEach(it => {
const mines = it.getNeighborCount(it => it.hasMine);
const flags = it.getNeighborCount(it => it.hasFlag);
const covered = it.getNeighborCount(it => it.isCovered);
if (this.preferences.showChordableHints && mines === covered && mines !== flags) {
ctx.fillStyle = this.chordableColor;
ctx.fillRect(it.coords.x * this.scale, it.coords.y * this.scale, this.scale, this.scale)
ctx.fillRect(it.coords.x, it.coords.y, 1, 1);
} else if (this.preferences.showAllNeighborsAreMinesHints && mines === flags && covered !== flags) {
ctx.fillStyle = this.allNeighborsAreMinesColor;
ctx.fillRect(it.coords.x * this.scale, it.coords.y * this.scale, this.scale, this.scale)
ctx.fillRect(it.coords.x, it.coords.y, 1, 1);
}
});
ctx.restore();
@ -391,7 +390,7 @@ export class Display {
ctx.save();
ctx.fillStyle = "#000";
ctx.font = Math.floor(0.55 * this.scale) + "px Courier New";
ctx.font = Math.floor(0.55 * this.scale).toString() + "px Courier New";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
@ -422,12 +421,12 @@ export class Display {
deathsSymbol = this.deathsSymbolA;
}
ctx.drawImage(
deathsSymbol!,
deathsSymbol ?? error("Failed to initialize deaths symbol."),
Math.floor(this.canvas.width / 2 - this.scale),
Math.floor(this.canvas.height - this.scale)
);
ctx.fillText(
"" + this.field.deathCount,
this.field.deathCount.toString(),
Math.floor(this.canvas.width / 2),
Math.floor(this.canvas.height - 0.5 * this.scale),
this.scale
@ -459,7 +458,7 @@ export class Display {
const rect = this.canvas.getBoundingClientRect();
if (this.field.hasWon && this.winTime == null) {
confetti({
void confetti({
origin: {
x: (rect.left + rect.width / 2) / document.documentElement.clientWidth,
y: (rect.top + rect.height / 2) / document.documentElement.clientHeight

View File

@ -1,8 +1,7 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {MemoryStorage} = (window as any).fwdekker.storage;
const {MemoryStorage} = (window as any).fwdekker.storage; // eslint-disable-line
import {Action, ActionHistory} from "./Action";
import {chunkifyArray} from "./Common";
import {chunkifyArray, req} from "./Common";
import {findDifficulty} from "./Difficulty";
import {HighScores} from "./HighScores";
import {Random} from "./Random";
@ -28,6 +27,10 @@ export class Field {
readonly squares: Square[][] = [];
readonly isSolvable: boolean;
get maxMines(): number {
return Field.maxMines(this.width, this.height);
}
private _coveredNonMineCount: number;
get coveredNonMineCount(): number {
return this._coveredNonMineCount;
@ -81,8 +84,7 @@ export class Field {
*/
constructor(width: number, height: number, mineCount: number, solvable: boolean, seed: number,
statistics: Statistics, highScores: HighScores) {
if (mineCount > Field.maxMines(width, height))
throw new Error(`Mine count must be at most ${Field.maxMines(width, height)}, but was ${mineCount}.`);
req(mineCount <= this.maxMines, () => `Mine count must be at most ${this.maxMines}, but was ${mineCount}.`);
this.statistics = statistics;
this.highScores = highScores;
@ -114,7 +116,7 @@ export class Field {
false,
0,
new Statistics(new MemoryStorage()),
new HighScores(new MemoryStorage())
new HighScores(new MemoryStorage()),
);
copy.squareList.length = 0;
@ -140,10 +142,9 @@ export class Field {
* @private
*/
private shuffle(): void {
if (this.hasStarted)
throw new Error("Cannot shuffle mines after field has started");
req(!this.hasStarted, () => "Cannot shuffle mines after field has started");
const newMineMask = Array(this.size).fill(true, 0, this.mineCount).fill(false, this.mineCount);
const newMineMask = Array<boolean>(this.size).fill(true, 0, this.mineCount).fill(false, this.mineCount);
this.random.shuffleInPlace(newMineMask);
newMineMask.forEach((hasMine, i) => this.squareList[i].hasMine = hasMine);
}
@ -169,7 +170,7 @@ export class Field {
this.runUndoably(() => {
const square = this.getSquare(coords);
const from = [square].concat(square.neighbors).filter(it => it.hasMine);
const to = this.squareList.filter(it => !it.hasMine && from.indexOf(it) < 0).slice(0, from.length);
const to = this.squareList.filter(it => !it.hasMine && !from.includes(it)).slice(0, from.length);
from.slice(0, to.length).forEach(it => this.runAction(swapAction(it, this.random.pop(to)!)));
});
@ -182,8 +183,7 @@ export class Field {
* @private
*/
private generate(start: Coords): void {
if (this.hasStarted)
throw new Error("Cannot generate new field after field has started.");
req(!this.hasStarted, () => "Cannot generate new field after field has started.");
this.statistics.gamesStarted++;
@ -212,7 +212,10 @@ export class Field {
* @returns the square at the given coordinates
*/
getSquare(coords: Coords): Square {
return this.squares[coords.y]![coords.x]!;
req(coords.x >= 0 && coords.x < this.width, () => `Expected 0 <= x < ${this.width}, actual ${coords.x}.`);
req(coords.y >= 0 && coords.y < this.height, () => `Expected 0 <= y < ${this.height}, actual ${coords.y}.`);
return this.squares[coords.y][coords.x];
}
/**
@ -253,9 +256,7 @@ export class Field {
* @param coords the coordinates of the square to chord
*/
chord(coords: Coords): void {
const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot chord undefined square at (${coords}).`);
const square = this.getSquare(coords);
if (square.isCovered || this.isOver) return;
if (square.getNeighborCount(it => it.hasFlag) !== square.getNeighborCount(it => it.hasMine)) return;
@ -280,9 +281,7 @@ export class Field {
* @param coords the coordinates of the square to anti-chord
*/
antiChord(coords: Coords): void {
const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot chord undefined square at (${coords}).`);
const square = this.getSquare(coords);
if (square.isCovered || this.isOver) return;
if (square.getNeighborCount(it => it.isCovered) !== square.getNeighborCount(it => it.hasMine)) return;
@ -304,7 +303,6 @@ export class Field {
*/
uncover(coords: Coords): void {
const square = this.getSquare(coords);
if (square === undefined) throw new Error(`Cannot uncover undefined square at (${coords}).`);
if (this.isOver) return;
this.runUndoably(() => {
@ -382,8 +380,7 @@ export class Field {
* @param coords the coordinates of the square to toggle the flag at
*/
toggleFlag(coords: Coords): void {
const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot toggle flag of undefined square at (${coords}).`);
const square = this.getSquare(coords);
if (!square.isCovered || square.hasMark || this.isOver) return;
this.runAction(new Action(
@ -408,8 +405,7 @@ export class Field {
* @param coords the coordinates of the square to toggle the question mark at
*/
toggleMark(coords: Coords): void {
const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot toggle flag of undefined square at (${coords}).`);
const square = this.getSquare(coords);
if (!square.isCovered || square.hasFlag || this.isOver) return;
this.runAction(new Action(
@ -618,7 +614,7 @@ export class Square {
].filter((it): it is Square => it !== null);
}
return this._neighbors!;
return this._neighbors;
}
/**
@ -636,4 +632,7 @@ export class Square {
/**
* A pair of coordinates, typically those of a `Square`.
*/
export type Coords = { readonly x: number, readonly y: number };
export interface Coords {
readonly x: number;
readonly y: number;
}

View File

@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {$, stringToHtml} = (window as any).fwdekker;
const {$, stringToHtml} = (window as any).fwdekker; // eslint-disable-line
import {stringToHash} from "./Common";
import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty";
@ -60,7 +59,7 @@ export class Game {
this.canvas.classList.remove("hidden");
this.random = new Random();
this.seed = "" + this.random.uniform();
this.seed = this.random.uniform().toString();
this.leftDown = false;
this.middleDown = false;
this.rightDown = false;
@ -95,9 +94,9 @@ export class Game {
this.customDifficultyOverlay = new ModalDialog({
dialog: $("#custom-difficulty-dialog"),
onOpen: () => {
this.widthInput.value = "" + (this.field?.width ?? defaultDifficulty.width);
this.heightInput.value = "" + (this.field?.height ?? defaultDifficulty.height);
this.minesInput.value = "" + (this.field?.mineCount ?? defaultDifficulty.mineCount);
this.widthInput.value = (this.field?.width ?? defaultDifficulty.width).toString();
this.heightInput.value = (this.field?.height ?? defaultDifficulty.height).toString();
this.minesInput.value = (this.field?.mineCount ?? defaultDifficulty.mineCount).toString();
this.solvableInput.checked = this.field?.isSolvable ?? defaultDifficulty.solvable;
this.setMineLimit();
},
@ -202,14 +201,13 @@ export class Game {
this.solve = $("#solve");
this.solve.addEventListener(
"click",
async (event: MouseEvent) => {
(event: MouseEvent) => {
event.preventDefault();
if (this.field != null) {
this.statistics.solverUsages++;
await Solver.solveAnimated(this.field);
void Solver.solveAnimated(this.field);
}
this.display.hintSquare = null;
}
);
@ -306,7 +304,7 @@ export class Game {
this.field.runUndoably(() => {
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
if (this.field == null || !this.field.hasSquareAt(coords)) return;
if (!this.field?.hasSquareAt(coords)) return;
const square = this.field.getSquare(coords);
this.holdStart = square;
@ -356,7 +354,7 @@ export class Game {
this.field.runUndoably(() => {
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
if (this.field == null || !this.field.hasSquareAt(coords)) return;
if (!this.field?.hasSquareAt(coords)) return;
const square = this.field.getSquare(coords);
switch (event.button) {
@ -408,7 +406,7 @@ export class Game {
* @private
*/
private setMineLimit(): void {
this.minesInput.max = "" + Field.maxMines(+this.widthInput.value, +this.heightInput.value);
this.minesInput.max = Field.maxMines(+this.widthInput.value, +this.heightInput.value).toString();
}
/**
@ -424,10 +422,10 @@ export class Game {
else element.removeAttribute("href");
};
toggleHref(this.undo, !!this.field?.canUndo());
toggleHref(this.redo, !!this.field?.canRedo());
toggleHref(this.hint, !!this.field?.hasStarted && !this.field?.isOver);
toggleHref(this.solve, !!this.field?.hasStarted && !this.field?.isOver);
toggleHref(this.undo, this.field?.canUndo() ?? false);
toggleHref(this.redo, this.field?.canRedo() ?? false);
toggleHref(this.hint, (this.field?.hasStarted ?? false) && !this.field?.isOver);
toggleHref(this.solve, (this.field?.hasStarted ?? false) && !this.field?.isOver);
}
/**
@ -456,7 +454,7 @@ export class Game {
solvable: boolean = defaultDifficulty.solvable,
seed?: string
) {
this.seed = seed ?? "" + this.random.uniform();
this.seed = seed ?? this.random.uniform().toString();
this.field = new Field(
width, height, mineCount, solvable,
isNaN(+this.seed) ? stringToHash(this.seed) : +this.seed,

View File

@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {LocalStorage} = (window as any).fwdekker.storage;
const {LocalStorage} = (window as any).fwdekker.storage; // eslint-disable-line
import {formatTime} from "./Common";
import {difficulties, Difficulty} from "./Difficulty";
@ -8,7 +7,10 @@ import {difficulties, Difficulty} from "./Difficulty";
/**
* A score obtained by clearing a field.
*/
export type Score = {time: number, deaths: number};
export interface Score {
time: number;
deaths: number;
}
/**

View File

@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {doAfterLoad} = (window as any).fwdekker;
const {doAfterLoad} = (window as any).fwdekker; // eslint-disable-line
import {waitForForkAwesome} from "./Common";
import {BasicIconFont, ForkAwesomeFont} from "./Display";

View File

@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {$} = (window as any).fwdekker;
const {$} = (window as any).fwdekker; // eslint-disable-line
/**

View File

@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {LocalStorage} = (window as any).fwdekker.storage;
const {LocalStorage} = (window as any).fwdekker.storage; // eslint-disable-line
import {BasicIconFont, IconFont} from "./Display";

View File

@ -15,7 +15,7 @@ export class Random {
* @param seed the seed to initialize the generator with
*/
constructor(seed: number = Date.now()) {
this.rng = alea("" + seed);
this.rng = alea(seed.toString());
}
/**

View File

@ -1,4 +1,4 @@
import {range} from "./Common";
import {range, req} from "./Common";
import {Coords, Field, Square} from "./Field";
@ -56,7 +56,7 @@ export class Solver {
if (!field.hasStarted) {
field.runUndoably(() => {
const target = {x: Math.floor(field.width / 2), y: Math.floor(field.height / 2)};
const targetSquare = field.getSquareOrElse(target)!;
const targetSquare = field.getSquare(target);
if (targetSquare.hasFlag) field.toggleFlag(target);
if (targetSquare.hasMark) field.toggleMark(target);
field.uncover(target);
@ -88,8 +88,7 @@ export class Solver {
* @param initialSquare the initial coordinates to click at
*/
static canSolve(field: Field, initialSquare: Coords | undefined = undefined): boolean {
if (!field.hasStarted && initialSquare === undefined)
throw new Error("Cannot determine solvability of unstarted field.");
req(field.hasStarted || initialSquare !== undefined, () => "Cannot determine solvability of unstarted field.");
const copy = field.copy();
if (initialSquare !== undefined) copy.runUndoably(() => copy.uncover(initialSquare));
@ -311,17 +310,17 @@ export class Solver {
let unknowns: Square[];
if (adjacentSquaresOnly) {
unknowns = Array
.from(new Set(knowns.reduce((acc, it) => acc.concat(it.neighbors), <Square[]>[])))
.filter(it => it.isCovered && !it.hasFlag && knowns.indexOf(it) < 0);
.from(new Set(knowns.reduce<Square[]>((acc, it) => acc.concat(it.neighbors), [])))
.filter(it => it.isCovered && !it.hasFlag && !knowns.includes(it));
} else {
unknowns = field.squareList
.filter(it => it.isCovered && !it.hasFlag && knowns.indexOf(it) < 0);
.filter(it => it.isCovered && !it.hasFlag && !knowns.includes(it));
}
if (unknowns.length === 0) return [];
const matrix: number[][] = [];
knowns.forEach(square => {
const row = Array(unknowns.length).fill(0);
const row = Array<number>(unknowns.length).fill(0);
square.neighbors
.filter(it => it.isCovered && !it.hasFlag)
.forEach(it => row[unknowns.indexOf(it)] = 1);
@ -371,8 +370,8 @@ export class Matrix {
* @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.");
req(cells.length > 0, () => "Matrix must have at least 1 row.");
req(cells[0].length > 0, () => "Matrix must have at least 1 column.");
this.cells = cells;
this.rowCount = this.cells.length;
@ -387,8 +386,7 @@ export class Matrix {
* @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}.`);
req(row >= 0 && row < this.rowCount, () => `Row must be in range [0, ${this.rowCount}) but was ${row}.`);
return this.cells[row];
}
@ -400,8 +398,7 @@ export class Matrix {
* @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}.`);
req(col >= 0 && col < this.colCount, () => `Col must be in range [0, ${this.colCount}) but was ${col}.`);
return this.cells.map(row => row[col]);
}
@ -414,10 +411,8 @@ export class Matrix {
* @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}.`);
req(row >= 0 && row < this.rowCount, () => `Expected 0 <= row < ${this.rowCount}, actual ${row}.`);
req(col >= 0 && col < this.colCount, () => `Expected 0 <= col < ${this.colCount}, actual ${col}.`);
return this.cells[row][col];
}
@ -490,7 +485,7 @@ export class Matrix {
* @private
*/
private solveBinarySub(): (number | undefined)[] {
const results = Array(this.colCount - 1).fill(undefined);
const results = Array<number | undefined>(this.colCount - 1).fill(undefined);
this.cells.forEach(row => {
// ax = b
const a = row.slice(0, -1);

View File

@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {LocalStorage} = (window as any).fwdekker.storage;
const {LocalStorage} = (window as any).fwdekker.storage; // eslint-disable-line
import {formatTime} from "./Common";