Make async to solve in parallel

This commit is contained in:
Florine W. Dekker 2021-11-14 16:49:44 +01:00
parent 74b2cb9b27
commit 30e3467598
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
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
* @private
*/
private clearMines(square: Square): void {
private async moveMinesAwayFrom(square: Square): Promise<void> {
const swapAction = (source: Square, target: Square) => new Action(
() => {
source.hasMine = false;
@ -174,21 +174,20 @@ export class Field {
() => true
);
this.runUndoably(() => {
await this.runUndoably(async () => {
if (square.hasMine) {
const target = this.squareList.find(it => !it.hasMine && it !== square)!;
this.addAction(swapAction(square, target));
await this.addAction(swapAction(square, target));
}
square.neighbors
.filter(it => it.hasMine)
.forEach(it => {
const target = this.squareList
.find(it => !it.hasMine && it !== square && square.neighbors.indexOf(it) < 0);
const eligibleNeighbors = square.neighbors.filter(it => it.hasMine);
for (const neighbors of eligibleNeighbors) {
const target = this.squareList
.find(it => !it.hasMine && it !== square && square.neighbors.indexOf(it) < 0);
if (target !== undefined)
this.addAction(swapAction(it, target));
});
if (target !== undefined)
await this.addAction(swapAction(neighbors, target));
}
});
}
@ -214,7 +213,7 @@ export class Field {
*
* @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];
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 (!this.isAutoSolving) this.statistics.squaresChorded++;
this.runUndoably(() => {
square.neighbors
.filter(it => it.isCovered && !it.hasFlag)
.forEach(it => this.uncover(it.coords));
await this.runUndoably(async () => {
const eligibleNeighbors = square.neighbors.filter(it => it.isCovered && !it.hasFlag);
for (const neighbor of eligibleNeighbors) {
await this.uncover(neighbor.coords);
}
});
if (!this.isAutoSolving && this.hasLost) this.statistics.squaresChordedLeadingToLoss++;
@ -237,31 +237,50 @@ export class Field {
*
* @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];
if (square === undefined) throw new Error(`Cannot uncover undefined square at (${coords}).`);
if (this.hasWon || this.hasLost) return;
this.runUndoably(() => {
await this.runUndoably(async () => {
if (!this.hasStarted) {
this.statistics.gamesStarted++;
if (this.isSolvable) {
let i = 1;
const time = Timer.time(() => {
while (!Solver.canSolve(this, coords)) {
this.shuffle(this.rng.uint32());
i++;
let i = 0;
const time = await Timer.time(async () => {
// while (!(await Solver.canSolve(this, coords))) {
// this.shuffle(this.rng.uint32());
// 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.`);
}
this.clearMines(square);
await this.moveMinesAwayFrom(square);
this._hasStarted = true;
this.timer.start();
// @formatter:off
this.addAction(new Action(() => {}, () => false, () => false));
await this.addAction(new Action(() => {}, () => false, () => false));
// @formatter:on
}
@ -271,7 +290,7 @@ export class Field {
if (!next.isCovered || next.hasFlag || next.hasMark) continue;
let remainingFlags: Square[] | undefined;
this.addAction(new Action(
await this.addAction(new Action(
() => {
next.isCovered = false;
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
*/
toggleFlag(coords: { x: number, y: number }): void {
async toggleFlag(coords: { x: number, y: number }): Promise<void> {
const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot toggle flag of undefined square at (${coords}).`);
if (!square.isCovered || square.hasMark || this.hasWon || this.hasLost) return;
this.addAction(new Action(
await this.addAction(new Action(
() => {
square.hasFlag = !square.hasFlag;
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
*/
toggleMark(coords: { x: number, y: number }): void {
async toggleMark(coords: { x: number, y: number }): Promise<void> {
const square = this.squares[coords.y][coords.x];
if (square === undefined) throw new Error(`Cannot toggle flag of undefined square at (${coords}).`);
if (!square.isCovered || square.hasFlag || this.hasWon || this.hasLost) return;
this.addAction(new Action(
await this.addAction(new Action(
() => {
square.hasMark = !square.hasMark;
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
* `#undo`
*/
runUndoably(callback: () => void): void {
async runUndoably(callback: () => Promise<void>): Promise<void> {
this.history.startSequence();
callback();
await callback();
this.history.commitSequence();
}
@ -403,11 +422,11 @@ export class Field {
* @param action the action that can be undone
* @private
*/
private addAction(action: Action): void {
private async addAction(action: Action): Promise<void> {
if (this.history.hasUncommittedSequence)
this.history.addAction(action);
else
this.runUndoably(() => this.history.addAction(action));
await this.runUndoably(async () => this.history.addAction(action));
action.run();
}

View File

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

View File

@ -13,28 +13,30 @@ export class Solver {
*
* @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.hasStarted && !this.step(field.copy())) return;
if (field.hasStarted && !(await this.step(field.copy()))) return;
if (!field.hasStarted) {
field.isAutoSolving = true;
field.runUndoably(() => {
await field.runUndoably(async () => {
const target = {x: Math.floor(field.width / 2), y: Math.floor(field.height / 2)};
const targetSquare = field.getSquareOrElse(target, undefined)!;
if (targetSquare.hasFlag) field.toggleFlag(target);
if (targetSquare.hasMark) field.toggleMark(target);
field.uncover(target);
if (targetSquare.hasFlag) await field.toggleFlag(target);
if (targetSquare.hasMark) await field.toggleMark(target);
await field.uncover(target);
});
field.isAutoSolving = false;
}
field.isAutoSolving = true;
field.runUndoably(() => {
field.squareList.filter(it => it.hasFlag).forEach(it => field.toggleFlag(it.coords));
field.squareList.filter(it => it.hasMark).forEach(it => field.toggleMark(it.coords));
await field.runUndoably(async () => {
for (const squares of field.squareList.filter(it => it.hasFlag))
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
}
});
@ -52,10 +54,10 @@ export class Solver {
* @param field the field to check for solvability
* @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();
if (initialSquare !== undefined) copy.runUndoably(() => copy.uncover(initialSquare));
this.solve(copy);
if (initialSquare !== undefined) await copy.runUndoably(async () => await copy.uncover(initialSquare));
await this.solve(copy);
return copy.hasWon;
}
@ -65,7 +67,7 @@ export class Solver {
* @param field the field to suggest a move for
* @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;
const knowns = Solver.getKnowns(field);
@ -102,22 +104,22 @@ export class Solver {
* @returns `true` if a step could be solved
* @private
*/
private static step(field: Field): boolean {
private static async step(field: Field): Promise<boolean> {
let flagCount = field.flagCount;
let coveredCount = field.coveredNonMineCount;
if (field.hasWon || field.hasLost)
return false;
this.stepSingleSquares(field);
await this.stepSingleSquares(field);
if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount)
return true;
this.stepNeighboringSquares(field);
await this.stepNeighboringSquares(field);
if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount)
return true;
this.stepAllSquares(field);
await this.stepAllSquares(field);
// noinspection RedundantIfStatementJS // Makes it easier to add more steps
if (field.hasWon || field.flagCount !== flagCount || field.coveredNonMineCount !== coveredCount)
return true;
@ -134,13 +136,16 @@ export class Solver {
* @param field the field to solve
* @private
*/
private static stepSingleSquares(field: Field): void {
Solver.getKnowns(field)
.forEach(square => {
field.chord(square);
if (square.getNeighborCount(it => it.isCovered) === square.getNeighborCount(it => it.hasMine))
square.neighbors.filter(it => !it.hasFlag).forEach(it => field.toggleFlag(it));
});
private static async stepSingleSquares(field: Field): Promise<void> {
for (const square of Solver.getKnowns(field)) {
await field.chord(square);
if (square.getNeighborCount(it => it.isCovered) !== square.getNeighborCount(it => it.hasMine))
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
* @private
*/
private static stepNeighboringSquares(field: Field): void {
private static async stepNeighboringSquares(field: Field): Promise<void> {
const knowns = Solver.getKnowns(field);
knowns.forEach(known => {
Solver.applySolution(
for (const known of knowns) {
await Solver.applySolution(
field,
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
* @private
*/
private static stepAllSquares(field: Field): void {
private static async stepAllSquares(field: Field): Promise<void> {
if (!field.hasStarted || field.hasWon || field.hasLost) return;
const knowns = Solver.getKnowns(field);
Solver.applySolution(
await Solver.applySolution(
field,
this.matrixSolve(field, knowns, false)
);
@ -245,14 +250,14 @@ export class Solver {
* @param solution the solution to apply
* @private
*/
private static applySolution(field: Field, solution: Solution): void {
solution.forEach(target => {
if (target === undefined) return;
private static async applySolution(field: Field, solution: Solution): Promise<void> {
for (const target of solution) {
if (target === undefined) continue;
const [solution, square] = target;
if (solution === 0) field.uncover(square.coords);
else if (solution === 1) field.toggleFlag(square.coords);
});
if (solution === 0) await field.uncover(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
*/
time(callback: () => void): void {
async time(callback: () => Promise<void>): Promise<void> {
this.start();
callback();
await callback();
this.stop();
}
@ -82,9 +82,9 @@ export class Timer {
* @param callback the function to time the execution of
* @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();
timer.time(callback);
await timer.time(callback);
return timer.elapsedTime;
}
}