From 5120494309cd86827ffb1d49a9c31537551097b0 Mon Sep 17 00:00:00 2001 From: Michael Doyle Date: Thu, 12 Dec 2024 19:16:16 -0500 Subject: [PATCH] Add game stats on win --- src/minesweeper/Minesweeper.css | 12 +++++ src/minesweeper/Minesweeper.tsx | 70 +++++++++++++++++++++++++++-- src/minesweeper/minesweeper-game.ts | 61 +++++++++++++++++++------ 3 files changed, 125 insertions(+), 18 deletions(-) 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; } } }