From 11bf3821a9a70d123520336e70181e3bc0d827ab Mon Sep 17 00:00:00 2001 From: Daniel Ledda Date: Thu, 25 Jun 2020 20:57:58 +0200 Subject: [PATCH] Begun writing custom abstraction layers for mongo, removing mongoose. --- notes.txt | 4 +- src/classes/ScoreBlock.ts | 114 +++++++++++ src/classes/ScoreCalculator.ts | 70 +++++++ src/classes/ScoreCell.ts | 149 ++++++++++++++ src/models/dbUser.ts | 9 +- src/models/player.ts | 103 +++++----- src/models/stats.ts | 358 +++++++++++++++++---------------- src/models/utils.ts | 60 +++++- 8 files changed, 644 insertions(+), 223 deletions(-) create mode 100755 src/classes/ScoreBlock.ts create mode 100755 src/classes/ScoreCalculator.ts create mode 100755 src/classes/ScoreCell.ts diff --git a/notes.txt b/notes.txt index db67a2c..d492534 100644 --- a/notes.txt +++ b/notes.txt @@ -4,4 +4,6 @@ corresponding methods. - Decide on whether to always use the namespace to call methods and pass an id, with the returned objects "Player", "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). - -> This way will be completely decoupled from Mongoose and MongoDB and will have full control. \ No newline at end of file + -> 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 \ No newline at end of file diff --git a/src/classes/ScoreBlock.ts b/src/classes/ScoreBlock.ts new file mode 100755 index 0000000..578ee78 --- /dev/null +++ b/src/classes/ScoreBlock.ts @@ -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; +} + +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): 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; \ No newline at end of file diff --git a/src/classes/ScoreCalculator.ts b/src/classes/ScoreCalculator.ts new file mode 100755 index 0000000..38e30fc --- /dev/null +++ b/src/classes/ScoreCalculator.ts @@ -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; +} + +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): 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; \ No newline at end of file diff --git a/src/classes/ScoreCell.ts b/src/classes/ScoreCell.ts new file mode 100755 index 0000000..cac8354 --- /dev/null +++ b/src/classes/ScoreCell.ts @@ -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; \ No newline at end of file diff --git a/src/models/dbUser.ts b/src/models/dbUser.ts index 4a65665..595b598 100755 --- a/src/models/dbUser.ts +++ b/src/models/dbUser.ts @@ -135,11 +135,16 @@ DbUserSchema.statics.getSerializedAuthUser = async function (id: string): Promis }; DbUserSchema.statics.incrementTimesNoWinner = async function (id: string): Promise { - ... + return tryQuery(() => { + return DbUser.findById(id, {id: 1, username: 1, password: 1, lang: 1, email: 1}); + }); }; DbUserSchema.statics.incrementGamesPlayed = async function (id: string): Promise { - ... + return tryQuery(async () => { + const user = await DbUser.findById(id); + user.accountStats.gamesPlayed = + }); }; DbUserSchema.methods.getGuests = async function (this: IDbUser): Promise { diff --git a/src/models/player.ts b/src/models/player.ts index becde27..99c8bd8 100755 --- a/src/models/player.ts +++ b/src/models/player.ts @@ -1,68 +1,73 @@ import mongoose from "mongoose"; -import {IPlayerStats, IPlayerStatsDoc, PlayerStatsSchema} from "./stats"; -import {globalSchemaOptions} from "./utils"; +import { + IPlayerStats, + IPlayerStatsDoc, OutcomeType, + PlayerGameResults, + PlayerStats, + PlayerStatsMongoObjectInterface, + PlayerStatsSchema, PlayerStatsUpdater +} from "./stats"; +import { + MongoStoredObject, MongoStoredObjectCollection, +} from "./utils"; import {CellValue} from "../controllers/statsController"; +import mongo from "mongodb"; +import {Ruleset} from "../../../shared/rulesets"; -interface CellDetails { +export interface CellDetails { id: string; value: CellValue; } -export interface IPlayer { - id: string; +export interface StoredPlayerData { + _id: string; nick: string; - stats: IPlayerStats; + stats: PlayerStats; } -export interface IPlayerDoc extends mongoose.Document { - id: string; - nick: string; - stats: IPlayerStatsDoc; +interface StoredPlayerCollection { + findPlayerById(id: string): Promise; } -export interface IPlayerModel extends mongoose.Model { - incrementGamesPlayed(playerId: string): void; - incrementWinFor(playerId: string): void; - incrementDrawFor(playerId: string): void; - incrementRunnerUpFor(playerId: string): void; - incrementLossFor(playerId: string): void; - updateCellStats(playerId: string, cellDetails: CellDetails): void; - incrementBonus(playerId: string): Promise; +class MongoStoredPlayerCollection + extends MongoStoredObjectCollection + implements StoredPlayerCollection { + + private updater: PlayerStatsUpdater; + constructor(collectionClient: mongo.Collection) { + super(collectionClient); + this.updater = new PlayerStatsUpdater(); + } + + async findPlayerById(id: string): Promise { + const data = await this.findObjectById(id); + return new MongoStoredPlayer(data); + } } -export const PlayerSchema = new mongoose.Schema({ - nick: { type: String, required: true }, - stats: { type: PlayerStatsSchema, required: true, default: () => ({}) }, -}, {...globalSchemaOptions}); +export interface StoredPlayer { + nick(): string; + setNick(newNick: string): Promise; + updateStats(results: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): Promise; +} -PlayerSchema.statics.incrementGamesPlayed = async function (playerId: string): Promise { - ... -}; +class MongoStoredPlayer extends MongoStoredObject implements StoredPlayer { + constructor(data: StoredPlayerData) { + super(data); + } -PlayerSchema.statics.incrementWinFor = async function (playerId: string): Promise { - ... -}; + nick(): string { + return this.data.nick; + } -PlayerSchema.statics.incrementDrawFor = async function (playerId: string): Promise { - ... -}; + async setNick(newNick: string): Promise { + this.data.nick = newNick; + } -PlayerSchema.statics.incrementRunnerUpFor = async function (playerId: string): Promise { - ... -}; + async updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset) { + const statsInterface = new PlayerStatsMongoObjectInterface(this.data.stats); + await statsInterface.updateStats(playerGameResults, ruleset); + } +} -PlayerSchema.statics.incrementLossFor = async function (playerId: string): Promise { - ... -}; - -PlayerSchema.statics.updateCellStats = async function (playerId: string, cellDetails: CellDetails): void { - ... -}; - -PlayerSchema.statics.incrementBonus = async function (playerId: string): Promise { - ... -}; - - -const Player = mongoose.model("Player", PlayerSchema); -export default Player; \ No newline at end of file +export default PlayerCollection; \ No newline at end of file diff --git a/src/models/stats.ts b/src/models/stats.ts index 954c5b1..66eb168 100755 --- a/src/models/stats.ts +++ b/src/models/stats.ts @@ -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 -export interface IPlayerStats extends IBaseStats { - wins: number; - runnerUps: number; - draws: number; - losses: number; -} -export interface IAccountStats extends IBaseStats { - timesNoWinner: number; -} -export interface IBaseStats { - one: IMultiplierFieldStats; - 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 { - average: number; - best: number; - worst: number; -} -interface IBoolFieldStats { - average: number; - timesStruck: number; - total: number; -} -interface INumberFieldStats { - average: number; - timesStruck: number; - best: number; - worst: number; -} -type IMultiplierFieldStats = INumberFieldStats; -type IYahtzeeFieldStats = INumberFieldStats; - -// Mongoose doc interfaces and types -export interface IPlayerStatsDoc extends IBaseStatsDoc { - wins: number; - runnerUps: number; - draws: number; - losses: number; -} -export interface IAccountStatsDoc extends IBaseStatsDoc { - timesNoWinner: number; -} -interface IBaseStatsDoc extends mongoose.Document { - one: IMultiplierFieldStatsDoc; - two: IMultiplierFieldStatsDoc; - three: IMultiplierFieldStatsDoc; - four: IMultiplierFieldStatsDoc; - five: IMultiplierFieldStatsDoc; - six: IMultiplierFieldStatsDoc; - upperTotal: ITotalFieldStatsDoc; - threeKind: INumberFieldStatsDoc; - fourKind: INumberFieldStatsDoc; - fullHouse: IBoolFieldStatsDoc; - smlStraight: IBoolFieldStatsDoc; - lgStraight: IBoolFieldStatsDoc; - yahtzee: IYahtzeeFieldStatsDoc; - chance: INumberFieldStatsDoc; - grandTotal: ITotalFieldStatsDoc; - lowerTotal: ITotalFieldStatsDoc; - gamesPlayed: number; -} - -type ITotalFieldStatsDoc = mongoose.Document & ITotalFieldStats; -type IBoolFieldStatsDoc = mongoose.Document & IBoolFieldStats; -type INumberFieldStatsDoc = mongoose.Document & INumberFieldStats; -type IMultiplierFieldStatsDoc = mongoose.Document & IMultiplierFieldStats; -type IYahtzeeFieldStatsDoc = mongoose.Document & IYahtzeeFieldStats; - -// Mongoose schemata -class Int extends mongoose.SchemaType { - constructor(key: string, options: any) { - super(key, options, 'Int'); +class UpdateError extends Error { + constructor(message: string) { + super(message); + this.name = "UpdateError"; } - cast(val: any): number { - let _val = Number(val); - if (isNaN(_val)) { - throw new Error('ZeroPositiveInt: ' + val + ' is not a number'); +} + +export type OutcomeType = "win" | "loss" | "runnerUp" | "draw"; +export interface PlayerStats extends BaseStats { +} +export interface AccountStats extends BaseStats { + timesNoWinner: number; +} +interface BaseStats { + statsByRuleset: Record + gamesPlayed: number; +} +interface RulesetStats { + blockStats: Record; + wins: number; + runnerUps: number; + draws: number; + losses: number; + grandTotal: TotalFieldStats; +} +interface BlockStats { + cellStats: Record; + timesHadBonus?: number; + total: TotalFieldStats; +} +interface BaseCellStats { + runningTotal: number; +} +interface StrikeableFieldStats extends BaseCellStats { + timesStruck: number; +} +interface BestableFieldStats extends BaseCellStats { + best: number; + worst: number; +} +type TotalFieldStats = BestableFieldStats; +type BoolFieldStats = StrikeableFieldStats & { total: number }; +type NumberFieldStats = StrikeableFieldStats & BestableFieldStats; +type MultiplierFieldStats = NumberFieldStats; +type SuperkadiFieldStats = NumberFieldStats; +type CellStats = BoolFieldStats | NumberFieldStats | MultiplierFieldStats | SuperkadiFieldStats; + +export interface PlayerGameResults { + blocks: Record; +} +interface BlockResults { + cells: Record +} +interface CellResults { + value: CellValue; +} +type CellValue = "cellFlagStrike" | number | boolean; + +class BaseStatsUpdater { + private data?: BaseStats; + private validationRuleset?: Ruleset; + private calculator?: ScoreCalculator; + private currentStatsObject?: RulesetStats; + constructor() { + } + + use(data: BaseStats) { + this.data = data; + } + + updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void { + if (this.data) { + this.validationRuleset = ruleset; + 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); + } + this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal()); + this.currentStatsObject.wins += Number(playerGameResults.outcome === "win"); + this.currentStatsObject.draws += Number(playerGameResults.outcome === "draw"); + this.currentStatsObject.runnerUps += Number(playerGameResults.outcome === "runnerUp"); + this.currentStatsObject.losses += Number(playerGameResults.outcome === "loss"); } - _val = Math.round(_val); - return _val; + else { + throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data + to analyse.`); + } + } + + private updateTotalFieldStats(statsObject: TotalFieldStats, total: number) { + statsObject.best = total > statsObject.best ? total : statsObject.best; + statsObject.worst = total < statsObject.worst ? total : statsObject.worst; + statsObject.runningTotal += total; + } + + private updateBlockStats(blockId: string) { + 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]; } } -(mongoose.Schema.Types as any).Int = Int; -const TotalFieldStatsSchema = new mongoose.Schema( { - average: {type: Number, 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 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 }); +class RulesetMongoObjectInterface { + private data: Ruleset; + constructor(data: Ruleset) { + this.data = data; + } + getId() { + return this.data.id; + } +} -export const PlayerStats = mongoose.model("PlayerStats", PlayerStatsSchema); -export const AccountStats = mongoose.model("AccountStats", AccountStatsSchema); +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.`); + } + } +} \ No newline at end of file diff --git a/src/models/utils.ts b/src/models/utils.ts index 231178b..bfb1a6d 100644 --- a/src/models/utils.ts +++ b/src/models/utils.ts @@ -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 { + protected mongoDbClientCollection: mongo.Collection; + protected constructor(collectionClient: mongo.Collection) { + this.mongoDbClientCollection = collectionClient; + } + + protected async findObjectById(id: string): Promise { + return this.mongoDbClientCollection!.findOne({_id: id}); + } + + async save(...objects: T[]): Promise { + 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) { super(message); this.name = "GenericModelError";