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
|
||||
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
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> {
|
||||
...
|
||||
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[]> {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
export const PlayerSchema = new mongoose.Schema({
|
||||
nick: { type: String, required: true },
|
||||
stats: { type: PlayerStatsSchema, required: true, default: () => ({}) },
|
||||
}, {...globalSchemaOptions});
|
||||
async findPlayerById(id: string): Promise<StoredPlayer> {
|
||||
const data = await this.findObjectById(id);
|
||||
return new MongoStoredPlayer(data);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
...
|
||||
};
|
||||
|
||||
PlayerSchema.statics.incrementBonus = async function (playerId: string): Promise<void> {
|
||||
...
|
||||
};
|
||||
|
||||
|
||||
const Player = mongoose.model<IPlayerDoc, IPlayerModel>("Player", PlayerSchema);
|
||||
export default Player;
|
||||
export default PlayerCollection;
|
||||
@@ -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;
|
||||
|
||||
class BaseStatsUpdater {
|
||||
private data?: BaseStats;
|
||||
private validationRuleset?: Ruleset;
|
||||
private calculator?: ScoreCalculator;
|
||||
private currentStatsObject?: RulesetStats;
|
||||
constructor() {
|
||||
}
|
||||
|
||||
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;
|
||||
use(data: BaseStats) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
// Mongoose schemata
|
||||
class Int extends mongoose.SchemaType {
|
||||
constructor(key: string, options: any) {
|
||||
super(key, options, 'Int');
|
||||
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);
|
||||
}
|
||||
cast(val: any): number {
|
||||
let _val = Number(val);
|
||||
if (isNaN(_val)) {
|
||||
throw new Error('ZeroPositiveInt: ' + val + ' is not a number');
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
(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 });
|
||||
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;
|
||||
}
|
||||
|
||||
export const PlayerStats = mongoose.model<IPlayerStatsDoc>("PlayerStats", PlayerStatsSchema);
|
||||
export const AccountStats = mongoose.model<IAccountStatsDoc>("AccountStats", AccountStatsSchema);
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
super(message);
|
||||
this.name = "GenericModelError";
|
||||
|
||||
Reference in New Issue
Block a user