diff --git a/package-lock.json b/package-lock.json index 8dc9a9a..7ca695f 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 4b69dea..5656c91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minesweeper", - "version": "0.86.2", + "version": "0.86.3", "description": "Just Minesweeper!", "author": "Florine W. Dekker", "browser": "dist/bundle.js", diff --git a/src/main/js/Field.ts b/src/main/js/Field.ts index a222235..ed82ade 100644 --- a/src/main/js/Field.ts +++ b/src/main/js/Field.ts @@ -445,6 +445,16 @@ export class Field { this.invokeEventListeners(); } + /** + * Runs `#runUndoably` asynchronously. + */ + async runUndoablyAsync(callback: () => Promise): Promise { + this.history.startSequence(); + await callback(); + if (this.history.commitSequence()) + this.invokeEventListeners(); + } + /** * Runs and stores the given action such that it can be undone. * diff --git a/src/main/js/Game.ts b/src/main/js/Game.ts index 865bc7b..cd688f1 100644 --- a/src/main/js/Game.ts +++ b/src/main/js/Game.ts @@ -198,12 +198,12 @@ export class Game { this.solve = $("#solve"); this.solve.addEventListener( "click", - (event: MouseEvent) => { + async (event: MouseEvent) => { event.preventDefault(); if (this.field != null) { this.statistics.solverUsages++; - Solver.solve(this.field); + await Solver.solveAnimated(this.field); } this.display.hintSquare = null; diff --git a/src/main/js/Solver.ts b/src/main/js/Solver.ts index c3b12c4..64811d7 100644 --- a/src/main/js/Solver.ts +++ b/src/main/js/Solver.ts @@ -17,8 +17,43 @@ export class Solver { if (field.isOver) return; if (field.hasStarted && !this.step(field.copy())) return; + field.isAutoSolving = true; + this.solveStart(field); + field.runUndoably(() => { + this.clearUserInputs(field); + + while (this.step(field)) { + // Intentionally left empty + } + }); + field.isAutoSolving = false; + } + + /** + * Runs `#solve`, animating steps in between. + */ + static async solveAnimated(field: Field): Promise { + if (field.isOver) return; + if (field.hasStarted && !this.step(field.copy())) return; + + field.isAutoSolving = true; + this.solveStart(field); + await field.runUndoablyAsync(async () => { + this.clearUserInputs(field); + + while (this.step(field)) { + // Timeout in between steps to create animation + await new Promise(it => setTimeout(it, 10)); + } + }); + field.isAutoSolving = false; + } + + /** + * If the field has not started yet, clicks somewhere and starts solving. + */ + private static solveStart(field: Field): void { if (!field.hasStarted) { - field.isAutoSolving = true; field.runUndoably(() => { const target = {x: Math.floor(field.width / 2), y: Math.floor(field.height / 2)}; const targetSquare = field.getSquareOrElse(target)!; @@ -26,21 +61,21 @@ export class Solver { if (targetSquare.hasMark) field.toggleMark(target); 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)); - - while (this.step(field)) { - // Repeat until `step` returns false - } - }); - field.isAutoSolving = false; } + /** + * Removes all the user's inputs from the given field. + * + * @param field the field to remove the user's inputs from + * @private + */ + private static clearUserInputs(field: Field): void { + field.squareList.filter(it => it.hasFlag).forEach(it => field.toggleFlag(it.coords)); + field.squareList.filter(it => it.hasMark).forEach(it => field.toggleMark(it.coords)); + } + + /** * Returns `true` if and only if this solver can solve the given field. * @@ -127,6 +162,7 @@ export class Solver { return false; } + /** * Returns a suggestion for a next move based on the current state of the field. *