Implement hint button

Fixes #12.
This commit is contained in:
Florine W. Dekker 2020-08-09 17:38:39 +02:00
parent 5b62f2c427
commit 0e9c1816c7
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
6 changed files with 154 additions and 36 deletions

View File

@ -1,6 +1,6 @@
{
"name": "minesweeper",
"version": "0.79.3",
"version": "0.80.0",
"description": "Just Minesweeper!",
"author": "Felix W. Dekker",
"browser": "dist/bundle.js",

View File

@ -68,6 +68,11 @@
<button><i class="fa fa-repeat"></i>&emsp;Redo</button>
</form>
<!-- Hint -->
<form id="hintForm">
<button><i class="fa fa-question-circle-o"></i>&emsp;Hint</button>
</form>
<!-- Solver -->
<form id="solveForm">
<button><i class="fa fa-fast-forward"></i>&emsp;Solve</button>

View File

@ -15,6 +15,7 @@ export class Display {
private field: Field | null = null;
private winTime: number | null = null;
private loseTime: number | null = null;
hintSquare: Square | null = null;
mouseSquare: Square | null = null;
mouseHoldUncover: boolean = false;
mouseHoldChord: boolean = false;
@ -154,6 +155,7 @@ export class Display {
* @param field the field to draw, or `null` if no field should be drawn
*/
setField(field: Field | null): void {
this.hintSquare = null;
this.field = field;
if (this.field === null) return;
@ -294,6 +296,13 @@ export class Display {
.filter(it => it.getNeighborCount(it => it.hasMine) < it.getNeighborCount(it => it.hasFlag))
.forEach(square => ctx.fillRect(square.x * this.scale, square.y * this.scale, this.scale, this.scale));
ctx.restore();
if (this.hintSquare !== null) {
ctx.save();
ctx.fillStyle = "rgba(0, 255, 0, 0.3)";
ctx.fillRect(this.hintSquare.x * this.scale, this.hintSquare.y * this.scale, this.scale, this.scale);
ctx.restore();
}
}
/**

View File

@ -27,6 +27,7 @@ export class Game {
private readonly seedInput: HTMLFormElement;
private readonly undoForm: HTMLFormElement;
private readonly redoForm: HTMLFormElement;
private readonly hintForm: HTMLFormElement;
private readonly solveForm: HTMLFormElement;
private readonly customDifficultyOverlay: Overlay;
private readonly widthInput: HTMLInputElement;
@ -195,6 +196,20 @@ export class Game {
}
);
// Hint
this.hintForm = $("#hintForm");
this.hintForm.addEventListener(
"submit",
event => {
event.preventDefault();
if (this.field !== null) {
this.statistics.hintsRequested++;
this.display.hintSquare = (new Solver()).getHint(this.field);
}
}
);
// Solve
this.solveForm = $("#solveForm");
this.solveForm.addEventListener(
@ -202,8 +217,10 @@ export class Game {
event => {
event.preventDefault();
if (this.field !== null)
new Solver().solve(this.field);
if (this.field !== null) {
this.statistics.solverUsages++;
(new Solver()).solve(this.field);
}
}
);
@ -281,6 +298,7 @@ export class Game {
}
});
this.display.hintSquare = null;
this.display.mouseHoldUncover = this.leftDown && !this.holdsAfterChord;
this.display.mouseHoldChord = this.leftDown && this.rightDown;
}
@ -318,6 +336,7 @@ export class Game {
}
});
this.display.hintSquare = null;
this.display.mouseHoldUncover = this.leftDown && !this.holdsAfterChord;
this.display.mouseHoldChord = this.leftDown && this.rightDown;
}

View File

@ -87,6 +87,36 @@ export class Solver {
return copy.hasWon;
}
/**
* Returns a suggestion for a next move based on the current state of the field.
*
* @param field the field to suggest a move for
* @returns a suggestion for a next move based on the current state of the field
*/
getHint(field: Field): Square | null {
if (!field.hasStarted || field.hasWon || field.hasLost) return null;
let candidate: Square | undefined;
const knowns = this.getKnowns(field);
candidate = knowns.find(square =>
// Can chord
square.getNeighborCount(it => it.hasFlag) === square.getNeighborCount(it => it.hasMine) ||
// Can flag
square.getNeighborCount(it => it.isCovered && !it.hasFlag) ===
(square.getNeighborCount(it => it.hasMine) - square.getNeighborCount(it => it.hasFlag))
);
if (candidate !== undefined) return candidate;
candidate = knowns.find(square =>
this.matrixSolve(field, square.neighbors.filter(it => !it.isCovered).concat(square), true)
.some(it => it !== undefined)
);
if (candidate !== undefined) return candidate;
return null;
}
/**
* Solves the field as much as by considering just one square at a time and looking for trivial solutions.
@ -98,8 +128,7 @@ export class Solver {
* @private
*/
private stepSingleSquares(field: Field): void {
field.squareList
.filter(it => !it.isCovered)
this.getKnowns(field)
.forEach(square => {
field.chord(square);
if (square.getNeighborCount(it => it.isCovered) === square.getNeighborCount(it => it.hasMine))
@ -120,19 +149,12 @@ export class Solver {
* @private
*/
private stepNeighboringSquares(field: Field): void {
const knowns = field.squareList
.filter(it => !it.isCovered)
.filter(it => it.getNeighborCount(it => it.isCovered && !it.hasFlag) > 0);
const knowns = this.getKnowns(field);
knowns.forEach(known => {
const system = this.matrixSolve(field, known.neighbors.filter(it => !it.isCovered).concat(known), true);
system.forEach(target => {
if (target === undefined) return;
const [solution, square] = target;
if (solution === 0) field.uncover(square.coords);
else if (solution === 1) field.flag(square.coords);
});
this.applySolution(
field,
this.matrixSolve(field, known.neighbors.filter(it => !it.isCovered).concat(known), true)
);
});
}
@ -148,18 +170,11 @@ export class Solver {
private stepAllSquares(field: Field): void {
if (!field.hasStarted || field.hasWon || field.hasLost) return;
const knowns = field.squareList
.filter(it => !it.isCovered)
.filter(it => it.getNeighborCount(it => it.isCovered && !it.hasFlag) > 0);
const system = this.matrixSolve(field, knowns, false);
system.forEach(target => {
if (target === undefined) return;
const [solution, square] = target;
if (solution === 0) field.uncover(square.coords);
else if (solution === 1) field.flag(square.coords);
});
const knowns = this.getKnowns(field);
this.applySolution(
field,
this.matrixSolve(field, knowns, false)
);
}
/**
@ -200,6 +215,38 @@ export class Solver {
return new Matrix(matrix).solveBinary()
.map((it, i) => it === undefined ? undefined : [it, unknowns[i]]);
}
/**
* Returns all uncovered squares that have at least one covered unflagged neighbor.
*
* @param field the field to find the known squares in
* @returns all uncovered squares that have at least one covered unflagged neighbor
* @private
*/
private getKnowns(field: Field): Square[] {
return field.squareList
.filter(it => !it.isCovered)
.filter(it => it.getNeighborCount(it => it.isCovered && !it.hasFlag) > 0);
}
/**
* Applies the given solution to the field.
*
* @param field the field to apply the solution to
* @param solution an array in which each `undefined` element is ignored, and each pair contains a square and the
* number `1` if the square contains a mine or the number `0` if the square does not contain a mine
* @private
*/
private applySolution(field: Field, solution: ([number, Square] | undefined)[]): void {
solution.forEach(target => {
if (target === undefined) return;
const [solution, square] = target;
if (solution === 0) field.uncover(square.coords);
else if (solution === 1) field.flag(square.coords);
});
}
}

View File

@ -8,6 +8,7 @@ export interface Statistics {
actionsUndone: number;
actionsRedone: number;
lossesUndone: number;
timeSpent: number;
gamesStarted: number;
gamesLost: number;
@ -19,7 +20,8 @@ export interface Statistics {
squaresFlagged: number;
squaresUncovered: number;
timeSpent: number;
hintsRequested: number;
solverUsages: number;
/**
@ -89,6 +91,16 @@ export class LocalStatistics implements Statistics {
this.write(statistics);
}
get timeSpent(): number {
return +(this.read()["timeSpent"] ?? 0);
}
set timeSpent(value: number) {
const statistics = this.read();
statistics["timeSpent"] = "" + value;
this.write(statistics);
}
get gamesStarted(): number {
return +(this.read()["gamesStarted"] ?? 0);
}
@ -169,13 +181,23 @@ export class LocalStatistics implements Statistics {
this.write(statistics);
}
get timeSpent(): number {
return +(this.read()["timeSpent"] ?? 0);
get hintsRequested(): number {
return +(this.read()["hintsRequested"] ?? 0);
}
set timeSpent(value: number) {
set hintsRequested(value: number) {
const statistics = this.read();
statistics["timeSpent"] = "" + value;
statistics["hintsRequested"] = "" + value;
this.write(statistics);
}
get solverUsages(): number {
return +(this.read()["solverUsages"] ?? 0);
}
set solverUsages(value: number) {
const statistics = this.read();
statistics["solverUsages"] = "" + value;
this.write(statistics);
}
@ -243,6 +265,18 @@ export class LocalStatistics implements Statistics {
<th>Squares uncovered</th>
<td>${this.squaresUncovered}</td>
</tr>
</table>
<h3>Solver usage</h3>
<table>
<tr>
<th>Hints requested</th>
<td>${this.hintsRequested}</td>
</tr>
<tr>
<th>Solver usages</th>
<td>${this.solverUsages}</td>
</tr>
</table>`;
}
}
@ -254,6 +288,7 @@ export class MemoryStatistics implements Statistics {
actionsUndone: number = 0;
actionsRedone: number = 0;
lossesUndone: number = 0;
timeSpent: number = 0;
gamesLost: number = 0;
gamesStarted: number = 0;
gamesWon: number = 0;
@ -262,13 +297,15 @@ export class MemoryStatistics implements Statistics {
squaresChordedLeadingToLoss: number = 0;
squaresFlagged: number = 0;
squaresUncovered: number = 0;
timeSpent: number = 0;
hintsRequested: number = 0;
solverUsages: number = 0;
clear() {
this.actionsUndone = 0;
this.actionsRedone = 0;
this.lossesUndone = 0;
this.timeSpent = 0;
this.gamesLost = 0;
this.gamesStarted = 0;
this.gamesWon = 0;
@ -277,6 +314,7 @@ export class MemoryStatistics implements Statistics {
this.squaresChordedLeadingToLoss = 0;
this.squaresFlagged = 0;
this.squaresUncovered = 0;
this.timeSpent = 0;
this.hintsRequested = 0;
this.solverUsages = 0;
}
}