diff --git a/src/minesweeper/Minesweeper.css b/src/minesweeper/Minesweeper.css
index 73ff660..e0a010c 100644
--- a/src/minesweeper/Minesweeper.css
+++ b/src/minesweeper/Minesweeper.css
@@ -49,3 +49,15 @@
.clock-numbers {
font-family: monospace;
}
+
+.game-stats {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ align-content: center;
+ border: 2px solid black;
+ border-radius: 5px;
+ padding: 5px;
+ margin: 0 0 10px 0;
+}
diff --git a/src/minesweeper/Minesweeper.tsx b/src/minesweeper/Minesweeper.tsx
index 490fac9..40d0e13 100644
--- a/src/minesweeper/Minesweeper.tsx
+++ b/src/minesweeper/Minesweeper.tsx
@@ -5,6 +5,12 @@ import './Minesweeper.css';
import { SyntheticEvent, useEffect, useState } from 'react';
import { action, autorun } from 'mobx';
+interface IGameStats {
+ wins: number,
+ consecutiveWins: number,
+ previousWinSeed: string,
+}
+
function SerializeGameState(gamestate: MinesweeperGameState): string {
return JSON.stringify(gamestate);
}
@@ -13,13 +19,20 @@ function DeserializeGameState(gamestate: string): MinesweeperGameState {
return JSON.parse(gamestate);
}
-function getTodaysSeed(): string {
+function generateSeed(year: number, month: number, day: number): string {
const MAGIC_NUMBER = 329246;
- const now = new Date();
- const seed = (now.getUTCDay() * now.getUTCMonth() * now.getUTCFullYear() * MAGIC_NUMBER).toString();
+ const seed = (year * month * day * MAGIC_NUMBER).toString();
return seed
}
+function generateSeedFromDate(date: Date): string {
+ return generateSeed(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
+}
+
+function getTodaysSeed(): string {
+ return generateSeedFromDate(new Date());
+}
+
function Minesweeper() {
const width = 20;
const height = 20;
@@ -40,6 +53,18 @@ function Minesweeper() {
return new MinesweeperGame(width, height, bombs, todaySeed);
});
+ const [stats, _] = useState(() => {
+ let stats: IGameStats = {
+ wins: 0,
+ consecutiveWins: 0,
+ previousWinSeed: getTodaysSeed(),
+ };
+ let storedStatsString = localStorage.getItem("stats");
+ if (storedStatsString != null) {
+ stats = JSON.parse(storedStatsString);
+ }
+ return stats;
+ });
const rowCount = game.board.length;
const columnCount = game.board[0].length;
@@ -75,7 +100,32 @@ function Minesweeper() {
console.log("Saved gamestate");
});
return () => disposer();
- }, [])
+ }, []);
+
+ // Update stats on game win
+ useEffect(() => {
+ const disposer = autorun(() => {
+ if (game.isOver && game.isWin()) {
+ // Don't increment stats multiple times for same day
+ if (stats.wins > 0 && stats.previousWinSeed == game.seed) {
+ return;
+ }
+
+ const now = new Date();
+ const millisecondsInADay = 1000 * 60 * 60 * 24;
+ if (stats.previousWinSeed == generateSeedFromDate(new Date(now.getTime() - millisecondsInADay))) {
+ stats.consecutiveWins++;
+ } else {
+ stats.consecutiveWins = 1;
+ }
+ stats.previousWinSeed = game.seed;
+ stats.wins++;
+ localStorage.setItem("stats", JSON.stringify(stats));
+ console.log("Saved stats");
+ }
+ });
+ return () => disposer();
+ }, []);
const boardTileStyle = {
cursor: 'pointer',
@@ -139,9 +189,21 @@ function Minesweeper() {
padding: '2px',
}
+ const GameStats = observer((observable: { stats: IGameStats }) => {
+ const { wins, consecutiveWins, previousWinSeed } = observable.stats;
+ return (
+
+
You Win!
+
Wins: {wins}
+
Streak: {consecutiveWins}
+
+ );
+ });
+
return (
+ {game.isOver &&
}
diff --git a/src/minesweeper/minesweeper-game.ts b/src/minesweeper/minesweeper-game.ts
index 0a20dd8..657a217 100644
--- a/src/minesweeper/minesweeper-game.ts
+++ b/src/minesweeper/minesweeper-game.ts
@@ -42,9 +42,9 @@ export class MinesweeperGame {
readonly mineCount: number;
readonly boardRows: number;
readonly boardColumns: number;
- readonly _seed: string = 'TESTSEED';
+ readonly seed: string = 'TESTSEED';
private _board: BoardTile[][];
- private isOver: boolean;
+ private _isOver: boolean;
constructor(
boardWidth: number = 9,
@@ -57,9 +57,9 @@ export class MinesweeperGame {
this.boardRows = this._board.length;
this.boardColumns = this._board[0].length; // Assumes board > 0 rows
this.mineCount = mineCount;
- this._seed = seed;
+ this.seed = seed;
this._board = this.generateGameBoard();
- this.isOver = isOver;
+ this._isOver = isOver;
makeAutoObservable(this);
}
@@ -85,19 +85,19 @@ export class MinesweeperGame {
BoardColumns: this.boardColumns,
BoardData: this._board.slice(),
MineCount: this.mineCount,
- Seed: this._seed,
- isOver: this.isOver,
+ Seed: this.seed,
+ isOver: this._isOver,
};
}
public reset() {
this._board = this.generateGameBoard();
- this.isOver = false;
+ this._isOver = false;
}
public click(x: number, y: number) {
// Prevent click if game is already over
- if (this.isOver) return;
+ if (this._isOver) return;
// Prevent click outside of game bounds
if (x < 0 || x >= this.boardColumns || y < 0 || y > this.boardRows) return;
@@ -119,7 +119,7 @@ export class MinesweeperGame {
// If a bomb was clicked, end the game
if (tile.isBomb) {
- this.isOver = true;
+ this._isOver = true;
return;
}
@@ -127,14 +127,28 @@ export class MinesweeperGame {
if (tile.adjacentBombCount === 0) {
this._board = this.propagateEmptyTiles(this._board, x, y);
}
+
+ console.log("Checking win...");
+ if (this.isWin()) {
+ console.log("...game is won!");
+ this._isOver = true;
+ }
}
public flag(x: number, y: number) {
- if (this.isOver) return;
+ if (this._isOver) return;
if (x < 0 || x >= this.boardColumns || y < 0 || y > this.boardRows) return;
const tile = this._board[x][y];
if (tile.isVisible) return;
this._board[x][y].isFlagged = !this._board[x][y].isFlagged;
+
+ if (this._board[x][y].isFlagged) {
+ console.log("Checking win...");
+ if (this.isWin()) {
+ console.log("...game is won!");
+ this._isOver = true;
+ }
+ }
}
public get board(): BoardTile[][] {
@@ -146,9 +160,28 @@ export class MinesweeperGame {
return;
}
+ public get isOver(): boolean {
+ return this._isOver;
+ }
+
+ public isWin(): boolean {
+ for (let j = 0; j < this.boardRows; ++j) {
+ for (let k = 0; k < this.boardColumns; ++k) {
+ const tile = this._board[j][k];
+ if (!tile.isBomb && !tile.isVisible) {
+ return false;
+ }
+ if (tile.isBomb && !tile.isFlagged) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
private clickWithoutEnding(board: BoardTile[][],x: number, y: number): number {
// Prevent click if game is already over
- if (this.isOver) return 0;
+ if (this._isOver) return 0;
// Prevent click outside of game bounds
if (x < 0 || x >= this.boardColumns || y < 0 || y > this.boardRows) return 0;
@@ -165,7 +198,7 @@ export class MinesweeperGame {
// If a bomb was clicked, end the game
if (tile.isBomb) {
- this.isOver = true;
+ this._isOver = true;
return 1;
}
@@ -189,7 +222,7 @@ export class MinesweeperGame {
}
private generateGameBoard(): BoardTile[][] {
- let rng = new Rand(this._seed);
+ let rng = new Rand(this.seed);
const minePositions = this.chooseSeededMinePositions(rng);
let board = this.createEmptyBoard(this.boardColumns, this.boardRows);
for (const bomb of minePositions) {
@@ -314,7 +347,7 @@ export class MinesweeperGame {
bombCount += this.clickWithoutEnding(board, x1, y1);
}
if (bombCount > 0) {
- this.isOver = true;
+ this._isOver = true;
}
}
}