Add draft implementation of high scores

This commit is contained in:
Florine W. Dekker 2020-09-02 17:58:40 +02:00
parent ddb27c8061
commit 53dd327786
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
7 changed files with 155 additions and 6 deletions

View File

@ -1,6 +1,6 @@
{
"name": "minesweeper",
"version": "0.81.11",
"version": "0.82.0",
"description": "Just Minesweeper!",
"author": "Felix W. Dekker",
"browser": "dist/bundle.js",

View File

@ -38,6 +38,11 @@
<form id="statisticsOpenForm">
<button><i class="fa fa-tachometer"></i>&emsp;Statistics</button>
</form>
<!-- High scores -->
<form id="highScoresOpenForm">
<button><i class="fa fa-trophy"></i>&emsp;High scores</button>
</form>
</div>
</div>
<div class="row controls">
@ -172,6 +177,18 @@
</form>
</div>
</div>
<!-- High scores overlay -->
<div class="overlayWrapper" id="highScoresOverlay">
<div class="overlay">
<div id="highScoresDiv"></div>
<form id="highScoresResetForm">
<button>Reset</button>
</form>
<form id="highScoresCloseForm">
<button class="cancel">Close</button>
</form>
</div>
</div>
</main>

View File

@ -30,7 +30,7 @@ export class Difficulty {
/**
* The default difficulty levels.
*/
export const difficulties = [
export const difficulties: Difficulty[] = [
new Difficulty("Beginner (9x9, 10 mines)", 9, 9, 10, true),
new Difficulty("Intermediate (16x16, 40 mines)", 16, 16, 40, true),
new Difficulty("Expert (30x16, 99 mines)", 30, 16, 99, true),
@ -46,3 +46,22 @@ export const defaultDifficulty = difficulties[0];
* The custom difficulty level, which is not a real difficulty level.
*/
export const customDifficulty = difficulties[difficulties.length - 1];
/**
* Returns the difficulty in `difficulties` that matches the given parameters.
*
* @param width the width to match a difficulty with
* @param height the height to match a difficulty with
* @param mineCount the number of mines to match a difficulty with
*/
export const findDifficulty = function(width: number, height: number, mineCount: number): Difficulty {
for (let i = 0; i < difficulties.length; i++) {
const difficulty = difficulties[i];
if (difficulty === customDifficulty) continue;
if (width === difficulty.width && height === difficulty.height && mineCount === difficulty.mineCount)
return difficulty;
}
return customDifficulty;
}

View File

@ -2,6 +2,8 @@
import alea from "alea";
import {Action, ActionHistory} from "./Action";
import {chunkifyArray, shuffleArrayInPlace} from "./Common";
import {findDifficulty} from "./Difficulty";
import {HighScores} from "./HighScores";
import {Solver} from "./Solver";
import {Statistics} from "./Statistics";
import {MemoryStorage} from "./Storage";
@ -13,6 +15,7 @@ import {Timer} from "./Timer";
*/
export class Field {
private readonly statistics: Statistics;
private readonly highScores: HighScores;
private readonly history = new ActionHistory();
private readonly rng: any;
private timer = new Timer();
@ -69,11 +72,12 @@ export class Field {
* @param statistics the statistics tracker, or `null` if statistics should not be tracked
*/
constructor(width: number, height: number, mineCount: number, solvable: boolean, seed: number,
statistics: Statistics) {
statistics: Statistics, highScores: HighScores) {
if (mineCount > Field.maxMines(width, height))
throw new Error(`Mine count must be at most ${Field.maxMines(width, height)}, but was ${mineCount}.`);
this.statistics = statistics;
this.highScores = highScores;
this.width = width;
this.height = height;
@ -100,7 +104,8 @@ export class Field {
this.width, this.height,
this.mineCount,
false, 0,
new Statistics(new MemoryStorage())
new Statistics(new MemoryStorage()),
new HighScores(new MemoryStorage())
);
copy.squareList.length = 0;
@ -290,6 +295,10 @@ export class Field {
if (!this.isAutoSolving) {
this.statistics.gamesWon++;
if (this.deathCount === 0) this.statistics.gamesWonWithoutLosing++;
const difficulty = findDifficulty(this.width, this.height, this.mineCount);
const score = {time: this.elapsedTime, deaths: this.deathCount};
this.highScores.addScore(difficulty, score);
}
}
}

View File

@ -6,6 +6,7 @@ import {blurActiveElement, stringToHash} from "./Common";
import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty";
import {Display} from "./Display";
import {Field} from "./Field";
import {HighScores} from "./HighScores";
import {Preferences} from "./Preferences";
import {Solver} from "./Solver";
import {Statistics} from "./Statistics";
@ -17,6 +18,7 @@ import {Overlay} from "./UI";
*/
export class Game {
private readonly statistics = new Statistics();
private readonly highScores = new HighScores();
private statisticsTimer: number | undefined;
private readonly canvas: HTMLCanvasElement;
@ -43,6 +45,10 @@ export class Game {
private readonly statisticsDiv: HTMLDivElement;
private readonly statisticsResetForm: HTMLFormElement;
private readonly statisticsOpenForm: HTMLFormElement;
private readonly highScoresOverlay: Overlay;
private readonly highScoresDiv: HTMLDivElement;
private readonly highScoresResetForm: HTMLFormElement;
private readonly highScoresOpenForm: HTMLFormElement;
private readonly rng: any;
private seed: string;
@ -293,6 +299,38 @@ export class Game {
);
this.updateStatistics();
// High scores
this.highScoresDiv = $("#highScoresDiv");
this.highScoresOpenForm = $("#highScoresOpenForm");
this.highScoresOpenForm.addEventListener(
"submit",
event => {
event.preventDefault();
this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport();
this.highScoresOverlay.show();
blurActiveElement();
}
);
this.highScoresOverlay = new Overlay(
$("#highScoresOverlay"),
null,
$("#highScoresCloseForm")
);
this.highScoresResetForm = $("#highScoresResetForm");
this.highScoresResetForm.addEventListener(
"submit",
event => {
event.preventDefault();
if (!window.confirm("Are you sure you want to reset all high scores? This cannot be undone."))
return;
this.highScores.clear();
this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport();
}
);
// Canvas
this.canvas.addEventListener(
"mousemove",
@ -434,7 +472,8 @@ export class Game {
this.field = new Field(
width, height, mineCount, solvable,
isNaN(+this.seed) ? stringToHash(this.seed) : +this.seed,
this.statistics
this.statistics,
this.highScores
);
this.display.setField(this.field);

64
src/main/js/HighScores.ts Normal file
View File

@ -0,0 +1,64 @@
import {difficulties, Difficulty} from "./Difficulty";
import {LocalStorage, Storage} from "./Storage";
/**
* A score obtained by clearing a field.
*/
export type Score = {time: number, deaths: number};
/**
* The highest scores.
*/
export class HighScores {
private static readonly scoresToStore: number = 10;
private readonly storage: Storage;
constructor(storage: Storage = new LocalStorage("/tools/minesweeper//high-scores")) {
this.storage = storage;
}
clear(): void {
this.storage.clear();
}
addScore(difficulty: Difficulty, score: Score): void {
const scores = this.storage.getArray(difficulty.name, []);
scores.push(score);
scores.sort((a, b) => a.time - b.time);
this.storage.setArray(difficulty.name, scores.slice(0, HighScores.scoresToStore));
}
getScores(difficulty: Difficulty): Score[] {
return this.storage.getArray(difficulty.name, []);
}
generateHtmlReport(): string {
let report = "";
for (let i = 0; i < difficulties.length; i++) {
const difficulty = difficulties[i];
const highScores = this.getScores(difficulty);
report += "" +
`<h3>${difficulty.name}</h3>
<table>
<tr>
<th>Time (seconds)</th>
<th>Deaths</th>
</tr>`;
for (let j = 0; j < highScores.length; j++) {
const score = highScores[j];
report += "" +
`<tr>
<td>${score.time / 1000}</td>
<td>${score.deaths}</td>
</tr>`;
}
report += "</table>";
}
return report;
}
}

View File

@ -110,7 +110,8 @@ export class LocalStorage implements Storage {
}
getArray(name: string, def: any[] = []): any[] {
return JSON.parse(this.read()[name]) ?? def;
const array = this.read()[name];
return array === undefined ? def : JSON.parse(array);
}
setArray(name: string, value: any[]): void {