parent
b4f2586aa8
commit
9843b24770
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "minesweeper",
|
||||
"version": "0.83.0",
|
||||
"version": "0.83.1",
|
||||
"description": "Just Minesweeper!",
|
||||
"author": "Florine W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -71,10 +71,10 @@
|
|||
</canvas>
|
||||
</div>
|
||||
<footer class="controls">
|
||||
<a role="button" href="#" id="undo"><i class="fa fa-undo"></i> Undo</a>
|
||||
<a role="button" href="#" id="redo"><i class="fa fa-repeat"></i> Redo</a>
|
||||
<a role="button" href="#" id="hint"><i class="fa fa-lightbulb-o"></i> Hint</a>
|
||||
<a role="button" href="#" id="solve"><i class="fa fa-key"></i> Solve</a>
|
||||
<a role="button" id="undo"><i class="fa fa-undo"></i> Undo</a>
|
||||
<a role="button" id="redo"><i class="fa fa-repeat"></i> Redo</a>
|
||||
<a role="button" id="hint"><i class="fa fa-lightbulb-o"></i> Hint</a>
|
||||
<a role="button" id="solve"><i class="fa fa-key"></i> Solve</a>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
|
|
@ -146,21 +146,12 @@ export class ActionHistory {
|
|||
|
||||
|
||||
/**
|
||||
* Redoes up to the last `amount` of sequences that were undone.
|
||||
* Returns `true` if and only if there is an action sequence that can be undone.
|
||||
*
|
||||
* @param amount the maximum number of sequences to redo
|
||||
* @returns the actual amount of sequences that have been redone
|
||||
* @returns `true` if and only if there is an action sequence that can be undone
|
||||
*/
|
||||
redo(amount: number = this.sequences.length - (this.sequenceIndex + 1)): number {
|
||||
if (this.hasUncommittedSequence)
|
||||
throw new Error("Cannot redo sequences while there is an uncommitted sequence.");
|
||||
|
||||
amount = Math.min(amount, this.sequences.length - (this.sequenceIndex + 1));
|
||||
for (let i = 0; i < amount; i++) {
|
||||
this.sequenceIndex++;
|
||||
this.currentSequence!.run();
|
||||
}
|
||||
return amount;
|
||||
canUndo(): boolean {
|
||||
return this.sequenceIndex >= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -186,4 +177,31 @@ export class ActionHistory {
|
|||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if and only if there is an action sequence that can be redone.
|
||||
*
|
||||
* @returns `true` if and only if there is an action sequence that can be redone
|
||||
*/
|
||||
canRedo(): boolean {
|
||||
return this.sequenceIndex < this.sequences.length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes up to the last `amount` of sequences that were undone.
|
||||
*
|
||||
* @param amount the maximum number of sequences to redo
|
||||
* @returns the actual amount of sequences that have been redone
|
||||
*/
|
||||
redo(amount: number = this.sequences.length - (this.sequenceIndex + 1)): number {
|
||||
if (this.hasUncommittedSequence)
|
||||
throw new Error("Cannot redo sequences while there is an uncommitted sequence.");
|
||||
|
||||
amount = Math.min(amount, this.sequences.length - (this.sequenceIndex + 1));
|
||||
for (let i = 0; i < amount; i++) {
|
||||
this.sequenceIndex++;
|
||||
this.currentSequence!.run();
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -279,7 +279,7 @@ export class Display {
|
|||
.filter(it => it.isCovered)
|
||||
.filter(it => {
|
||||
// True if square should be covered
|
||||
if (this.field!.hasLost || this.field!.hasWon || this.mouseSquare == null)
|
||||
if (this.field!.isOver || this.mouseSquare == null)
|
||||
return true;
|
||||
if (this.mouseHoldUncover && this.mouseSquare === it)
|
||||
return it.hasFlag || it.hasMark;
|
||||
|
|
|
@ -54,6 +54,10 @@ export class Field {
|
|||
return this._hasLost;
|
||||
}
|
||||
|
||||
get isOver(): boolean {
|
||||
return this.hasWon || this.hasLost;
|
||||
}
|
||||
|
||||
private _deathCount: number = 0;
|
||||
get deathCount(): number {
|
||||
return this._deathCount;
|
||||
|
@ -61,6 +65,8 @@ export class Field {
|
|||
|
||||
isAutoSolving: boolean = false;
|
||||
|
||||
private _changeListeners: (() => void)[] = [];
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new playing field for a game of Minesweeper.
|
||||
|
@ -221,7 +227,7 @@ export class Field {
|
|||
const square = this.squares[coords.y][coords.x];
|
||||
|
||||
if (square === undefined) throw new Error(`Cannot chord undefined square at (${coords}).`);
|
||||
if (square.isCovered || this.hasWon || this.hasLost) return;
|
||||
if (square.isCovered || this.isOver) return;
|
||||
if (square.getNeighborCount(it => it.hasMark) > 0) return;
|
||||
if (square.getNeighborCount(it => it.hasFlag) !== square.getNeighborCount(it => it.hasMine)) return;
|
||||
|
||||
|
@ -231,6 +237,7 @@ export class Field {
|
|||
.filter(it => it.isCovered && !it.hasFlag)
|
||||
.forEach(it => this.uncover(it.coords));
|
||||
});
|
||||
this.invokeEventListeners();
|
||||
|
||||
if (!this.isAutoSolving && this.hasLost) this.statistics.squaresChordedLeadingToLoss++;
|
||||
}
|
||||
|
@ -243,7 +250,7 @@ export class Field {
|
|||
uncover(coords: { x: number, y: number }): 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;
|
||||
if (this.isOver) return;
|
||||
|
||||
this.runUndoably(() => {
|
||||
if (!this.hasStarted) {
|
||||
|
@ -331,6 +338,7 @@ export class Field {
|
|||
uncoverQueue.push(...next.neighbors.filter(it => it.isCovered));
|
||||
}
|
||||
});
|
||||
this.invokeEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -341,7 +349,7 @@ export class Field {
|
|||
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;
|
||||
if (!square.isCovered || square.hasMark || this.isOver) return;
|
||||
|
||||
this.addAction(new Action(
|
||||
() => {
|
||||
|
@ -356,6 +364,7 @@ export class Field {
|
|||
},
|
||||
() => true
|
||||
));
|
||||
this.invokeEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -366,7 +375,7 @@ export class Field {
|
|||
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;
|
||||
if (!square.isCovered || square.hasFlag || this.isOver) return;
|
||||
|
||||
this.addAction(new Action(
|
||||
() => {
|
||||
|
@ -380,6 +389,7 @@ export class Field {
|
|||
},
|
||||
() => true
|
||||
));
|
||||
this.invokeEventListeners();
|
||||
}
|
||||
|
||||
|
||||
|
@ -416,18 +426,12 @@ export class Field {
|
|||
}
|
||||
|
||||
/**
|
||||
* Redoes the last `amount` actions in the field's history, if possible.
|
||||
* Returns `true` if and only if there is an action that can be undone.
|
||||
*
|
||||
* Redoing an action removes all subsequent actions from its history.
|
||||
*
|
||||
* @param amount the maximum amount of actions to redo, or all future actions if `undefined`
|
||||
* @returns `true` if and only if there is an action that can be undone
|
||||
*/
|
||||
redo(amount: number | undefined = undefined): void {
|
||||
const wasLost = this.hasLost;
|
||||
|
||||
const redone = this.history.redo(amount);
|
||||
if (!this.isAutoSolving && redone > 0) this.statistics.actionsRedone++;
|
||||
if (!this.isAutoSolving && !wasLost && this.hasLost) this.statistics.lossesRedone++;
|
||||
canUndo(): boolean {
|
||||
return this.history.canUndo();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -441,6 +445,32 @@ export class Field {
|
|||
const undone = this.history.undo(amount);
|
||||
if (!this.isAutoSolving && undone > 0) this.statistics.actionsUndone++;
|
||||
if (!this.isAutoSolving && wasLost && !this.hasLost) this.statistics.lossesUndone++;
|
||||
this.invokeEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if and only if there is an action that can be redone.
|
||||
*
|
||||
* @returns `true` if and only if there is an action that can be redone
|
||||
*/
|
||||
canRedo(): boolean {
|
||||
return this.history.canRedo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes the last `amount` actions in the field's history, if possible.
|
||||
*
|
||||
* Redoing an action removes all subsequent actions from its history.
|
||||
*
|
||||
* @param amount the maximum amount of actions to redo, or all future actions if `undefined`
|
||||
*/
|
||||
redo(amount: number | undefined = undefined): void {
|
||||
const wasLost = this.hasLost;
|
||||
|
||||
const redone = this.history.redo(amount);
|
||||
if (!this.isAutoSolving && redone > 0) this.statistics.actionsRedone++;
|
||||
if (!this.isAutoSolving && !wasLost && this.hasLost) this.statistics.lossesRedone++;
|
||||
this.invokeEventListeners();
|
||||
}
|
||||
|
||||
|
||||
|
@ -452,6 +482,25 @@ export class Field {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set up `changeListener` to be invoked each time the state of this field changes.
|
||||
*
|
||||
* @param changeListener
|
||||
*/
|
||||
addEventListener(changeListener: () => void): void {
|
||||
this._changeListeners.push(changeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes each registered event listener.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private invokeEventListeners(): void {
|
||||
this._changeListeners.forEach(it => it());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the maximum number of mines that can be placed in a `width` x `height` field.
|
||||
*
|
||||
|
|
|
@ -27,6 +27,10 @@ export class Game {
|
|||
private readonly heightInput: HTMLInputElement;
|
||||
private readonly minesInput: HTMLInputElement;
|
||||
private readonly solvableInput: HTMLInputElement;
|
||||
private readonly undo: HTMLAnchorElement;
|
||||
private readonly redo: HTMLAnchorElement;
|
||||
private readonly hint: HTMLAnchorElement;
|
||||
private readonly solve: HTMLAnchorElement;
|
||||
private readonly statisticsDiv: HTMLDivElement;
|
||||
private readonly highScoresDiv: HTMLDivElement;
|
||||
|
||||
|
@ -145,13 +149,16 @@ export class Game {
|
|||
});
|
||||
|
||||
// Undo
|
||||
$("#undo").addEventListener("click", () => this.field?.undo(1));
|
||||
this.undo = $("#undo");
|
||||
this.undo.addEventListener("click", () => this.field?.undo(1));
|
||||
|
||||
// Redo
|
||||
$("#redo").addEventListener("click", () => this.field?.redo(1));
|
||||
this.redo = $("#redo");
|
||||
this.redo.addEventListener("click", () => this.field?.redo(1));
|
||||
|
||||
// Hint
|
||||
$("#hint").addEventListener(
|
||||
this.hint = $("#hint");
|
||||
this.hint.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
if (this.field != null) {
|
||||
|
@ -162,7 +169,8 @@ export class Game {
|
|||
);
|
||||
|
||||
// Solve
|
||||
$("#solve").addEventListener(
|
||||
this.solve = $("#solve");
|
||||
this.solve.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
if (this.field != null) {
|
||||
|
@ -227,6 +235,7 @@ export class Game {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
// Canvas
|
||||
this.canvas.addEventListener(
|
||||
"mousemove",
|
||||
|
@ -336,6 +345,25 @@ export class Game {
|
|||
this.minesInput.max = "" + Field.maxMines(+this.widthInput.value, +this.heightInput.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates states of controls relating to the game.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private updateControls(): void {
|
||||
const toggleHref = (element: HTMLAnchorElement | undefined, forceOn: boolean): void => {
|
||||
if (element === undefined) return;
|
||||
|
||||
if (forceOn) element.href = "#";
|
||||
else element.removeAttribute("href");
|
||||
};
|
||||
|
||||
toggleHref(this.undo, !!this.field?.canUndo());
|
||||
toggleHref(this.redo, !!this.field?.canRedo());
|
||||
toggleHref(this.hint, !this.field?.isOver);
|
||||
toggleHref(this.solve, !this.field?.isOver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the statistics report in the statistics overview.
|
||||
*
|
||||
|
@ -370,6 +398,7 @@ export class Game {
|
|||
this.highScores
|
||||
);
|
||||
this.display.setField(this.field);
|
||||
this.field.addEventListener(() => this.updateControls());
|
||||
|
||||
// Start timer for statistics
|
||||
let lastTime: number | null = null;
|
||||
|
|
|
@ -51,7 +51,6 @@ export class ModalDialog {
|
|||
event => {
|
||||
if (!(event.target instanceof Node)) return;
|
||||
|
||||
console.log("close");
|
||||
if (event.target !== openButton && !this.dialog.contains(event.target) || this.dialog === event.target)
|
||||
this.close();
|
||||
}
|
||||
|
@ -101,7 +100,6 @@ export class ModalDialog {
|
|||
* Opens the dialog.
|
||||
*/
|
||||
open(): void {
|
||||
console.log("opening");
|
||||
blurActiveElement();
|
||||
setTimeout(() => $("[autofocus]", this.dialog)?.focus(), 100);
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ export class Solver {
|
|||
let flagCount = field.flagCount;
|
||||
let coveredCount = field.coveredNonMineCount;
|
||||
|
||||
if (field.hasWon || field.hasLost)
|
||||
if (field.isOver)
|
||||
return false;
|
||||
|
||||
this.stepSingleSquares(field);
|
||||
|
|
Loading…
Reference in New Issue