Move change detection control to Field
This commit is contained in:
parent
551d36ca56
commit
b7361256a2
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "minesweeper",
|
||||
"version": "0.72.2",
|
||||
"version": "0.73.0",
|
||||
"description": "Just Minesweeper!",
|
||||
"author": "Felix W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -156,19 +156,20 @@ export class Display {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the square at the given coordinates, or `null` if there is no square there.
|
||||
* Returns the square grid coordinates for the given client coordinates.
|
||||
*
|
||||
* Note that the returned coordinates need not actually be valid in the current field.
|
||||
*
|
||||
* @param pos the client-relative pixel coordinates to find the square at
|
||||
* @returns the square at the given coordinates
|
||||
* @returns the square grid coordinates corresponding to the given client coordinates
|
||||
*/
|
||||
posToSquare(pos: { x: number, y: number }): Square | null {
|
||||
posToSquare(pos: { x: number, y: number }): {x: number, y: number} {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const offset = this.getFieldOffset();
|
||||
return this.field?.getSquareOrElse(
|
||||
Math.floor((pos.x - rect.left - this.canvas.clientLeft - offset.x) / this.scale),
|
||||
Math.floor((pos.y - rect.top - this.canvas.clientTop - offset.y) / this.scale),
|
||||
null
|
||||
);
|
||||
return {
|
||||
x: Math.floor((pos.x - rect.left - this.canvas.clientLeft - offset.x) / this.scale),
|
||||
y: Math.floor((pos.y - rect.top - this.canvas.clientTop - offset.y) / this.scale)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@ export class Field {
|
|||
*/
|
||||
onUncover(square: Square): void {
|
||||
if (square.isCovered)
|
||||
throw new Error(`Cannot invoke 'onUncover' on covered square at (${square.x}, ${square.y}).`);
|
||||
throw new Error(`Cannot invoke 'onUncover' on covered square at (${square.coords}).`);
|
||||
|
||||
if (!this.started) {
|
||||
this.history.addAction(new UnAction());
|
||||
|
@ -114,7 +114,7 @@ export class Field {
|
|||
this.coveredRemaining--;
|
||||
if (this.coveredRemaining === 0) {
|
||||
this.timer.stop();
|
||||
this.squareList.filter(it => it.isCovered && !it.hasFlag).forEach(it => it.flag());
|
||||
this.squareList.filter(it => it.isCovered && !it.hasFlag).forEach(it => this.flag(it.coords));
|
||||
this.won = true;
|
||||
}
|
||||
} else {
|
||||
|
@ -139,13 +139,13 @@ export class Field {
|
|||
copy = this.copy();
|
||||
|
||||
copy.history.startSequence();
|
||||
const copySquare = copy.getSquareOrElse(clickedSquare.x, clickedSquare.y)!;
|
||||
const copySquare = copy.getSquareOrElse(clickedSquare.coords)!;
|
||||
copySquare.isCovered = true;
|
||||
copySquare.uncover();
|
||||
copy.uncover(copySquare.coords);
|
||||
copy.history.commitSequence();
|
||||
|
||||
new Solver().solve(copy);
|
||||
} while(!copy.won);
|
||||
} while (!copy.won);
|
||||
}
|
||||
this.clearMines(clickedSquare);
|
||||
}
|
||||
|
@ -181,13 +181,22 @@ export class Field {
|
|||
/**
|
||||
* Returns the square at the given coordinates, or `orElse` if there is no square there.
|
||||
*
|
||||
* @param x the horizontal coordinate of the square to look up
|
||||
* @param y the vertical coordinate of the square to look up
|
||||
* @param coords the coordinates of the square to look up
|
||||
* @param orElse the value to return if there is no square at the given coordinates
|
||||
* @returns the square at the given coordinates, or `orElse` if there is no square there
|
||||
*/
|
||||
getSquareOrElse(x: number, y: number, orElse: any = undefined): Square | any {
|
||||
return this.squares[y]?.[x] ?? orElse;
|
||||
getSquareOrElse(coords: { x: number, y: number }, orElse: any = undefined): Square | any {
|
||||
return this.squares[coords.y]?.[coords.x] ?? orElse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if and only if this field contains a square at the given coordinates.
|
||||
*
|
||||
* @param coords the coordinates to check
|
||||
* @returns `true` if and only if this field contains a square at the given coordinates
|
||||
*/
|
||||
hasSquareAt(coords: { x: number, y: number }): boolean {
|
||||
return coords.x >= 0 && coords.x < this.width && coords.y >= 0 && coords.y < this.height;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -205,6 +214,63 @@ export class Field {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Chords the square at the given position, i.e. if the square is covered and the number of neighboring flags equals
|
||||
* the number in the square, then all unflagged neighbors are uncovered.
|
||||
*
|
||||
* @param coords the coordinates of the square to chord
|
||||
*/
|
||||
chord(coords: { x: number, y: number }): void {
|
||||
const square = this.squares[coords.y][coords.x];
|
||||
|
||||
if (square === undefined) throw new Error(`Cannot chord undefined square at (${coords}).`);
|
||||
if (square.isCovered || this.won || this.lost) return;
|
||||
if (square.getNeighborCount(it => it.hasFlag) !== square.getNeighborCount(it => it.hasMine)) return;
|
||||
|
||||
square.neighbors
|
||||
.filter(it => it.isCovered && !it.hasFlag)
|
||||
.forEach(it => this.uncover(it.coords));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.won || this.lost) return;
|
||||
|
||||
this.addAction(new FlagAction(square));
|
||||
square.hasFlag = !square.hasFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uncovers this square, revealing the contents beneath.
|
||||
*
|
||||
* @param coords the coordinates of the square to uncover
|
||||
*/
|
||||
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.won || this.lost) return;
|
||||
|
||||
const uncoverQueue: Square[] = [square];
|
||||
while (uncoverQueue.length > 0) {
|
||||
const next = uncoverQueue.pop()!;
|
||||
if (!next.isCovered || next.hasFlag) continue;
|
||||
|
||||
this.addAction(new UncoverAction(next));
|
||||
next.isCovered = false;
|
||||
this.onUncover(next); // Also moves mine on first click and swaps mines through field
|
||||
|
||||
if (!next.hasMine && next.getNeighborCount(it => it.hasMine || it.hasFlag) === 0)
|
||||
uncoverQueue.push(...next.neighbors.filter(it => it.isCovered));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Runs the given callback such that all calls to `#addAction` can be undone with a single invocation of `#undo`.
|
||||
*
|
||||
|
@ -310,20 +376,27 @@ export class Square {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* The coordinates of this square in the field.
|
||||
*/
|
||||
get coords(): { x: number, y: number } {
|
||||
return {x: this.x, y: this.y};
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Square`s that are adjacent to this square.
|
||||
*/
|
||||
get neighbors(): Square[] {
|
||||
if (this._neighbors === undefined) {
|
||||
this._neighbors = [
|
||||
this.field.getSquareOrElse(this.x - 1, this.y - 1),
|
||||
this.field.getSquareOrElse(this.x, this.y - 1),
|
||||
this.field.getSquareOrElse(this.x + 1, this.y - 1),
|
||||
this.field.getSquareOrElse(this.x - 1, this.y),
|
||||
this.field.getSquareOrElse(this.x + 1, this.y),
|
||||
this.field.getSquareOrElse(this.x - 1, this.y + 1),
|
||||
this.field.getSquareOrElse(this.x, this.y + 1),
|
||||
this.field.getSquareOrElse(this.x + 1, this.y + 1),
|
||||
this.field.getSquareOrElse({x: this.x - 1, y: this.y - 1}),
|
||||
this.field.getSquareOrElse({x: this.x, y: this.y - 1}),
|
||||
this.field.getSquareOrElse({x: this.x + 1, y: this.y - 1}),
|
||||
this.field.getSquareOrElse({x: this.x - 1, y: this.y}),
|
||||
this.field.getSquareOrElse({x: this.x + 1, y: this.y}),
|
||||
this.field.getSquareOrElse({x: this.x - 1, y: this.y + 1}),
|
||||
this.field.getSquareOrElse({x: this.x, y: this.y + 1}),
|
||||
this.field.getSquareOrElse({x: this.x + 1, y: this.y + 1}),
|
||||
].filter(it => it !== undefined);
|
||||
}
|
||||
|
||||
|
@ -339,52 +412,4 @@ export class Square {
|
|||
getNeighborCount(property: (neighbor: Square) => boolean): number {
|
||||
return this.neighbors.filter(property).length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Chords this square, i.e. if this square is covered and the number of neighboring flags equals the number in this
|
||||
* square, then all unflagged neighbors are uncovered.
|
||||
*
|
||||
* The `#startSequence` function on this square's field must be called before invoking this function.
|
||||
*/
|
||||
chord(): void {
|
||||
if (this.isCovered || this.field.won || this.field.lost) return;
|
||||
if (this.getNeighborCount(it => it.hasFlag) !== this.getNeighborCount(it => it.hasMine)) return;
|
||||
|
||||
this.neighbors
|
||||
.filter(it => it.isCovered && !it.hasFlag)
|
||||
.forEach(it => it.uncover());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or removes a flag at this square.
|
||||
*
|
||||
* The `#startSequence` function on this square's field must be called before invoking this function.
|
||||
*/
|
||||
flag(): void {
|
||||
if (!this.isCovered || this.field.won || this.field.lost) return;
|
||||
|
||||
this.field.addAction(new FlagAction(this));
|
||||
this.hasFlag = !this.hasFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uncovers this square, revealing the contents beneath.
|
||||
*/
|
||||
uncover(): void {
|
||||
if (this.field.won || this.field.lost) return;
|
||||
|
||||
const uncoverQueue: Square[] = [this];
|
||||
while (uncoverQueue.length > 0) {
|
||||
const next = uncoverQueue.pop()!;
|
||||
if (!next.isCovered || next.hasFlag) continue;
|
||||
|
||||
this.field.addAction(new UncoverAction(next));
|
||||
next.isCovered = false;
|
||||
this.field.onUncover(next); // Also moves mine on first click and swaps mines through field
|
||||
|
||||
if (!next.hasMine && next.getNeighborCount(it => it.hasMine || it.hasFlag) === 0)
|
||||
uncoverQueue.push(...next.neighbors.filter(it => it.isCovered));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -206,7 +206,12 @@ export class Game {
|
|||
// Canvas
|
||||
this.canvas.addEventListener(
|
||||
"mousemove",
|
||||
event => this.display.mouseSquare = this.display.posToSquare({x: event.clientX, y: event.clientY})
|
||||
event => {
|
||||
this.display.mouseSquare = this.field?.getSquareOrElse(
|
||||
this.display.posToSquare({x: event.clientX, y: event.clientY}),
|
||||
null
|
||||
);
|
||||
}
|
||||
);
|
||||
this.canvas.addEventListener(
|
||||
"mouseleave",
|
||||
|
@ -226,13 +231,16 @@ export class Game {
|
|||
if (this.field === null) return;
|
||||
|
||||
this.field.runUndoably(() => {
|
||||
const square = this.display.posToSquare({x: event.clientX, y: event.clientY});
|
||||
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
|
||||
if (this.field === null || !this.field.hasSquareAt(coords)) return;
|
||||
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
this.leftDown = true;
|
||||
break;
|
||||
case 2:
|
||||
if (!this.leftDown && square !== null) square.flag();
|
||||
if (!this.leftDown)
|
||||
this.field.flag(coords);
|
||||
|
||||
this.rightDown = true;
|
||||
break;
|
||||
|
@ -249,23 +257,25 @@ export class Game {
|
|||
if (this.field === null) return;
|
||||
|
||||
this.field.runUndoably(() => {
|
||||
const square = this.display.posToSquare({x: event.clientX, y: event.clientY});
|
||||
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
|
||||
if (this.field === null || !this.field.hasSquareAt(coords)) return;
|
||||
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
if (square !== null && this.leftDown && this.rightDown)
|
||||
square.chord();
|
||||
else if (square !== null && !this.holdsAfterChord && this.leftDown)
|
||||
square.uncover();
|
||||
if (this.leftDown && this.rightDown)
|
||||
this.field.chord(coords);
|
||||
else if (!this.holdsAfterChord && this.leftDown)
|
||||
this.field.uncover(coords);
|
||||
|
||||
this.leftDown = false;
|
||||
this.holdsAfterChord = this.rightDown;
|
||||
break;
|
||||
case 1:
|
||||
if (square !== null) square.chord();
|
||||
this.field.chord(coords);
|
||||
break;
|
||||
case 2:
|
||||
if (square !== null && this.leftDown && this.rightDown)
|
||||
square.chord();
|
||||
if (this.leftDown && this.rightDown)
|
||||
this.field.chord(coords);
|
||||
|
||||
this.rightDown = false;
|
||||
this.holdsAfterChord = this.leftDown;
|
||||
|
|
|
@ -16,13 +16,13 @@ export class Solver {
|
|||
solve(field: Field) {
|
||||
if (!field.started) {
|
||||
field.runUndoably(() => {
|
||||
field.squareList.filter(it => it.hasFlag).forEach(it => it.flag());
|
||||
field.getSquareOrElse(Math.floor(field.width / 2), Math.floor(field.height / 2)).uncover();
|
||||
field.squareList.filter(it => it.hasFlag).forEach(it => field.flag(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 => it.flag());
|
||||
field.squareList.filter(it => it.hasFlag).forEach(it => field.flag(it.coords));
|
||||
|
||||
let flagCount = -1;
|
||||
let coveredCount = -1;
|
||||
|
@ -72,8 +72,8 @@ export class Solver {
|
|||
|
||||
system.forEach((it, i) => {
|
||||
const square = neighs[i];
|
||||
if (it === 0) square.uncover();
|
||||
else if (it === 1) square.flag();
|
||||
if (it === 0) field.uncover(square.coords);
|
||||
else if (it === 1) field.flag(square.coords);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue