Remove duplication by separating types in Storage

This commit is contained in:
Florine W. Dekker 2020-09-02 17:27:37 +02:00
parent 11bc3f9be7
commit ddb27c8061
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
9 changed files with 249 additions and 211 deletions

View File

@ -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",

View File

@ -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}`;
}

View File

@ -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";
/**

View File

@ -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)));

View File

@ -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);
}
}

View File

@ -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(() => {

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}