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

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 {
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<string, RulesetStats>
gamesPlayed: number;
}
interface RulesetStats {
blockStats: Record<string, BlockStats>;
wins: number;
runnerUps: number;
draws: number;
losses: number;
grandTotal: TotalFieldStats;
}
interface BlockStats {
cellStats: Record<string, CellStats>;
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<string, BlockResults>;
}
interface BlockResults {
cells: Record<string, CellResults>
}
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<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";