Add draft implementation of high scores
This commit is contained in:
parent
ddb27c8061
commit
53dd327786
|
@ -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",
|
||||
|
|
|
@ -38,6 +38,11 @@
|
|||
<form id="statisticsOpenForm">
|
||||
<button><i class="fa fa-tachometer"></i> Statistics</button>
|
||||
</form>
|
||||
|
||||
<!-- High scores -->
|
||||
<form id="highScoresOpenForm">
|
||||
<button><i class="fa fa-trophy"></i> 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>
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue