Implement question marks

Fixes #75.
This commit is contained in:
Florine W. Dekker 2020-08-12 18:52:02 +02:00
parent bd5f9acb68
commit 1e840ae7c2
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
6 changed files with 104 additions and 36 deletions

View File

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

View File

@ -24,6 +24,7 @@ export class Display {
private coverSymbol: HTMLCanvasElement | undefined;
private flagSymbol: HTMLCanvasElement | undefined;
private markSymbol: HTMLCanvasElement | undefined;
private uncoveredMineSymbol: HTMLCanvasElement | undefined;
private mineSymbol: HTMLCanvasElement | undefined;
private digitSymbols: HTMLCanvasElement[] | undefined;
@ -74,6 +75,10 @@ export class Display {
ctx = this.flagSymbol.getContext("2d")!;
fillText(this.font.flag, this.font.fontFace, "#f00");
this.markSymbol = createCanvas(this.scale, this.scale);
ctx = this.markSymbol.getContext("2d")!;
fillText(this.font.mark, this.font.fontFace, "#00f");
this.uncoveredMineSymbol = createCanvas(this.scale, this.scale);
ctx = this.uncoveredMineSymbol.getContext("2d")!;
ctx.fillStyle = "#f00";
@ -275,9 +280,9 @@ export class Display {
if (this.field!.hasLost || this.field!.hasWon || this.mouseSquare === null)
return true;
if (this.mouseHoldUncover && this.mouseSquare === it)
return it.hasFlag;
return it.hasFlag || it.hasMark;
if (this.mouseHoldChord && (this.mouseSquare === it || this.mouseSquare.neighbors.indexOf(it) >= 0))
return it.hasFlag;
return it.hasFlag || it.hasMark;
return true;
})
@ -326,6 +331,8 @@ export class Display {
let icon;
if (square.hasFlag)
icon = this.flagSymbol;
else if (square.hasMark)
icon = this.markSymbol;
else if (square.hasMine && !square.isCovered)
icon = this.uncoveredMineSymbol;
else if (!square.isCovered)
@ -438,6 +445,7 @@ export interface IconFont {
fontFace: string;
flag: string;
mark: string;
uncoveredMine: string;
mine: string;
clock: string;
@ -451,6 +459,7 @@ export class BasicIconFont implements IconFont {
fontFace = "Courier New";
flag = "F";
mark = "?";
uncoveredMine = "X";
mine = "Mines";
clock = "Time";
@ -464,6 +473,7 @@ export class ForkAwesomeFont implements IconFont {
fontFace = "ForkAwesome";
flag = "\uf024";
mark = "\uf059";
uncoveredMine = "\uf1e2";
mine = "\uf1e2";
clock = "\uf017";

View File

@ -208,6 +208,7 @@ export class Field {
if (square === undefined) throw new Error(`Cannot chord undefined square at (${coords}).`);
if (square.isCovered || this.hasWon || this.hasLost) return;
if (square.getNeighborCount(it => it.hasMark) > 0) return;
if (square.getNeighborCount(it => it.hasFlag) !== square.getNeighborCount(it => it.hasMine)) return;
this.statistics.squaresChorded++;
@ -220,31 +221,6 @@ export class Field {
if (this.hasLost) this.statistics.squaresChordedLeadingToLoss++;
}
/**
* Toggles the flag at the given square.
*
* @param coords the coordinates of the square to flag
*/
flag(coords: { x: number, y: number }): 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 || this.hasWon || this.hasLost) return;
this.addAction(new Action(
() => {
square.hasFlag = !square.hasFlag;
if (square.hasFlag) this.statistics.squaresFlagged++;
this._flagCount += (square.hasFlag ? 1 : -1);
},
() => {
square.hasFlag = !square.hasFlag;
this._flagCount += (square.hasFlag ? 1 : -1);
return true;
},
() => true
));
}
/**
* Uncovers this square, revealing the contents beneath.
*
@ -282,7 +258,7 @@ export class Field {
const uncoverQueue: Square[] = [square];
while (uncoverQueue.length > 0) {
const next = uncoverQueue.pop()!;
if (!next.isCovered || next.hasFlag) continue;
if (!next.isCovered || next.hasFlag || next.hasMark) continue;
let remainingFlags: Square[] | undefined;
this.addAction(new Action(
@ -337,12 +313,61 @@ export class Field {
));
if (next.hasMine) break;
if (next.getNeighborCount(it => it.hasMine || it.hasFlag) === 0)
if (next.getNeighborCount(it => it.hasMine || it.hasFlag || it.hasMark) === 0)
uncoverQueue.push(...next.neighbors.filter(it => it.isCovered));
}
});
}
/**
* Toggles the flag at the given square.
*
* @param coords the coordinates of the square to toggle the flag at
*/
toggleFlag(coords: { x: number, y: number }): 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(
() => {
square.hasFlag = !square.hasFlag;
if (square.hasFlag) this.statistics.squaresFlagged++;
this._flagCount += (square.hasFlag ? 1 : -1);
},
() => {
square.hasFlag = !square.hasFlag;
this._flagCount += (square.hasFlag ? 1 : -1);
return true;
},
() => true
));
}
/**
* Toggles the question mark at the given square.
*
* @param coords the coordinates of the square to toggle the question mark at
*/
toggleMark(coords: { x: number, y: number }): 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(
() => {
square.hasMark = !square.hasMark;
if (square.hasMark) this.statistics.squaresMarked++;
},
() => {
square.hasMark = !square.hasMark;
if (square.hasMark) this.statistics.squaresMarked++;
return true;
},
() => true
));
}
/**
* Runs the given callback such that all calls to `#addAction` can be undone with a single invocation of `#undo`.
@ -433,6 +458,7 @@ export class Square {
isCovered: boolean;
hasMine: boolean;
hasFlag: boolean;
hasMark: boolean;
/**
@ -451,6 +477,7 @@ export class Square {
this.isCovered = true;
this.hasMine = hasMine;
this.hasFlag = false;
this.hasMark = false;
}
/**
@ -463,6 +490,7 @@ export class Square {
const copy = new Square(field, this.x, this.y, this.hasMine);
copy.isCovered = this.isCovered;
copy.hasFlag = this.hasFlag;
copy.hasMark = this.hasMark;
return copy;
}

View File

@ -293,8 +293,19 @@ export class Game {
this.leftDown = true;
break;
case 2:
if (!this.leftDown)
this.field.flag(coords);
if (!this.leftDown) {
const square = this.field.getSquareOrElse(coords);
if (square !== undefined) {
if (square.hasFlag) {
this.field.toggleFlag(coords);
this.field.toggleMark(coords);
} else if (square.hasMark) {
this.field.toggleMark(coords);
} else {
this.field.toggleFlag(coords);
}
}
}
this.rightDown = true;
break;

View File

@ -19,13 +19,15 @@ export class Solver {
if (!field.hasStarted) {
field.runUndoably(() => {
field.squareList.filter(it => it.hasFlag).forEach(it => field.flag(it.coords));
field.squareList.filter(it => it.hasFlag).forEach(it => field.toggleFlag(it.coords));
field.squareList.filter(it => it.hasMark).forEach(it => field.toggleMark(it.coords));
field.uncover({x: Math.floor(field.width / 2), y: Math.floor(field.height / 2)});
});
}
field.runUndoably(() => {
field.squareList.filter(it => it.hasFlag).forEach(it => field.flag(it.coords));
field.squareList.filter(it => it.hasFlag).forEach(it => field.toggleFlag(it.coords));
field.squareList.filter(it => it.hasMark).forEach(it => field.toggleMark(it.coords));
let flagCount = -1;
let coveredCount = -1;
@ -137,7 +139,7 @@ export class Solver {
.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.flag(it));
square.neighbors.filter(it => !it.hasFlag).forEach(it => field.toggleFlag(it));
});
}
@ -249,7 +251,7 @@ export class Solver {
const [solution, square] = target;
if (solution === 0) field.uncover(square.coords);
else if (solution === 1) field.flag(square.coords);
else if (solution === 1) field.toggleFlag(square.coords);
});
}
}

View File

@ -18,6 +18,7 @@ export interface Statistics {
squaresChorded: number;
squaresChordedLeadingToLoss: number;
squaresFlagged: number;
squaresMarked: number;
squaresUncovered: number;
hintsRequested: number;
@ -171,6 +172,16 @@ export class LocalStatistics implements Statistics {
this.write(statistics);
}
get squaresMarked(): number {
return +(this.read()["squaresMarked"] ?? 0);
}
set squaresMarked(value: number) {
const statistics = this.read();
statistics["squaresMarked"] = "" + value;
this.write(statistics);
}
get squaresUncovered(): number {
return +(this.read()["squaresUncovered"] ?? 0);
}
@ -261,6 +272,10 @@ export class LocalStatistics implements Statistics {
<th>Squares flagged</th>
<td>${this.squaresFlagged}</td>
</tr>
<tr>
<th>Squares marked</th>
<td>${this.squaresMarked}</td>
</tr>
<tr>
<th>Squares uncovered</th>
<td>${this.squaresUncovered}</td>
@ -296,6 +311,7 @@ export class MemoryStatistics implements Statistics {
squaresChorded: number = 0;
squaresChordedLeadingToLoss: number = 0;
squaresFlagged: number = 0;
squaresMarked: number = 0;
squaresUncovered: number = 0;
hintsRequested: number = 0;
solverUsages: number = 0;
@ -313,6 +329,7 @@ export class MemoryStatistics implements Statistics {
this.squaresChorded = 0;
this.squaresChordedLeadingToLoss = 0;
this.squaresFlagged = 0;
this.squaresMarked = 0;
this.squaresUncovered = 0;
this.hintsRequested = 0;
this.solverUsages = 0;