Begun writing custom abstraction layers for mongo, removing mongoose.

This commit is contained in:
Daniel Ledda
2020-06-25 20:57:58 +02:00
parent c13ac5f12b
commit 11bf3821a9
8 changed files with 644 additions and 223 deletions

View File

@@ -5,3 +5,5 @@ corresponding methods.
"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.
- Fix the stats logic so that the stats model does most of it

114
src/classes/ScoreBlock.ts Executable file
View 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
View 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
View 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;

View File

@@ -135,11 +135,16 @@ DbUserSchema.statics.getSerializedAuthUser = async function (id: string): Promis
};
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> {
...
return tryQuery(async () => {
const user = await DbUser.findById(id);
user.accountStats.gamesPlayed =
});
};
DbUserSchema.methods.getGuests = async function (this: IDbUser): Promise<IPlayer[]> {

View File

@@ -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<StoredPlayer>;
}
export interface IPlayerModel extends mongoose.Model<IPlayerDoc> {
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<void>;
class MongoStoredPlayerCollection
extends MongoStoredObjectCollection<MongoStoredPlayer>
implements StoredPlayerCollection {
private updater: PlayerStatsUpdater;
constructor(collectionClient: mongo.Collection) {
super(collectionClient);
this.updater = new PlayerStatsUpdater();
}
async findPlayerById(id: string): Promise<StoredPlayer> {
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<void>;
updateStats(results: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): Promise<void>;
}
PlayerSchema.statics.incrementGamesPlayed = async function (playerId: string): Promise<void> {
...
};
class MongoStoredPlayer extends MongoStoredObject implements StoredPlayer {
constructor(data: StoredPlayerData) {
super(data);
}
PlayerSchema.statics.incrementWinFor = async function (playerId: string): Promise<void> {
...
};
nick(): string {
return this.data.nick;
}
PlayerSchema.statics.incrementDrawFor = async function (playerId: string): Promise<void> {
...
};
async setNick(newNick: string): Promise<void> {
this.data.nick = newNick;
}
PlayerSchema.statics.incrementRunnerUpFor = 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.incrementLossFor = async function (playerId: string): Promise<void> {
...
};
PlayerSchema.statics.updateCellStats = async function (playerId: string, cellDetails: CellDetails): void {
...
};
PlayerSchema.statics.incrementBonus = async function (playerId: string): Promise<void> {
...
};
const Player = mongoose.model<IPlayerDoc, IPlayerModel>("Player", PlayerSchema);
export default Player;
export default PlayerCollection;

View File

@@ -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 {
class UpdateError extends Error {
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;
runnerUps: number;
draws: number;
losses: number;
grandTotal: TotalFieldStats;
}
export interface IAccountStats extends IBaseStats {
timesNoWinner: number;
interface BlockStats {
cellStats: Record<string, CellStats>;
timesHadBonus?: number;
total: TotalFieldStats;
}
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 BaseCellStats {
runningTotal: number;
}
interface ITotalFieldStats {
average: number;
interface StrikeableFieldStats extends BaseCellStats {
timesStruck: number;
}
interface BestableFieldStats extends BaseCellStats {
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;
type TotalFieldStats = BestableFieldStats;
type BoolFieldStats = StrikeableFieldStats & { total: number };
type NumberFieldStats = StrikeableFieldStats & BestableFieldStats;
type MultiplierFieldStats = NumberFieldStats;
type SuperkadiFieldStats = NumberFieldStats;
type CellStats = BoolFieldStats | NumberFieldStats | MultiplierFieldStats | SuperkadiFieldStats;
// Mongoose doc interfaces and types
export interface IPlayerStatsDoc extends IBaseStatsDoc {
wins: number;
runnerUps: number;
draws: number;
losses: number;
export interface PlayerGameResults {
blocks: Record<string, BlockResults>;
}
export interface IAccountStatsDoc extends IBaseStatsDoc {
timesNoWinner: number;
interface BlockResults {
cells: Record<string, CellResults>
}
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;
interface CellResults {
value: CellValue;
}
type CellValue = "cellFlagStrike" | number | boolean;
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 BaseStatsUpdater {
private data?: BaseStats;
private validationRuleset?: Ruleset;
private calculator?: ScoreCalculator;
private currentStatsObject?: RulesetStats;
constructor() {
}
cast(val: any): number {
let _val = Number(val);
if (isNaN(_val)) {
throw new Error('ZeroPositiveInt: ' + val + ' is not a number');
use(data: BaseStats) {
this.data = data;
}
_val = Math.round(_val);
return _val;
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");
}
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<IPlayerStatsDoc>("PlayerStats", PlayerStatsSchema);
export const AccountStats = mongoose.model<IAccountStatsDoc>("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.`);
}
}
}

View File

@@ -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) {
super(message);
this.name = "GenericModelError";