Skip to content

Commit

Permalink
Add game stats on win
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeldoylecs committed Dec 13, 2024
1 parent 558f469 commit 5120494
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 18 deletions.
12 changes: 12 additions & 0 deletions src/minesweeper/Minesweeper.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
70 changes: 66 additions & 4 deletions src/minesweeper/Minesweeper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -139,9 +189,21 @@ function Minesweeper() {
padding: '2px',
}

const GameStats = observer((observable: { stats: IGameStats }) => {
const { wins, consecutiveWins, previousWinSeed } = observable.stats;
return (
<div className="game-stats">
<div><strong>You Win!</strong></div>
<div>Wins: {wins}</div>
<div>Streak: {consecutiveWins}</div>
</div>
);
});

return (
<div className="minesweeper">
<ResetClock />
{game.isOver && <GameStats stats={stats} />}
<div className='game-board' style={gameBoardStyle} onContextMenu={disableContextMenu}>
<GameBoardComponent game={game} />
</div>
Expand Down
61 changes: 47 additions & 14 deletions src/minesweeper/minesweeper-game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
}

Expand All @@ -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;
Expand All @@ -119,22 +119,36 @@ export class MinesweeperGame {

// If a bomb was clicked, end the game
if (tile.isBomb) {
this.isOver = true;
this._isOver = true;
return;
}

// If there are no adjacnet bombs, propagate out the revealed tiles.
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[][] {
Expand All @@ -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;
Expand All @@ -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;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -314,7 +347,7 @@ export class MinesweeperGame {
bombCount += this.clickWithoutEnding(board, x1, y1);
}
if (bombCount > 0) {
this.isOver = true;
this._isOver = true;
}
}
}

0 comments on commit 5120494

Please sign in to comment.