Clean up code using advanced linter feedback
This commit is contained in:
parent
fcd0207aa3
commit
3f82aed17b
|
@ -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",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
Binary file not shown.
|
@ -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",
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
Loading…
Reference in New Issue