Compare commits

...

1 Commits

Author SHA1 Message Date
Florine W. Dekker 30e3467598
Make async to solve in parallel 2021-11-14 16:49:44 +01:00
5 changed files with 115 additions and 91 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -160,7 +160,7 @@ export class Field {
* @param square the square from to move mines away from * @param square the square from to move mines away from
* @private * @private
*/ */
private clearMines(square: Square): void { private async moveMinesAwayFrom(square: Square): Promise<void> {
const swapAction = (source: Square, target: Square) => new Action( const swapAction = (source: Square, target: Square) => new Action(
() => { () => {
source.hasMine = false; source.hasMine = false;
@ -174,21 +174,20 @@ export class Field {
() => true () => true
); );
this.runUndoably(() => { await this.runUndoably(async () => {
if (square.hasMine) { if (square.hasMine) {
const target = this.squareList.find(it => !it.hasMine && it !== square)!; const target = this.squareList.find(it => !it.hasMine && it !== square)!;
this.addAction(swapAction(square, target)); await this.addAction(swapAction(square, target));
} }
square.neighbors const eligibleNeighbors = square.neighbors.filter(it => it.hasMine);
.filter(it => it.hasMine) for (const neighbors of eligibleNeighbors) {
.forEach(it => { const target = this.squareList
const target = this.squareList .find(it => !it.hasMine && it !== square && square.neighbors.indexOf(it) < 0);
.find(it => !it.hasMine && it !== square && square.neighbors.indexOf(it) < 0);
if (target !== undefined) if (target !== undefined)
this.addAction(swapAction(it, target)); await this.addAction(swapAction(neighbors, target));
}); }
}); });
} }
@ -214,7 +213,7 @@ export class Field {
* *
* @param coords the coordinates of the square to chord * @param coords the coordinates of the square to chord
*/ */
chord(coords: { x: number, y: number }): void { async chord(coords: { x: number, y: number }): Promise<void> {
const square = this.squares[coords.y][coords.x]; const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot chord undefined square at (${coords}).`); if (square === undefined) throw new Error(`Cannot chord undefined square at (${coords}).`);
@ -223,10 +222,11 @@ export class Field {
if (square.getNeighborCount(it => it.hasFlag) !== square.getNeighborCount(it => it.hasMine)) return; if (square.getNeighborCount(it => it.hasFlag) !== square.getNeighborCount(it => it.hasMine)) return;
if (!this.isAutoSolving) this.statistics.squaresChorded++; if (!this.isAutoSolving) this.statistics.squaresChorded++;
this.runUndoably(() => { await this.runUndoably(async () => {
square.neighbors const eligibleNeighbors = square.neighbors.filter(it => it.isCovered && !it.hasFlag);
.filter(it => it.isCovered && !it.hasFlag) for (const neighbor of eligibleNeighbors) {
.forEach(it => this.uncover(it.coords)); await this.uncover(neighbor.coords);
}
}); });
if (!this.isAutoSolving && this.hasLost) this.statistics.squaresChordedLeadingToLoss++; if (!this.isAutoSolving && this.hasLost) this.statistics.squaresChordedLeadingToLoss++;
@ -237,31 +237,50 @@ export class Field {
* *
* @param coords the coordinates of the square to uncover * @param coords the coordinates of the square to uncover
*/ */
uncover(coords: { x: number, y: number }): void { async uncover(coords: { x: number, y: number }): Promise<void> {
const square = this.squares[coords.y][coords.x]; const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot uncover undefined square at (${coords}).`); if (square === undefined) throw new Error(`Cannot uncover undefined square at (${coords}).`);
if (this.hasWon || this.hasLost) return; if (this.hasWon || this.hasLost) return;
this.runUndoably(() => { await this.runUndoably(async () => {
if (!this.hasStarted) { if (!this.hasStarted) {
this.statistics.gamesStarted++; this.statistics.gamesStarted++;
if (this.isSolvable) { if (this.isSolvable) {
let i = 1; let i = 0;
const time = Timer.time(() => { const time = await Timer.time(async () => {
while (!Solver.canSolve(this, coords)) { // while (!(await Solver.canSolve(this, coords))) {
this.shuffle(this.rng.uint32()); // this.shuffle(this.rng.uint32());
i++; // i++;
// }
let seed = undefined;
while (seed === undefined) {
const seeds = Array.from({length: 10}, () => this.rng.uint32());
i += seeds.length;
const tasks = seeds.map(async seed => {
const copy = this.copy();
copy.shuffle(seed);
return await Solver.canSolve(copy, coords);
});
const solvable = await Promise.all(tasks);
seed = seeds[solvable.indexOf(true)];
} }
this.shuffle(seed);
}); });
console.log(`Found solvable field in ${time}ms in ${i} attempts.`); console.log(`Found solvable field in ${time}ms in ${i} attempts.`);
} }
this.clearMines(square); await this.moveMinesAwayFrom(square);
this._hasStarted = true; this._hasStarted = true;
this.timer.start(); this.timer.start();
// @formatter:off // @formatter:off
this.addAction(new Action(() => {}, () => false, () => false)); await this.addAction(new Action(() => {}, () => false, () => false));
// @formatter:on // @formatter:on
} }
@ -271,7 +290,7 @@ export class Field {
if (!next.isCovered || next.hasFlag || next.hasMark) continue; if (!next.isCovered || next.hasFlag || next.hasMark) continue;
let remainingFlags: Square[] | undefined; let remainingFlags: Square[] | undefined;
this.addAction(new Action( await this.addAction(new Action(
() => { () => {
next.isCovered = false; next.isCovered = false;
if (!this.isAutoSolving) this.statistics.squaresUncovered++; if (!this.isAutoSolving) this.statistics.squaresUncovered++;
@ -335,12 +354,12 @@ export class Field {
* *
* @param coords the coordinates of the square to toggle the flag at * @param coords the coordinates of the square to toggle the flag at
*/ */
toggleFlag(coords: { x: number, y: number }): void { async toggleFlag(coords: { x: number, y: number }): Promise<void> {
const square = this.squares[coords.y][coords.x]; const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot toggle flag of undefined square at (${coords}).`); if (square === undefined) throw new Error(`Cannot toggle flag of undefined square at (${coords}).`);
if (!square.isCovered || square.hasMark || this.hasWon || this.hasLost) return; if (!square.isCovered || square.hasMark || this.hasWon || this.hasLost) return;
this.addAction(new Action( await this.addAction(new Action(
() => { () => {
square.hasFlag = !square.hasFlag; square.hasFlag = !square.hasFlag;
if (!this.isAutoSolving && square.hasFlag) this.statistics.squaresFlagged++; if (!this.isAutoSolving && square.hasFlag) this.statistics.squaresFlagged++;
@ -360,12 +379,12 @@ export class Field {
* *
* @param coords the coordinates of the square to toggle the question mark at * @param coords the coordinates of the square to toggle the question mark at
*/ */
toggleMark(coords: { x: number, y: number }): void { async toggleMark(coords: { x: number, y: number }): Promise<void> {
const square = this.squares[coords.y][coords.x]; const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot toggle flag of undefined square at (${coords}).`); if (square === undefined) throw new Error(`Cannot toggle flag of undefined square at (${coords}).`);
if (!square.isCovered || square.hasFlag || this.hasWon || this.hasLost) return; if (!square.isCovered || square.hasFlag || this.hasWon || this.hasLost) return;
this.addAction(new Action( await this.addAction(new Action(
() => { () => {
square.hasMark = !square.hasMark; square.hasMark = !square.hasMark;
if (!this.isAutoSolving && square.hasMark) this.statistics.squaresMarked++; if (!this.isAutoSolving && square.hasMark) this.statistics.squaresMarked++;
@ -389,9 +408,9 @@ export class Field {
* @param callback a function such that all its calls to `#addAction` should be undoable with a single invocation of * @param callback a function such that all its calls to `#addAction` should be undoable with a single invocation of
* `#undo` * `#undo`
*/ */
runUndoably(callback: () => void): void { async runUndoably(callback: () => Promise<void>): Promise<void> {
this.history.startSequence(); this.history.startSequence();
callback(); await callback();
this.history.commitSequence(); this.history.commitSequence();
} }
@ -403,11 +422,11 @@ export class Field {
* @param action the action that can be undone * @param action the action that can be undone
* @private * @private
*/ */
private addAction(action: Action): void { private async addAction(action: Action): Promise<void> {
if (this.history.hasUncommittedSequence) if (this.history.hasUncommittedSequence)
this.history.addAction(action); this.history.addAction(action);
else else
this.runUndoably(() => this.history.addAction(action)); await this.runUndoably(async () => this.history.addAction(action));
action.run(); action.run();
} }

View File

@ -218,12 +218,12 @@ export class Game {
this.hintForm = $("#hintForm"); this.hintForm = $("#hintForm");
this.hintForm.addEventListener( this.hintForm.addEventListener(
"submit", "submit",
event => { async event => {
event.preventDefault(); event.preventDefault();
if (this.field !== null) { if (this.field !== null) {
this.statistics.hintsRequested++; this.statistics.hintsRequested++;
this.display.hintSquare = Solver.getHint(this.field); this.display.hintSquare = await Solver.getHint(this.field);
} }
blurActiveElement(); blurActiveElement();
} }
@ -233,12 +233,12 @@ export class Game {
this.solveForm = $("#solveForm"); this.solveForm = $("#solveForm");
this.solveForm.addEventListener( this.solveForm.addEventListener(
"submit", "submit",
event => { async event => {
event.preventDefault(); event.preventDefault();
if (this.field !== null) { if (this.field !== null) {
this.statistics.solverUsages++; this.statistics.solverUsages++;
Solver.solve(this.field); await Solver.solve(this.field);
} }
blurActiveElement(); blurActiveElement();
} }
@ -355,11 +355,11 @@ export class Game {
this.canvas.addEventListener("contextmenu", event => event.preventDefault()); this.canvas.addEventListener("contextmenu", event => event.preventDefault());
this.canvas.addEventListener( this.canvas.addEventListener(
"mousedown", "mousedown",
event => { async event => {
event.preventDefault(); event.preventDefault();
if (this.field === null) return; if (this.field === null) return;
this.field.runUndoably(() => { await this.field.runUndoably(async () => {
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY}); const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
if (this.field === null || !this.field.hasSquareAt(coords)) return; if (this.field === null || !this.field.hasSquareAt(coords)) return;
@ -372,13 +372,13 @@ export class Game {
const square = this.field.getSquareOrElse(coords); const square = this.field.getSquareOrElse(coords);
if (square !== null) { if (square !== null) {
if (square.hasFlag) { if (square.hasFlag) {
this.field.toggleFlag(coords); await this.field.toggleFlag(coords);
if (preferences.marksEnabled) if (preferences.marksEnabled)
this.field.toggleMark(coords); await this.field.toggleMark(coords);
} else if (square.hasMark) { } else if (square.hasMark) {
this.field.toggleMark(coords); await this.field.toggleMark(coords);
} else { } else {
this.field.toggleFlag(coords); await this.field.toggleFlag(coords);
} }
} }
} }
@ -395,30 +395,30 @@ export class Game {
); );
this.canvas.addEventListener( this.canvas.addEventListener(
"mouseup", "mouseup",
event => { async event => {
event.preventDefault(); event.preventDefault();
if (this.field === null) return; if (this.field === null) return;
this.field.runUndoably(() => { await this.field.runUndoably(async () => {
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY}); const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
if (this.field === null || !this.field.hasSquareAt(coords)) return; if (this.field === null || !this.field.hasSquareAt(coords)) return;
switch (event.button) { switch (event.button) {
case 0: case 0:
if (this.leftDown && this.rightDown) if (this.leftDown && this.rightDown)
this.field.chord(coords); await this.field.chord(coords);
else if (!this.holdsAfterChord && this.leftDown) else if (!this.holdsAfterChord && this.leftDown)
this.field.uncover(coords); await this.field.uncover(coords);
this.leftDown = false; this.leftDown = false;
this.holdsAfterChord = this.rightDown; this.holdsAfterChord = this.rightDown;
break; break;
case 1: case 1:
this.field.chord(coords); await this.field.chord(coords);
break; break;
case 2: case 2:
if (this.leftDown && this.rightDown) if (this.leftDown && this.rightDown)
this.field.chord(coords); await this.field.chord(coords);
this.rightDown = false; this.rightDown = false;
this.holdsAfterChord = this.leftDown; this.holdsAfterChord = this.leftDown;

View File

@ -13,28 +13,30 @@ export class Solver {
* *
* @param field the field to solve * @param field the field to solve
*/ */
static solve(field: Field): void { static async solve(field: Field): Promise<void> {
if (field.hasWon || field.hasLost) return; if (field.hasWon || field.hasLost) return;
if (field.hasStarted && !this.step(field.copy())) return; if (field.hasStarted && !(await this.step(field.copy()))) return;
if (!field.hasStarted) { if (!field.hasStarted) {
field.isAutoSolving = true; field.isAutoSolving = true;
field.runUndoably(() => { await field.runUndoably(async () => {
const target = {x: Math.floor(field.width / 2), y: Math.floor(field.height / 2)}; const target = {x: Math.floor(field.width / 2), y: Math.floor(field.height / 2)};
const targetSquare = field.getSquareOrElse(target, undefined)!; const targetSquare = field.getSquareOrElse(target, undefined)!;
if (targetSquare.hasFlag) field.toggleFlag(target); if (targetSquare.hasFlag) await field.toggleFlag(target);
if (targetSquare.hasMark) field.toggleMark(target); if (targetSquare.hasMark) await field.toggleMark(target);
field.uncover(target); await field.uncover(target);
}); });
field.isAutoSolving = false; field.isAutoSolving = false;
} }
field.isAutoSolving = true; field.isAutoSolving = true;
field.runUndoably(() => { await field.runUndoably(async () => {
field.squareList.filter(it => it.hasFlag).forEach(it => field.toggleFlag(it.coords)); for (const squares of field.squareList.filter(it => it.hasFlag))
field.squareList.filter(it => it.hasMark).forEach(it => field.toggleMark(it.coords)); await field.toggleFlag(squares.coords);
for (const squares of field.squareList.filter(it => it.hasMark))
await field.toggleMark(squares.coords);
while (this.step(field)) { while (await this.step(field)) {
// Repeat until `step` returns false // Repeat until `step` returns false
} }
}); });
@ -52,10 +54,10 @@ export class Solver {
* @param field the field to check for solvability * @param field the field to check for solvability
* @param initialSquare the initial coordinates to click at * @param initialSquare the initial coordinates to click at
*/ */
static canSolve(field: Field, initialSquare: { x: number, y: number } | undefined = undefined): boolean { static async canSolve(field: Field, initialSquare: { x: number, y: number } | undefined = undefined): Promise<boolean> {
const copy = field.copy(); const copy = field.copy();
if (initialSquare !== undefined) copy.runUndoably(() => copy.uncover(initialSquare)); if (initialSquare !== undefined) await copy.runUndoably(async () => await copy.uncover(initialSquare));
this.solve(copy); await this.solve(copy);
return copy.hasWon; return copy.hasWon;
} }
@ -65,7 +67,7 @@ export class Solver {
* @param field the field to suggest a move for * @param field the field to suggest a move for
* @returns a suggestion for a next move based on the current state of the field * @returns a suggestion for a next move based on the current state of the field
*/ */
static getHint(field: Field): Square | null { static async getHint(field: Field): Promise<Square | null> {
if (!field.hasStarted || field.hasWon || field.hasLost) return null; if (!field.hasStarted || field.hasWon || field.hasLost) return null;
const knowns = Solver.getKnowns(field); const knowns = Solver.getKnowns(field);
@ -102,22 +104,22 @@ export class Solver {
* @returns `true` if a step could be solved * @returns `true` if a step could be solved
* @private * @private
*/ */
private static step(field: Field): boolean { private static async step(field: Field): Promise<boolean> {
let flagCount = field.flagCount; let flagCount = field.flagCount;
let coveredCount = field.coveredNonMineCount; let coveredCount = field.coveredNonMineCount;
if (field.hasWon || field.hasLost) if (field.hasWon || field.hasLost)
return false; return false;
this.stepSingleSquares(field); await this.stepSingleSquares(field);
if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount) if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount)
return true; return true;
this.stepNeighboringSquares(field); await this.stepNeighboringSquares(field);
if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount) if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount)
return true; return true;
this.stepAllSquares(field); await this.stepAllSquares(field);
// noinspection RedundantIfStatementJS // Makes it easier to add more steps // noinspection RedundantIfStatementJS // Makes it easier to add more steps
if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount) if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount)
return true; return true;
@ -134,13 +136,16 @@ export class Solver {
* @param field the field to solve * @param field the field to solve
* @private * @private
*/ */
private static stepSingleSquares(field: Field): void { private static async stepSingleSquares(field: Field): Promise<void> {
Solver.getKnowns(field) for (const square of Solver.getKnowns(field)) {
.forEach(square => { await field.chord(square);
field.chord(square);
if (square.getNeighborCount(it => it.isCovered) === square.getNeighborCount(it => it.hasMine)) if (square.getNeighborCount(it => it.isCovered) !== square.getNeighborCount(it => it.hasMine))
square.neighbors.filter(it => !it.hasFlag).forEach(it => field.toggleFlag(it)); continue;
});
for (const neighbor of square.neighbors.filter(it => !it.hasFlag))
await field.toggleFlag(neighbor);
}
} }
/** /**
@ -155,14 +160,14 @@ export class Solver {
* @param field the field to solve * @param field the field to solve
* @private * @private
*/ */
private static stepNeighboringSquares(field: Field): void { private static async stepNeighboringSquares(field: Field): Promise<void> {
const knowns = Solver.getKnowns(field); const knowns = Solver.getKnowns(field);
knowns.forEach(known => { for (const known of knowns) {
Solver.applySolution( await Solver.applySolution(
field, field,
this.matrixSolve(field, known.neighbors.filter(it => !it.isCovered).concat(known), true) this.matrixSolve(field, known.neighbors.filter(it => !it.isCovered).concat(known), true)
); );
}); }
} }
/** /**
@ -174,11 +179,11 @@ export class Solver {
* @param field the field to solve * @param field the field to solve
* @private * @private
*/ */
private static stepAllSquares(field: Field): void { private static async stepAllSquares(field: Field): Promise<void> {
if (!field.hasStarted || field.hasWon || field.hasLost) return; if (!field.hasStarted || field.hasWon || field.hasLost) return;
const knowns = Solver.getKnowns(field); const knowns = Solver.getKnowns(field);
Solver.applySolution( await Solver.applySolution(
field, field,
this.matrixSolve(field, knowns, false) this.matrixSolve(field, knowns, false)
); );
@ -245,14 +250,14 @@ export class Solver {
* @param solution the solution to apply * @param solution the solution to apply
* @private * @private
*/ */
private static applySolution(field: Field, solution: Solution): void { private static async applySolution(field: Field, solution: Solution): Promise<void> {
solution.forEach(target => { for (const target of solution) {
if (target === undefined) return; if (target === undefined) continue;
const [solution, square] = target; const [solution, square] = target;
if (solution === 0) field.uncover(square.coords); if (solution === 0) await field.uncover(square.coords);
else if (solution === 1) field.toggleFlag(square.coords); else if (solution === 1) await field.toggleFlag(square.coords);
}); }
} }
} }

View File

@ -69,9 +69,9 @@ export class Timer {
* *
* @param callback the callback to time the execution of * @param callback the callback to time the execution of
*/ */
time(callback: () => void): void { async time(callback: () => Promise<void>): Promise<void> {
this.start(); this.start();
callback(); await callback();
this.stop(); this.stop();
} }
@ -82,9 +82,9 @@ export class Timer {
* @param callback the function to time the execution of * @param callback the function to time the execution of
* @returns the number of milliseconds the callback took to execute * @returns the number of milliseconds the callback took to execute
*/ */
static time(callback: () => void): number { static async time(callback: () => Promise<void>): Promise<number> {
const timer = new Timer(); const timer = new Timer();
timer.time(callback); await timer.time(callback);
return timer.elapsedTime; return timer.elapsedTime;
} }
} }