Remove duplication by separating types in Storage
This commit is contained in:
parent
11bc3f9be7
commit
ddb27c8061
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "minesweeper",
|
||||
"version": "0.81.10",
|
||||
"version": "0.81.11",
|
||||
"description": "Just Minesweeper!",
|
||||
"author": "Felix W. Dekker",
|
||||
"browser": "dist/bundle.js",
|
||||
|
|
|
@ -2,24 +2,6 @@
|
|||
import alea from "alea";
|
||||
|
||||
|
||||
/**
|
||||
* Shuffles the given array in-place.
|
||||
*
|
||||
* @param array the array to shuffle
|
||||
* @param seed the seed for the random number generator
|
||||
* @returns the array that was given to this function to shuffle
|
||||
*/
|
||||
export function shuffleArrayInPlace(array: any[], seed: number): any[] {
|
||||
const rng = alea("" + seed);
|
||||
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = rng.uint32() % (i + 1);
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blurs the currently active element, if possible.
|
||||
*/
|
||||
|
@ -44,6 +26,28 @@ export function chunkifyArray(array: any[], chunkSize: number): any[] {
|
|||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given time into a nice representation of `hh:mm:ss`.
|
||||
*
|
||||
* @param seconds the number of seconds; the time to be formatted
|
||||
* @param minutes whether to include minutes
|
||||
* @param hours whether to include hours; requires that `minutes` is true
|
||||
* @return the formatted time
|
||||
*/
|
||||
export function formatTime(seconds: number, minutes: boolean, hours: boolean): string {
|
||||
if (!minutes && hours) throw new Error("Cannot format time with hours but without minutes.");
|
||||
if (!minutes && !hours) return "" + seconds;
|
||||
|
||||
const secondsString = ("" + (seconds % 60)).padStart(2, '0');
|
||||
const minutesString = hours
|
||||
? ("" + Math.floor((seconds % 3600) / 60)).padStart(2, '0')
|
||||
: ("" + Math.floor(seconds / 60));
|
||||
if (!hours) return `${minutesString}:${secondsString}`;
|
||||
|
||||
const hoursString = Math.floor(seconds / 3600);
|
||||
return `${hoursString}:${minutesString}:${secondsString}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array of `size` consecutive integers starting at `startAt`.
|
||||
*
|
||||
|
@ -57,6 +61,40 @@ export function range(length: number, beginAt: number = 0): number[] {
|
|||
return [...Array(length).keys()].map(i => i + beginAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles the given array in-place.
|
||||
*
|
||||
* @param array the array to shuffle
|
||||
* @param seed the seed for the random number generator
|
||||
* @returns the array that was given to this function to shuffle
|
||||
*/
|
||||
export function shuffleArrayInPlace(array: any[], seed: number): any[] {
|
||||
const rng = alea("" + seed);
|
||||
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = rng.uint32() % (i + 1);
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the given string.
|
||||
*
|
||||
* @param string the string to hash
|
||||
* @returns the hash of the string
|
||||
*/
|
||||
export function stringToHash(string: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
const chr = string.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for FontAwesome to have loaded and then invokes the callback.
|
||||
*
|
||||
|
@ -112,41 +150,3 @@ export function waitForForkAwesome(onSuccess: () => void, onFailure: () => void,
|
|||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the given string.
|
||||
*
|
||||
* @param string the string to hash
|
||||
* @returns the hash of the string
|
||||
*/
|
||||
export function stringToHash(string: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
const chr = string.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given time into a nice representation of `hh:mm:ss`.
|
||||
*
|
||||
* @param seconds the number of seconds; the time to be formatted
|
||||
* @param minutes whether to include minutes
|
||||
* @param hours whether to include hours; requires that `minutes` is true
|
||||
* @return the formatted time
|
||||
*/
|
||||
export function formatTime(seconds: number, minutes: boolean, hours: boolean): string {
|
||||
if (!minutes && hours) throw new Error("Cannot format time with hours but without minutes.");
|
||||
if (!minutes && !hours) return "" + seconds;
|
||||
|
||||
const secondsString = ("" + (seconds % 60)).padStart(2, '0');
|
||||
const minutesString = hours
|
||||
? ("" + Math.floor((seconds % 3600) / 60)).padStart(2, '0')
|
||||
: ("" + Math.floor(seconds / 60));
|
||||
if (!hours) return `${minutesString}:${secondsString}`;
|
||||
|
||||
const hoursString = Math.floor(seconds / 3600);
|
||||
return `${hoursString}:${minutesString}:${secondsString}`;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import confetti from "canvas-confetti";
|
||||
import {formatTime, range} from "./Common";
|
||||
import {Field, Square} from "./Field";
|
||||
import {Preferences} from "./Game";
|
||||
import {Preferences} from "./Preferences";
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,8 @@ import alea from "alea";
|
|||
import {Action, ActionHistory} from "./Action";
|
||||
import {chunkifyArray, shuffleArrayInPlace} from "./Common";
|
||||
import {Solver} from "./Solver";
|
||||
import {MemoryStatistics, Statistics} from "./Statistics";
|
||||
import {Statistics} from "./Statistics";
|
||||
import {MemoryStorage} from "./Storage";
|
||||
import {Timer} from "./Timer";
|
||||
|
||||
|
||||
|
@ -95,7 +96,12 @@ export class Field {
|
|||
* @returns a deep copy of this field
|
||||
*/
|
||||
copy(): Field {
|
||||
const copy = new Field(this.width, this.height, this.mineCount, false, 0, new MemoryStatistics());
|
||||
const copy = new Field(
|
||||
this.width, this.height,
|
||||
this.mineCount,
|
||||
false, 0,
|
||||
new Statistics(new MemoryStorage())
|
||||
);
|
||||
|
||||
copy.squareList.length = 0;
|
||||
copy.squareList.push(...this.squareList.map(it => it.copy(copy)));
|
||||
|
|
|
@ -4,11 +4,11 @@ import {$} from "@fwdekker/template";
|
|||
import alea from "alea";
|
||||
import {blurActiveElement, stringToHash} from "./Common";
|
||||
import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty";
|
||||
import {BasicIconFont, Display, ForkAwesomeFont, IconFont} from "./Display";
|
||||
import {Display} from "./Display";
|
||||
import {Field} from "./Field";
|
||||
import {Preferences} from "./Preferences";
|
||||
import {Solver} from "./Solver";
|
||||
import {LocalStatistics} from "./Statistics";
|
||||
import {Storage} from "./Storage";
|
||||
import {Statistics} from "./Statistics";
|
||||
import {Overlay} from "./UI";
|
||||
|
||||
|
||||
|
@ -16,7 +16,7 @@ import {Overlay} from "./UI";
|
|||
* Controls the interaction with a game of Minesweeper.
|
||||
*/
|
||||
export class Game {
|
||||
private readonly statistics = new LocalStatistics();
|
||||
private readonly statistics = new Statistics();
|
||||
private statisticsTimer: number | undefined;
|
||||
|
||||
private readonly canvas: HTMLCanvasElement;
|
||||
|
@ -452,34 +452,3 @@ export class Game {
|
|||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The player's preferences.
|
||||
*
|
||||
* Contains a mixture of persistent and transient preferences.
|
||||
*/
|
||||
export class Preferences {
|
||||
private readonly storage = new Storage("/tools/minesweeper//preferences");
|
||||
|
||||
|
||||
/**
|
||||
* The font to be used when drawing the display.
|
||||
*/
|
||||
font: IconFont = new BasicIconFont();
|
||||
|
||||
get marksEnabled(): boolean {
|
||||
return this.storage.getBoolean("marksEnabled", true);
|
||||
}
|
||||
|
||||
set marksEnabled(value: boolean) {
|
||||
this.storage.setBoolean("marksEnabled", value);
|
||||
}
|
||||
|
||||
get showTooManyFlagsHints(): boolean {
|
||||
return this.storage.getBoolean("showTooManyFlagsHints", true);
|
||||
}
|
||||
|
||||
set showTooManyFlagsHints(value: boolean) {
|
||||
this.storage.setBoolean("showTooManyFlagsHints", value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ import "../css/main.css";
|
|||
import {$, doAfterLoad, footer, header, nav} from "@fwdekker/template";
|
||||
import {waitForForkAwesome} from "./Common";
|
||||
import {BasicIconFont, ForkAwesomeFont} from "./Display";
|
||||
import {Game, Preferences} from "./Game";
|
||||
import {Game} from "./Game";
|
||||
import {Preferences} from "./Preferences";
|
||||
|
||||
|
||||
doAfterLoad(() => {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import {BasicIconFont, IconFont} from "./Display";
|
||||
import {LocalStorage, Storage} from "./Storage";
|
||||
|
||||
|
||||
/**
|
||||
* The player's preferences.
|
||||
*
|
||||
* Contains a mixture of persistent and transient preferences.
|
||||
*/
|
||||
export class Preferences {
|
||||
private readonly storage: Storage;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new preferences container.
|
||||
*
|
||||
* @param storage the underlying object to store preferences in
|
||||
*/
|
||||
constructor(storage: Storage = new LocalStorage("/tools/minesweeper//preferences")) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The font to be used when drawing the display.
|
||||
*/
|
||||
font: IconFont = new BasicIconFont();
|
||||
|
||||
get marksEnabled(): boolean {
|
||||
return this.storage.getBoolean("marksEnabled", true);
|
||||
}
|
||||
|
||||
set marksEnabled(value: boolean) {
|
||||
this.storage.setBoolean("marksEnabled", value);
|
||||
}
|
||||
|
||||
get showTooManyFlagsHints(): boolean {
|
||||
return this.storage.getBoolean("showTooManyFlagsHints", true);
|
||||
}
|
||||
|
||||
set showTooManyFlagsHints(value: boolean) {
|
||||
this.storage.setBoolean("showTooManyFlagsHints", value);
|
||||
}
|
||||
}
|
|
@ -1,45 +1,27 @@
|
|||
import {formatTime} from "./Common";
|
||||
import {Storage} from "./Storage";
|
||||
import {LocalStorage, Storage} from "./Storage";
|
||||
|
||||
|
||||
/**
|
||||
* A storage for game statistics.
|
||||
*/
|
||||
export interface Statistics {
|
||||
actionsUndone: number;
|
||||
actionsRedone: number;
|
||||
lossesUndone: number;
|
||||
lossesRedone: number;
|
||||
timeSpent: number;
|
||||
|
||||
gamesStarted: number;
|
||||
gamesWon: number;
|
||||
gamesWonWithoutLosing: number;
|
||||
|
||||
squaresChorded: number;
|
||||
squaresChordedLeadingToLoss: number;
|
||||
squaresFlagged: number;
|
||||
squaresMarked: number;
|
||||
squaresUncovered: number;
|
||||
minesUncovered: number;
|
||||
|
||||
hintsRequested: number;
|
||||
solverUsages: number;
|
||||
|
||||
|
||||
/**
|
||||
* Resets the statistics.
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores game statistics in the browser's localstorage.
|
||||
*/
|
||||
export class LocalStatistics implements Statistics {
|
||||
private readonly storage = new Storage("/tools/minesweeper//statistics");
|
||||
export class Statistics implements Statistics {
|
||||
private readonly storage: Storage;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new statistics container.
|
||||
*
|
||||
* @param storage the underlying object to store statistics in
|
||||
*/
|
||||
constructor(storage: Storage = new LocalStorage("/tools/minesweeper//statistics")) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clears all statistics.
|
||||
*/
|
||||
clear(): void {
|
||||
this.storage.clear();
|
||||
}
|
||||
|
@ -261,45 +243,3 @@ export class LocalStatistics implements Statistics {
|
|||
</table>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores game statistics in memory, right here in the fields of this object.
|
||||
*/
|
||||
export class MemoryStatistics implements Statistics {
|
||||
actionsUndone: number = 0;
|
||||
actionsRedone: number = 0;
|
||||
lossesUndone: number = 0;
|
||||
lossesRedone: number = 0;
|
||||
timeSpent: number = 0;
|
||||
gamesStarted: number = 0;
|
||||
gamesWon: number = 0;
|
||||
gamesWonWithoutLosing: number = 0;
|
||||
squaresChorded: number = 0;
|
||||
squaresChordedLeadingToLoss: number = 0;
|
||||
squaresFlagged: number = 0;
|
||||
squaresMarked: number = 0;
|
||||
squaresUncovered: number = 0;
|
||||
minesUncovered: number = 0;
|
||||
hintsRequested: number = 0;
|
||||
solverUsages: number = 0;
|
||||
|
||||
|
||||
clear() {
|
||||
this.actionsUndone = 0;
|
||||
this.actionsRedone = 0;
|
||||
this.lossesUndone = 0;
|
||||
this.lossesRedone = 0;
|
||||
this.timeSpent = 0;
|
||||
this.gamesStarted = 0;
|
||||
this.gamesWon = 0;
|
||||
this.gamesWonWithoutLosing = 0;
|
||||
this.squaresChorded = 0;
|
||||
this.squaresChordedLeadingToLoss = 0;
|
||||
this.squaresFlagged = 0;
|
||||
this.squaresMarked = 0;
|
||||
this.squaresUncovered = 0;
|
||||
this.minesUncovered = 0;
|
||||
this.hintsRequested = 0;
|
||||
this.solverUsages = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,72 @@
|
|||
/**
|
||||
* Persistent storage, in particular `localStorage`.
|
||||
* Stores key-value pairs.
|
||||
*/
|
||||
export class Storage {
|
||||
export interface Storage {
|
||||
/**
|
||||
* Removes the data from storage.
|
||||
*/
|
||||
clear(): void
|
||||
|
||||
/**
|
||||
* Retrieves an array from storage.
|
||||
*
|
||||
* @param name the name of the array to retrieve
|
||||
* @param def the value to return if no array is stored with the given name
|
||||
*/
|
||||
getArray(name: string, def: any[]): any[]
|
||||
|
||||
/**
|
||||
* Stores an array.
|
||||
*
|
||||
* @param name the name of the array to store
|
||||
* @param value the array to store under the given name
|
||||
* @protected
|
||||
*/
|
||||
setArray(name: string, value: any[]): void
|
||||
|
||||
/**
|
||||
* Retrieves a boolean from storage.
|
||||
*
|
||||
* @param name the name of the boolean to retrieve
|
||||
* @param def the value to return if no boolean is stored with the given name
|
||||
* @protected
|
||||
*/
|
||||
getBoolean(name: string, def: boolean): boolean
|
||||
|
||||
/**
|
||||
* Stores a boolean.
|
||||
*
|
||||
* @param name the name of the boolean to store
|
||||
* @param value the boolean to store under the given name
|
||||
* @protected
|
||||
*/
|
||||
setBoolean(name: string, value: boolean): void
|
||||
|
||||
/**
|
||||
* Retrieves a number from storage.
|
||||
*
|
||||
* @param name the name of the number to retrieve
|
||||
* @param def the value to return if no number is stored with the given name
|
||||
* @protected
|
||||
*/
|
||||
getNumber(name: string, def: number): number
|
||||
|
||||
/**
|
||||
* Stores a number.
|
||||
*
|
||||
* @param name the name of the number to store
|
||||
* @param value the number to store under the given name
|
||||
* @protected
|
||||
*/
|
||||
setNumber(name: string, value: number): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores key-value pairs in a single entry in `localStorage`.
|
||||
*/
|
||||
export class LocalStorage implements Storage {
|
||||
private readonly key: string;
|
||||
private _cache: { [key: string]: string } | null = null;
|
||||
private cache: { [key: string]: string } | null = null;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -23,10 +86,10 @@ export class Storage {
|
|||
* @private
|
||||
*/
|
||||
private read(): { [key: string]: string } {
|
||||
if (this._cache === null)
|
||||
this._cache = JSON.parse(localStorage.getItem(this.key) ?? "{}");
|
||||
if (this.cache === null)
|
||||
this.cache = JSON.parse(localStorage.getItem(this.key) ?? "{}");
|
||||
|
||||
return this._cache!;
|
||||
return this.cache!;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -36,64 +99,79 @@ export class Storage {
|
|||
* @private
|
||||
*/
|
||||
private write(item: { [key: string]: string }): void {
|
||||
this._cache = item;
|
||||
this.cache = item;
|
||||
localStorage.setItem(this.key, JSON.stringify(item));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the data from storage.
|
||||
*/
|
||||
clear(): void {
|
||||
this._cache = null;
|
||||
this.cache = null;
|
||||
localStorage.removeItem(this.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a boolean from storage.
|
||||
*
|
||||
* @param name the name of the boolean to retrieve
|
||||
* @param def the value to return if no boolean is stored with the given name
|
||||
* @protected
|
||||
*/
|
||||
getArray(name: string, def: any[] = []): any[] {
|
||||
return JSON.parse(this.read()[name]) ?? def;
|
||||
}
|
||||
|
||||
setArray(name: string, value: any[]): void {
|
||||
const item = this.read();
|
||||
item[name] = JSON.stringify(value);
|
||||
this.write(item);
|
||||
}
|
||||
|
||||
getBoolean(name: string, def: boolean = false): boolean {
|
||||
return (this.read()[name] ?? `${def}`) === "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a boolean.
|
||||
*
|
||||
* @param name the name of the boolean to store
|
||||
* @param value the boolean to store under the given name
|
||||
* @protected
|
||||
*/
|
||||
setBoolean(name: string, value: boolean): void {
|
||||
const item = this.read();
|
||||
item[name] = "" + value;
|
||||
this.write(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a number from storage.
|
||||
*
|
||||
* @param name the name of the number to retrieve
|
||||
* @param def the value to return if no number is stored with the given name
|
||||
* @protected
|
||||
*/
|
||||
getNumber(name: string, def: number = 0): number {
|
||||
return +(this.read()[name] ?? def);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a number.
|
||||
*
|
||||
* @param name the name of the number to store
|
||||
* @param value the number to store under the given name
|
||||
* @protected
|
||||
*/
|
||||
setNumber(name: string, value: number): void {
|
||||
const item = this.read();
|
||||
item[name] = "" + value;
|
||||
this.write(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores key-value pairs in an object.
|
||||
*/
|
||||
export class MemoryStorage implements Storage {
|
||||
private storage: { [key: string]: any } = {};
|
||||
|
||||
|
||||
clear(): void {
|
||||
this.storage = {};
|
||||
}
|
||||
|
||||
setArray(name: string, value: any[] = []): void {
|
||||
this.storage[name] = value;
|
||||
}
|
||||
|
||||
getArray(name: string, def: any[]): any[] {
|
||||
return this.storage[name] ?? def;
|
||||
}
|
||||
|
||||
setBoolean(name: string, value: boolean): void {
|
||||
this.storage[name] = value;
|
||||
}
|
||||
|
||||
getBoolean(name: string, def: boolean): boolean {
|
||||
return this.storage[name] ?? def;
|
||||
}
|
||||
|
||||
setNumber(name: string, value: number): void {
|
||||
this.storage[name] = value;
|
||||
}
|
||||
|
||||
getNumber(name: string, def: number): number {
|
||||
return this.storage[name] ?? def;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue