Begun writing custom abstraction layers for mongo, removing mongoose.
This commit is contained in:
@@ -5,3 +5,5 @@ corresponding methods.
|
|||||||
"DbUser", etc. all being immutable object copies with limited attributes and no methods, or actually create extra
|
"DbUser", etc. all being immutable object copies with limited attributes and no methods, or actually create extra
|
||||||
classes that have methods that can be called (right now thinking the first idea is better).
|
classes that have methods that can be called (right now thinking the first idea is better).
|
||||||
-> This way will be completely decoupled from Mongoose and MongoDB and will have full control.
|
-> This way will be completely decoupled from Mongoose and MongoDB and will have full control.
|
||||||
|
|
||||||
|
- Fix the stats logic so that the stats model does most of it
|
||||||
114
src/classes/ScoreBlock.ts
Executable file
114
src/classes/ScoreBlock.ts
Executable file
@@ -0,0 +1,114 @@
|
|||||||
|
import ScoreCell, {
|
||||||
|
createCellFromDef,
|
||||||
|
ScoreCellValue,
|
||||||
|
CellState,
|
||||||
|
ScoreCellJSONRepresentation
|
||||||
|
} from "./ScoreCell";
|
||||||
|
import {CellDef, BlockDef, BonusBlockDef, NoBonusBlockDef, CellFlag } from "../../../shared/rulesets";
|
||||||
|
|
||||||
|
export const createBlockFromDef = (blockId: string, blockDef: BlockDef) : ScoreBlock => {
|
||||||
|
if (blockDef.hasBonus) {
|
||||||
|
return new ScoreBlockWithBonus(blockId, blockDef);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new ScoreBlockNoBonus(blockId, blockDef);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ScoreBlockJSONRepresentation {
|
||||||
|
cells: Record<string, ScoreCellJSONRepresentation>;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ScoreBlock {
|
||||||
|
protected cells: ScoreCell[];
|
||||||
|
protected id: string;
|
||||||
|
|
||||||
|
protected constructor(blockId: string, blockDef: BlockDef) {
|
||||||
|
this.cells = ScoreBlock.generateCells(blockDef.cells);
|
||||||
|
this.id = blockId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static generateCells(cellDefs: Record<string, CellDef>): ScoreCell[] {
|
||||||
|
const cells = [];
|
||||||
|
for (const cellId in cellDefs) {
|
||||||
|
cells.push(createCellFromDef(cellId, cellDefs[cellId]));
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getTotal(): number;
|
||||||
|
|
||||||
|
abstract bonusAttained(): boolean;
|
||||||
|
|
||||||
|
getSubtotal(): number {
|
||||||
|
let blockScore = 0;
|
||||||
|
for (const cell of this.cells) {
|
||||||
|
blockScore += cell.getScore();
|
||||||
|
}
|
||||||
|
return blockScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellScoreById(cellId: string): number {
|
||||||
|
return this.getCellById(cellId).getScore();
|
||||||
|
}
|
||||||
|
|
||||||
|
cellWithIdIsStruck(cellId: string): boolean {
|
||||||
|
return this.getCellById(cellId).isStruck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCellById(cellId: string): ScoreCell {
|
||||||
|
const foundScoreCell = this.cells.find(cell => cell.getId() === cellId);
|
||||||
|
if (foundScoreCell !== undefined) {
|
||||||
|
return foundScoreCell;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("ScoreCell with ID " + cellId + " not found in block with ID " + this.id + "!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateWithJSON(jsonRep: ScoreBlockJSONRepresentation): void {
|
||||||
|
for (const cell of this.cells) {
|
||||||
|
cell.hydrateWithJSON(jsonRep.cells[cell.getId()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoreBlockWithBonus extends ScoreBlock {
|
||||||
|
protected readonly bonus: number;
|
||||||
|
protected readonly bonusFor: number;
|
||||||
|
|
||||||
|
constructor(blockId: string, blockDef: BonusBlockDef) {
|
||||||
|
super(blockId, blockDef);
|
||||||
|
this.bonus = blockDef.bonusScore;
|
||||||
|
this.bonusFor = blockDef.bonusFor;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotal(): number {
|
||||||
|
const prelimScore = this.getSubtotal();
|
||||||
|
return prelimScore >= this.bonusFor ? prelimScore + this.bonus : prelimScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
bonusAttained(): boolean {
|
||||||
|
return this.getSubtotal() >= this.bonusFor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoreBlockNoBonus extends ScoreBlock {
|
||||||
|
constructor(blockId: string, blockDef: NoBonusBlockDef) {
|
||||||
|
super(blockId, blockDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotal(): number {
|
||||||
|
return this.getSubtotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
bonusAttained(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScoreBlock;
|
||||||
70
src/classes/ScoreCalculator.ts
Executable file
70
src/classes/ScoreCalculator.ts
Executable file
@@ -0,0 +1,70 @@
|
|||||||
|
import {BlockDef, Ruleset} from "../../../shared/rulesets";
|
||||||
|
import ScoreBlock, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlock";
|
||||||
|
|
||||||
|
export type CellLocation = { blockId: string, cellId: string };
|
||||||
|
|
||||||
|
export interface ScoreCardJSONRepresentation {
|
||||||
|
blocks: Record<string, ScoreBlockJSONRepresentation>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoreCalculator {
|
||||||
|
private readonly blocks: ScoreBlock[];
|
||||||
|
|
||||||
|
constructor(gameSchema: Ruleset) {
|
||||||
|
this.blocks = ScoreCalculator.generateBlocks(gameSchema.blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateWithJSON(jsonRep: ScoreCardJSONRepresentation): void {
|
||||||
|
for (const block of this.blocks) {
|
||||||
|
block.hydrateWithJSON(jsonRep.blocks[block.getId()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static generateBlocks(blockDefs: Record<string, BlockDef>): ScoreBlock[] {
|
||||||
|
const blocks = [];
|
||||||
|
for (const blockId in blockDefs) {
|
||||||
|
blocks.push(createBlockFromDef(blockId, blockDefs[blockId]));
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotal(): number {
|
||||||
|
let playerTotal = 0;
|
||||||
|
for (const block of this.blocks) {
|
||||||
|
playerTotal += block.getTotal();
|
||||||
|
}
|
||||||
|
return playerTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockTotalById(blockId: string): number {
|
||||||
|
return this.getBlockById(blockId).getTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockSubTotalById(blockId: string): number {
|
||||||
|
return this.getBlockById(blockId).getSubtotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
blockWithIdHasBonus(blockId: string): boolean {
|
||||||
|
return this.getBlockById(blockId).bonusAttained();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellScoreByLocation(loc: CellLocation): number {
|
||||||
|
return this.getBlockById(loc.blockId).getCellScoreById(loc.cellId);
|
||||||
|
}
|
||||||
|
|
||||||
|
cellAtLocationIsStruck(loc: CellLocation): boolean {
|
||||||
|
return this.getBlockById(loc.blockId).cellWithIdIsStruck(loc.cellId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBlockById(blockId: string): ScoreBlock {
|
||||||
|
const foundScoreBlock = this.blocks.find(block => block.getId() === blockId);
|
||||||
|
if (foundScoreBlock !== undefined) {
|
||||||
|
return foundScoreBlock;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("ScoreBlock with ID " + blockId + " not found on ruleset for player score card!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScoreCalculator;
|
||||||
149
src/classes/ScoreCell.ts
Executable file
149
src/classes/ScoreCell.ts
Executable file
@@ -0,0 +1,149 @@
|
|||||||
|
import {
|
||||||
|
BoolCellDef,
|
||||||
|
CellDef,
|
||||||
|
CellFlag,
|
||||||
|
FieldType,
|
||||||
|
MultiplierCellDef,
|
||||||
|
NumberCellDef,
|
||||||
|
SuperkadiCellDef
|
||||||
|
} from "../../../shared/rulesets";
|
||||||
|
|
||||||
|
export const createCellFromDef = (cellId: string, cellDef: CellDef): ScoreCell => {
|
||||||
|
switch (cellDef.fieldType) {
|
||||||
|
case FieldType.number:
|
||||||
|
return new NumberScoreCell(cellId, cellDef);
|
||||||
|
case FieldType.bool:
|
||||||
|
return new BoolScoreCell(cellId, cellDef);
|
||||||
|
case FieldType.multiplier:
|
||||||
|
return new MultiplierScoreCell(cellId, cellDef);
|
||||||
|
case FieldType.superkadi:
|
||||||
|
return new SuperkadiScoreCell(cellId, cellDef);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScoreCellValue = number | boolean;
|
||||||
|
|
||||||
|
export interface CellState {
|
||||||
|
id: string;
|
||||||
|
struck: boolean;
|
||||||
|
value: number | boolean;
|
||||||
|
currentIteratorIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScoreCellJSONRepresentation {
|
||||||
|
value: number | boolean | CellFlag.strike;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ScoreCell {
|
||||||
|
protected readonly id: string;
|
||||||
|
protected static readonly fieldType: FieldType;
|
||||||
|
protected struck: boolean;
|
||||||
|
protected value: number | boolean;
|
||||||
|
|
||||||
|
protected constructor(cellId: string, cellDef: CellDef) {
|
||||||
|
this.id = cellId;
|
||||||
|
this.struck = false;
|
||||||
|
this.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getScore(): number;
|
||||||
|
|
||||||
|
isStruck(): boolean {
|
||||||
|
return this.struck;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getJSONRepresentation(): ScoreCellJSONRepresentation {
|
||||||
|
return { value: this.isStruck() ? CellFlag.strike : this.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateWithJSON(jsonRep: ScoreCellJSONRepresentation): void {
|
||||||
|
if (jsonRep.value === CellFlag.strike) {
|
||||||
|
this.struck = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.value = jsonRep.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumberScoreCell extends ScoreCell {
|
||||||
|
protected static readonly fieldType = FieldType.number;
|
||||||
|
|
||||||
|
constructor(cellId: string, cellDef: NumberCellDef) {
|
||||||
|
super(cellId, cellDef);
|
||||||
|
this.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getScore(): number {
|
||||||
|
return this.value as number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BoolScoreCell extends ScoreCell {
|
||||||
|
protected static readonly fieldType = FieldType.bool;
|
||||||
|
private readonly score: number;
|
||||||
|
protected value: boolean;
|
||||||
|
|
||||||
|
constructor(cellId: string, cellDef: BoolCellDef) {
|
||||||
|
super(cellId, cellDef);
|
||||||
|
this.score = cellDef.score;
|
||||||
|
this.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getScore(): number {
|
||||||
|
if (this.value && !this.isStruck()) {
|
||||||
|
return this.score;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SuperkadiScoreCell extends ScoreCell {
|
||||||
|
protected static readonly fieldType = FieldType.superkadi;
|
||||||
|
private readonly score: number;
|
||||||
|
protected value: number;
|
||||||
|
|
||||||
|
constructor(cellId: string, cellDef: SuperkadiCellDef) {
|
||||||
|
super(cellId, cellDef);
|
||||||
|
this.score = cellDef.score;
|
||||||
|
this.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getScore(): number {
|
||||||
|
if (this.isStruck()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return this.score * this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiplierScoreCell extends ScoreCell {
|
||||||
|
protected static readonly fieldType = FieldType.multiplier;
|
||||||
|
protected readonly multiplier: number;
|
||||||
|
protected value: number;
|
||||||
|
|
||||||
|
constructor(cellId: string, cellDef: MultiplierCellDef) {
|
||||||
|
super(cellId, cellDef);
|
||||||
|
this.multiplier = cellDef.multiplier;
|
||||||
|
this.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getScore(): number {
|
||||||
|
if (this.isStruck()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return this.multiplier * this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScoreCell;
|
||||||
@@ -135,11 +135,16 @@ DbUserSchema.statics.getSerializedAuthUser = async function (id: string): Promis
|
|||||||
};
|
};
|
||||||
|
|
||||||
DbUserSchema.statics.incrementTimesNoWinner = async function (id: string): Promise<void> {
|
DbUserSchema.statics.incrementTimesNoWinner = async function (id: string): Promise<void> {
|
||||||
...
|
return tryQuery(() => {
|
||||||
|
return DbUser.findById(id, {id: 1, username: 1, password: 1, lang: 1, email: 1});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
DbUserSchema.statics.incrementGamesPlayed = async function (id: string): Promise<void> {
|
DbUserSchema.statics.incrementGamesPlayed = async function (id: string): Promise<void> {
|
||||||
...
|
return tryQuery(async () => {
|
||||||
|
const user = await DbUser.findById(id);
|
||||||
|
user.accountStats.gamesPlayed =
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
DbUserSchema.methods.getGuests = async function (this: IDbUser): Promise<IPlayer[]> {
|
DbUserSchema.methods.getGuests = async function (this: IDbUser): Promise<IPlayer[]> {
|
||||||
|
|||||||
@@ -1,68 +1,73 @@
|
|||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import {IPlayerStats, IPlayerStatsDoc, PlayerStatsSchema} from "./stats";
|
import {
|
||||||
import {globalSchemaOptions} from "./utils";
|
IPlayerStats,
|
||||||
|
IPlayerStatsDoc, OutcomeType,
|
||||||
|
PlayerGameResults,
|
||||||
|
PlayerStats,
|
||||||
|
PlayerStatsMongoObjectInterface,
|
||||||
|
PlayerStatsSchema, PlayerStatsUpdater
|
||||||
|
} from "./stats";
|
||||||
|
import {
|
||||||
|
MongoStoredObject, MongoStoredObjectCollection,
|
||||||
|
} from "./utils";
|
||||||
import {CellValue} from "../controllers/statsController";
|
import {CellValue} from "../controllers/statsController";
|
||||||
|
import mongo from "mongodb";
|
||||||
|
import {Ruleset} from "../../../shared/rulesets";
|
||||||
|
|
||||||
interface CellDetails {
|
export interface CellDetails {
|
||||||
id: string;
|
id: string;
|
||||||
value: CellValue;
|
value: CellValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPlayer {
|
export interface StoredPlayerData {
|
||||||
id: string;
|
_id: string;
|
||||||
nick: string;
|
nick: string;
|
||||||
stats: IPlayerStats;
|
stats: PlayerStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPlayerDoc extends mongoose.Document {
|
interface StoredPlayerCollection {
|
||||||
id: string;
|
findPlayerById(id: string): Promise<StoredPlayer>;
|
||||||
nick: string;
|
|
||||||
stats: IPlayerStatsDoc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPlayerModel extends mongoose.Model<IPlayerDoc> {
|
class MongoStoredPlayerCollection
|
||||||
incrementGamesPlayed(playerId: string): void;
|
extends MongoStoredObjectCollection<MongoStoredPlayer>
|
||||||
incrementWinFor(playerId: string): void;
|
implements StoredPlayerCollection {
|
||||||
incrementDrawFor(playerId: string): void;
|
|
||||||
incrementRunnerUpFor(playerId: string): void;
|
private updater: PlayerStatsUpdater;
|
||||||
incrementLossFor(playerId: string): void;
|
constructor(collectionClient: mongo.Collection) {
|
||||||
updateCellStats(playerId: string, cellDetails: CellDetails): void;
|
super(collectionClient);
|
||||||
incrementBonus(playerId: string): Promise<void>;
|
this.updater = new PlayerStatsUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayerSchema = new mongoose.Schema({
|
async findPlayerById(id: string): Promise<StoredPlayer> {
|
||||||
nick: { type: String, required: true },
|
const data = await this.findObjectById(id);
|
||||||
stats: { type: PlayerStatsSchema, required: true, default: () => ({}) },
|
return new MongoStoredPlayer(data);
|
||||||
}, {...globalSchemaOptions});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PlayerSchema.statics.incrementGamesPlayed = async function (playerId: string): Promise<void> {
|
export interface StoredPlayer {
|
||||||
...
|
nick(): string;
|
||||||
};
|
setNick(newNick: string): Promise<void>;
|
||||||
|
updateStats(results: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
PlayerSchema.statics.incrementWinFor = async function (playerId: string): Promise<void> {
|
class MongoStoredPlayer extends MongoStoredObject implements StoredPlayer {
|
||||||
...
|
constructor(data: StoredPlayerData) {
|
||||||
};
|
super(data);
|
||||||
|
}
|
||||||
|
|
||||||
PlayerSchema.statics.incrementDrawFor = async function (playerId: string): Promise<void> {
|
nick(): string {
|
||||||
...
|
return this.data.nick;
|
||||||
};
|
}
|
||||||
|
|
||||||
PlayerSchema.statics.incrementRunnerUpFor = async function (playerId: string): Promise<void> {
|
async setNick(newNick: string): Promise<void> {
|
||||||
...
|
this.data.nick = newNick;
|
||||||
};
|
}
|
||||||
|
|
||||||
PlayerSchema.statics.incrementLossFor = async function (playerId: string): Promise<void> {
|
async updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset) {
|
||||||
...
|
const statsInterface = new PlayerStatsMongoObjectInterface(this.data.stats);
|
||||||
};
|
await statsInterface.updateStats(playerGameResults, ruleset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PlayerSchema.statics.updateCellStats = async function (playerId: string, cellDetails: CellDetails): void {
|
export default PlayerCollection;
|
||||||
...
|
|
||||||
};
|
|
||||||
|
|
||||||
PlayerSchema.statics.incrementBonus = async function (playerId: string): Promise<void> {
|
|
||||||
...
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const Player = mongoose.model<IPlayerDoc, IPlayerModel>("Player", PlayerSchema);
|
|
||||||
export default Player;
|
|
||||||
@@ -1,177 +1,195 @@
|
|||||||
import mongoose from "mongoose";
|
import {FieldType, Ruleset} from "../../../shared/rulesets";
|
||||||
|
import ScoreCalculator, {ScoreCardJSONRepresentation} from "../classes/ScoreCalculator";
|
||||||
|
|
||||||
// Interfaces and types for business logic
|
class UpdateError extends Error {
|
||||||
export interface IPlayerStats extends IBaseStats {
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "UpdateError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutcomeType = "win" | "loss" | "runnerUp" | "draw";
|
||||||
|
export interface PlayerStats extends BaseStats {
|
||||||
|
}
|
||||||
|
export interface AccountStats extends BaseStats {
|
||||||
|
timesNoWinner: number;
|
||||||
|
}
|
||||||
|
interface BaseStats {
|
||||||
|
statsByRuleset: Record<string, RulesetStats>
|
||||||
|
gamesPlayed: number;
|
||||||
|
}
|
||||||
|
interface RulesetStats {
|
||||||
|
blockStats: Record<string, BlockStats>;
|
||||||
wins: number;
|
wins: number;
|
||||||
runnerUps: number;
|
runnerUps: number;
|
||||||
draws: number;
|
draws: number;
|
||||||
losses: number;
|
losses: number;
|
||||||
|
grandTotal: TotalFieldStats;
|
||||||
}
|
}
|
||||||
export interface IAccountStats extends IBaseStats {
|
interface BlockStats {
|
||||||
timesNoWinner: number;
|
cellStats: Record<string, CellStats>;
|
||||||
|
timesHadBonus?: number;
|
||||||
|
total: TotalFieldStats;
|
||||||
}
|
}
|
||||||
export interface IBaseStats {
|
interface BaseCellStats {
|
||||||
one: IMultiplierFieldStats;
|
runningTotal: number;
|
||||||
two: IMultiplierFieldStats;
|
|
||||||
three: IMultiplierFieldStats;
|
|
||||||
four: IMultiplierFieldStats;
|
|
||||||
five: IMultiplierFieldStats;
|
|
||||||
six: IMultiplierFieldStats;
|
|
||||||
upperTotal: ITotalFieldStats;
|
|
||||||
threeKind: INumberFieldStats;
|
|
||||||
fourKind: INumberFieldStats;
|
|
||||||
fullHouse: IBoolFieldStats;
|
|
||||||
smlStraight: IBoolFieldStats;
|
|
||||||
lgStraight: IBoolFieldStats;
|
|
||||||
yahtzee: IYahtzeeFieldStats;
|
|
||||||
chance: INumberFieldStats;
|
|
||||||
grandTotal: ITotalFieldStats;
|
|
||||||
lowerTotal: ITotalFieldStats;
|
|
||||||
gamesPlayed: number;
|
|
||||||
}
|
}
|
||||||
interface ITotalFieldStats {
|
interface StrikeableFieldStats extends BaseCellStats {
|
||||||
average: number;
|
timesStruck: number;
|
||||||
|
}
|
||||||
|
interface BestableFieldStats extends BaseCellStats {
|
||||||
best: number;
|
best: number;
|
||||||
worst: number;
|
worst: number;
|
||||||
}
|
}
|
||||||
interface IBoolFieldStats {
|
type TotalFieldStats = BestableFieldStats;
|
||||||
average: number;
|
type BoolFieldStats = StrikeableFieldStats & { total: number };
|
||||||
timesStruck: number;
|
type NumberFieldStats = StrikeableFieldStats & BestableFieldStats;
|
||||||
total: number;
|
type MultiplierFieldStats = NumberFieldStats;
|
||||||
}
|
type SuperkadiFieldStats = NumberFieldStats;
|
||||||
interface INumberFieldStats {
|
type CellStats = BoolFieldStats | NumberFieldStats | MultiplierFieldStats | SuperkadiFieldStats;
|
||||||
average: number;
|
|
||||||
timesStruck: number;
|
|
||||||
best: number;
|
|
||||||
worst: number;
|
|
||||||
}
|
|
||||||
type IMultiplierFieldStats = INumberFieldStats;
|
|
||||||
type IYahtzeeFieldStats = INumberFieldStats;
|
|
||||||
|
|
||||||
// Mongoose doc interfaces and types
|
export interface PlayerGameResults {
|
||||||
export interface IPlayerStatsDoc extends IBaseStatsDoc {
|
blocks: Record<string, BlockResults>;
|
||||||
wins: number;
|
|
||||||
runnerUps: number;
|
|
||||||
draws: number;
|
|
||||||
losses: number;
|
|
||||||
}
|
}
|
||||||
export interface IAccountStatsDoc extends IBaseStatsDoc {
|
interface BlockResults {
|
||||||
timesNoWinner: number;
|
cells: Record<string, CellResults>
|
||||||
}
|
}
|
||||||
interface IBaseStatsDoc extends mongoose.Document {
|
interface CellResults {
|
||||||
one: IMultiplierFieldStatsDoc;
|
value: CellValue;
|
||||||
two: IMultiplierFieldStatsDoc;
|
}
|
||||||
three: IMultiplierFieldStatsDoc;
|
type CellValue = "cellFlagStrike" | number | boolean;
|
||||||
four: IMultiplierFieldStatsDoc;
|
|
||||||
five: IMultiplierFieldStatsDoc;
|
class BaseStatsUpdater {
|
||||||
six: IMultiplierFieldStatsDoc;
|
private data?: BaseStats;
|
||||||
upperTotal: ITotalFieldStatsDoc;
|
private validationRuleset?: Ruleset;
|
||||||
threeKind: INumberFieldStatsDoc;
|
private calculator?: ScoreCalculator;
|
||||||
fourKind: INumberFieldStatsDoc;
|
private currentStatsObject?: RulesetStats;
|
||||||
fullHouse: IBoolFieldStatsDoc;
|
constructor() {
|
||||||
smlStraight: IBoolFieldStatsDoc;
|
|
||||||
lgStraight: IBoolFieldStatsDoc;
|
|
||||||
yahtzee: IYahtzeeFieldStatsDoc;
|
|
||||||
chance: INumberFieldStatsDoc;
|
|
||||||
grandTotal: ITotalFieldStatsDoc;
|
|
||||||
lowerTotal: ITotalFieldStatsDoc;
|
|
||||||
gamesPlayed: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ITotalFieldStatsDoc = mongoose.Document & ITotalFieldStats;
|
use(data: BaseStats) {
|
||||||
type IBoolFieldStatsDoc = mongoose.Document & IBoolFieldStats;
|
this.data = data;
|
||||||
type INumberFieldStatsDoc = mongoose.Document & INumberFieldStats;
|
}
|
||||||
type IMultiplierFieldStatsDoc = mongoose.Document & IMultiplierFieldStats;
|
|
||||||
type IYahtzeeFieldStatsDoc = mongoose.Document & IYahtzeeFieldStats;
|
|
||||||
|
|
||||||
// Mongoose schemata
|
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
|
||||||
class Int extends mongoose.SchemaType {
|
if (this.data) {
|
||||||
constructor(key: string, options: any) {
|
this.validationRuleset = ruleset;
|
||||||
super(key, options, 'Int');
|
this.calculator = new ScoreCalculator(ruleset);
|
||||||
|
this.calculator.hydrateWithJSON(playerGameResults as ScoreCardJSONRepresentation);
|
||||||
|
this.currentStatsObject = this.data.statsByRuleset[ruleset.id];
|
||||||
|
for (const blockId in ruleset.blocks) {
|
||||||
|
this.updateBlockStats(blockId);
|
||||||
}
|
}
|
||||||
cast(val: any): number {
|
this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal());
|
||||||
let _val = Number(val);
|
this.currentStatsObject.wins += Number(playerGameResults.outcome === "win");
|
||||||
if (isNaN(_val)) {
|
this.currentStatsObject.draws += Number(playerGameResults.outcome === "draw");
|
||||||
throw new Error('ZeroPositiveInt: ' + val + ' is not a number');
|
this.currentStatsObject.runnerUps += Number(playerGameResults.outcome === "runnerUp");
|
||||||
|
this.currentStatsObject.losses += Number(playerGameResults.outcome === "loss");
|
||||||
}
|
}
|
||||||
_val = Math.round(_val);
|
else {
|
||||||
return _val;
|
throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data
|
||||||
|
to analyse.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(mongoose.Schema.Types as any).Int = Int;
|
|
||||||
|
|
||||||
const TotalFieldStatsSchema = new mongoose.Schema( {
|
private updateTotalFieldStats(statsObject: TotalFieldStats, total: number) {
|
||||||
average: {type: Number, required: true, default: 0, min: 0},
|
statsObject.best = total > statsObject.best ? total : statsObject.best;
|
||||||
best: {type: Int, required: true, default: 0, min: 0},
|
statsObject.worst = total < statsObject.worst ? total : statsObject.worst;
|
||||||
worst: {type: Int, required: true, default: 0, min: 0},
|
statsObject.runningTotal += total;
|
||||||
}, { _id: false });
|
}
|
||||||
const BoolFieldStatsSchema = new mongoose.Schema( {
|
|
||||||
average: {type: Number, required: true, default: 0, min: 0, max: 1},
|
|
||||||
timesStruck: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
total: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
}, { _id: false });
|
|
||||||
const NumberFieldStatsSchema = new mongoose.Schema( {
|
|
||||||
average: {type: Number, required: true, default: 0, min: 0},
|
|
||||||
timesStruck: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
best: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
worst: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
}, { _id: false });
|
|
||||||
const MultiplierFieldStatsSchema = new mongoose.Schema( {
|
|
||||||
average: {type: Number, required: true, default: 0, min: 0},
|
|
||||||
timesStruck: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
best: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
worst: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
}, { _id: false });
|
|
||||||
const YahtzeeFieldStatsSchema = new mongoose.Schema( {
|
|
||||||
average: {type: Number, required: true, default: 0, min: 0},
|
|
||||||
timesStruck: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
best: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
worst: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
}, { _id: false });
|
|
||||||
export const PlayerStatsSchema = new mongoose.Schema( {
|
|
||||||
one: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
two: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
three: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
four: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
threeKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
fourKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
fullHouse: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
smlStraight: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
lgStraight: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
yahtzee: { type: YahtzeeFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
chance: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
grandTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
lowerTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
timesHadBonus: {type: Int, required: true, default: 0, min: 0},
|
|
||||||
gamesPlayed: { type: Int, required: true, default: 0, min: 0 },
|
|
||||||
wins: { type: Int, required: true, default: 0, min: 0 },
|
|
||||||
runnerUps: { type: Int, required: true, default: 0, min: 0 },
|
|
||||||
draws: { type: Int, required: true, default: 0, min: 0 },
|
|
||||||
losses: { type: Int, required: true, default: 0, min: 0 },
|
|
||||||
}, { _id: false });
|
|
||||||
export const AccountStatsSchema = new mongoose.Schema( {
|
|
||||||
one: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
two: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
three: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
four: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
threeKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
fourKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
fullHouse: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
smlStraight: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
lgStraight: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
yahtzee: { type: YahtzeeFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
chance: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
grandTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
lowerTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
|
|
||||||
gamesPlayed: { type: Int, required: true, default: 0, min: 0 },
|
|
||||||
timesNoWinner: { type: Int, required: true, default: 0, min: 0 },
|
|
||||||
}, { _id: false });
|
|
||||||
|
|
||||||
export const PlayerStats = mongoose.model<IPlayerStatsDoc>("PlayerStats", PlayerStatsSchema);
|
private updateBlockStats(blockId: string) {
|
||||||
export const AccountStats = mongoose.model<IAccountStatsDoc>("AccountStats", AccountStatsSchema);
|
if (this.currentStatsObject) {
|
||||||
|
const blockStats = this.currentStatsObject.blockStats[blockId];
|
||||||
|
this.updateTotalFieldStats(blockStats.total, this.calculator!.getBlockSubTotalById(blockId));
|
||||||
|
if (this.calculator!.blockWithIdHasBonus(blockId)) {
|
||||||
|
blockStats.timesHadBonus! += 1;
|
||||||
|
}
|
||||||
|
for (const cellId in this.validationRuleset!.blocks[blockId].cells) {
|
||||||
|
this.updateCellStatsByIds({cellId, blockId});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCellStatsByIds(ids: {cellId: string, blockId: string}) {
|
||||||
|
const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.id});
|
||||||
|
const cellFieldType = this.validationRuleset?.blocks[ids.blockId].cells[ids.cellId].fieldType;
|
||||||
|
const cellScore = this.calculator!.getCellScoreByLocation({...ids});
|
||||||
|
if (cellScore > 0 && cellFieldType === FieldType.bool) {
|
||||||
|
(cellStats as BoolFieldStats).total += 1;
|
||||||
|
}
|
||||||
|
else if (cellFieldType === FieldType.multiplier || FieldType.superkadi || FieldType.number) {
|
||||||
|
const bestableStats = (cellStats as BestableFieldStats);
|
||||||
|
if (bestableStats.best < cellScore) {
|
||||||
|
bestableStats.best = cellScore;
|
||||||
|
}
|
||||||
|
else if (bestableStats.worst > cellScore) {
|
||||||
|
bestableStats.worst = cellScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.calculator!.cellAtLocationIsStruck({...ids})) {
|
||||||
|
cellStats.timesStruck += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCellStatsByIds(ids: {cellId: string, blockId: string, rulesetId: string}): CellStats {
|
||||||
|
return this.data!.statsByRuleset[ids.rulesetId].blockStats[ids.blockId].cellStats[ids.cellId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RulesetMongoObjectInterface {
|
||||||
|
private data: Ruleset;
|
||||||
|
constructor(data: Ruleset) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
getId() {
|
||||||
|
return this.data.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlayerStatsUpdater {
|
||||||
|
private data?: PlayerStats;
|
||||||
|
private readonly updater: BaseStatsUpdater;
|
||||||
|
constructor(data?: PlayerStats) {
|
||||||
|
if (data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
this.updater = new BaseStatsUpdater();
|
||||||
|
}
|
||||||
|
|
||||||
|
use(data: PlayerStats) {
|
||||||
|
this.data = data;
|
||||||
|
this.updater.use(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
|
||||||
|
this.updater.updateStats(playerGameResults, ruleset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AccountStatsUpdater {
|
||||||
|
private data?: AccountStats;
|
||||||
|
private readonly updater: BaseStatsUpdater;
|
||||||
|
constructor(data?: AccountStats) {
|
||||||
|
if (data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
this.updater = new BaseStatsUpdater();
|
||||||
|
}
|
||||||
|
|
||||||
|
use(data: AccountStats) {
|
||||||
|
this.data = data;
|
||||||
|
this.updater.use(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
|
||||||
|
if (this.data) {
|
||||||
|
this.updater.updateStats(playerGameResults, ruleset);
|
||||||
|
this.data.gamesPlayed += 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data
|
||||||
|
to analyse.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,62 @@
|
|||||||
export class GenericModelError extends Error {
|
import mongo, {MongoClient, Db} from "mongodb";
|
||||||
|
import Settings from "../server-config.json";
|
||||||
|
import {PlayerData, StoredPlayer} from "./player";
|
||||||
|
|
||||||
|
let SessionDbClient: Db;
|
||||||
|
export async function initMongoSessionClient() {
|
||||||
|
if (SessionDbClient === undefined) {
|
||||||
|
const client = await MongoClient.connect(Settings.mongodb_uri, {useUnifiedTopology: true});
|
||||||
|
SessionDbClient = client.db();
|
||||||
|
}
|
||||||
|
return SessionDbClient;
|
||||||
|
}
|
||||||
|
export async function getMongoObjectCollection(collectionName: string) {
|
||||||
|
if (SessionDbClient === undefined) {
|
||||||
|
throw new MongoError("Cannot retrieve a collection before the session client has been initialised!");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return SessionDbClient.collection(collectionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class MongoStoredObjectCollection<T extends MongoStoredObject> {
|
||||||
|
protected mongoDbClientCollection: mongo.Collection;
|
||||||
|
protected constructor(collectionClient: mongo.Collection) {
|
||||||
|
this.mongoDbClientCollection = collectionClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async findObjectById(id: string): Promise<any | null> {
|
||||||
|
return this.mongoDbClientCollection!.findOne({_id: id});
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(...objects: T[]): Promise<void> {
|
||||||
|
for (const object of objects) {
|
||||||
|
await this.mongoDbClientCollection.findOneAndUpdate({_id: object.id()}, {...object.rawData()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class MongoStoredObject {
|
||||||
|
protected constructor(protected data: {_id: string} & any) {}
|
||||||
|
|
||||||
|
id(): string {
|
||||||
|
return this.data._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
rawData(): PlayerData {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class MongoError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "MongoError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GenericModelError extends MongoError {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "GenericModelError";
|
this.name = "GenericModelError";
|
||||||
|
|||||||
Reference in New Issue
Block a user