parent
5b62f2c427
commit
0e9c1816c7
|
@ -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",
|
||||
|
|
|
@ -68,6 +68,11 @@
|
|||
<button><i class="fa fa-repeat"></i> Redo</button>
|
||||
</form>
|
||||
|
||||
<!-- Hint -->
|
||||
<form id="hintForm">
|
||||
<button><i class="fa fa-question-circle-o"></i> Hint</button>
|
||||
</form>
|
||||
|
||||
<!-- Solver -->
|
||||
<form id="solveForm">
|
||||
<button><i class="fa fa-fast-forward"></i> Solve</button>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue