This commit is contained in:
Daniel Ledda
2020-12-24 12:38:25 +01:00
parent 5f747142e5
commit 1e1d31edba
13 changed files with 316 additions and 191 deletions

View File

@@ -43,6 +43,7 @@
},
"prettier": {
"tabWidth": 4,
"jsxBracketSameLine": true
"jsxBracketSameLine": true,
"printWidth": 100
}
}

View File

@@ -3,5 +3,25 @@ import RulesetCollection from "../ObjectCollections/RulesetCollection";
export const getRuleset: RequestHandler = async (req, res) => {
const ruleset = await RulesetCollection().read(req.params.id);
res.json(ruleset.getSchemaJSON());
res.json(ruleset.getSchema());
};
export const addRuleset: RequestHandler = async (req, res) => {
const submission = req.body;
if (validateRulesetSchema(submission)) {
const savedRuleset = await RulesetCollection().create(submission);
res.send({result: "success", newRuleset: savedRuleset})
}
else {
res.status(400).send({result: "failure", message: "Invalid ruleset submission format."});
}
};
export const getAllRulesets: RequestHandler = async (req, res) => {
const rulesets = await RulesetCollection().getAllRulesets();
res.send({rulesets: rulesets.map(ruleset => ruleset.getSchema())});
};
function validateRulesetSchema(object: any): boolean {
return true;
}

View File

@@ -17,7 +17,7 @@ interface GameSubmission {
export interface ProcessedGameSubmission {
ruleset: string;
players: {id: string, nick: string}[];
players: { id: string; nick: string }[];
results: ScoredResultsWithOutcome[];
}
@@ -32,16 +32,18 @@ enum ResultType {
loser,
}
type ScoredResults = {score: number, results: PlayerGameResult};
export type ScoredResultsWithOutcome = {score: number, results: PlayerGameResult & {outcome: OutcomeType}};
type ScoredResults = { score: number; results: PlayerGameResult };
export type ScoredResultsWithOutcome = {
score: number;
results: PlayerGameResult & { outcome: OutcomeType };
};
export const listGames: RequestHandler = async (req, res) => {
const user = req.user as KadiUser;
const gamesList = await KadiUserCollection().getSavedGamesForUser(user);
if (gamesList) {
res.json({ games: gamesList });
}
else {
} else {
res.sendStatus(404);
}
};
@@ -55,7 +57,10 @@ export const saveGame: RequestHandler = async (req, res) => {
}
const rulesetUsed = await RulesetCollection().read(submission.ruleset);
const scoredResultsWithOutcomes = await processStats(rulesetUsed, submission.results, user);
const newGame = await KadiUserCollection().addGameForUser(user, {...submission, results: scoredResultsWithOutcomes});
const newGame = await KadiUserCollection().addGameForUser(user, {
...submission,
results: scoredResultsWithOutcomes,
});
res.send({ message: "Game submitted successfully!", newGame: newGame });
};
@@ -64,11 +69,13 @@ export const getStats: RequestHandler = async (req, res) => {
const stats = await KadiUserCollection().getAllStatsForUser(user);
if (stats) {
res.json({
pStats: stats.pStats.map(pStats => ({...pStats, stats: pStats.stats.getData()})),
accStats: stats.accStats.getData()
pStats: stats.pStats.map((pStats) => ({
...pStats,
stats: pStats.stats.getData(),
})),
accStats: stats.accStats.getData(),
});
}
else {
} else {
res.sendStatus(404);
}
};
@@ -78,20 +85,30 @@ async function addNewGuests(submission: GameSubmission, user: KadiUser): Promise
for (const playerDetails of submission.players) {
const isNewPlayer = playerDetails.id === playerDetails.nick;
if (isNewPlayer) {
const newGuest: Player = await KadiUserCollection().addGuestForUser(user, playerDetails.nick);
const newGuest: Player = await KadiUserCollection().addGuestForUser(
user,
playerDetails.nick
);
newGuestIds.push(newGuest);
}
}
return newGuestIds;
}
function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: Player[]): GameSubmission {
function fillOutSubmissionWithNewIds(
submission: GameSubmission,
newGuestList: Player[]
): GameSubmission {
for (const newGuest of newGuestList) {
const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.getNick());
const gameResultsFromNewGuest = submission.results.find(
(result) => result.playerId === newGuest.getNick()
);
if (gameResultsFromNewGuest) {
gameResultsFromNewGuest.playerId = newGuest.getId().toString();
}
const playerEntryForNewGuest = submission.players.find((player) => player.id === newGuest.getNick());
const playerEntryForNewGuest = submission.players.find(
(player) => player.id === newGuest.getNick()
);
if (playerEntryForNewGuest) {
playerEntryForNewGuest.id = newGuest.getId().toString();
}
@@ -99,35 +116,45 @@ function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: P
return submission;
}
async function processStats(ruleset: Ruleset, results: PlayerGameResult[], account: KadiUser): Promise<ScoredResultsWithOutcome[]> {
async function processStats(
ruleset: Ruleset,
results: PlayerGameResult[],
account: KadiUser
): Promise<ScoredResultsWithOutcome[]> {
const calc = new ScoreCalculator(ruleset);
let playerScoreList: ScoredResults[] = [];
for (const result of results) {
calc.hydrateWithJSON(result);
playerScoreList.push({
score: calc.getTotal(),
results: result
results: result,
});
}
const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList);
await updateStatsForIndividualPlayers(playerScoreListWithOutcomes, ruleset);
const gameResults = playerScoreListWithOutcomes.map(scoredResults => scoredResults.results);
const gameResults = playerScoreListWithOutcomes.map((scoredResults) => scoredResults.results);
await KadiUserCollection().updateAccountStats(account.getId(), gameResults, ruleset);
return playerScoreListWithOutcomes;
}
function updateScoreListWithOutcomes(scoreResultsList: ScoredResults[]): ScoredResultsWithOutcome[] {
function updateScoreListWithOutcomes(
scoreResultsList: ScoredResults[]
): ScoredResultsWithOutcome[] {
scoreResultsList = sortDescendingByScore(scoreResultsList);
const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = scoreResultsList.map(scoredResults => {
const newResults = {...scoredResults.results, outcome: OutcomeType.loss};
const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = scoreResultsList.map(
(scoredResults) => {
const newResults = {
...scoredResults.results,
outcome: OutcomeType.loss,
};
return { ...scoredResults, results: newResults };
});
}
);
let runnerUpsStart: number;
if (playerScoreListWithOutcomes[0].score !== playerScoreListWithOutcomes[1].score) {
playerScoreListWithOutcomes[0].results.outcome = OutcomeType.win;
runnerUpsStart = 1;
}
else {
} else {
runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes);
}
const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes, runnerUpsStart);
@@ -139,28 +166,32 @@ function updateScoreListWithDraws(scoreResultsList: ScoredResultsWithOutcome[]):
for (let i = 0; i < scoreResultsList.length; i++) {
if (scoreResultsList[i].score === scoreResultsList[0].score) {
scoreResultsList[i].results.outcome = OutcomeType.draw;
}
else {
} else {
return i;
}
}
return scoreResultsList.length;
}
function updateScoreListWithRunnerUps(scoreResultsList: ScoredResultsWithOutcome[], runnerUpsStartIndex: number): number {
function updateScoreListWithRunnerUps(
scoreResultsList: ScoredResultsWithOutcome[],
runnerUpsStartIndex: number
): number {
scoreResultsList[runnerUpsStartIndex].results.outcome = OutcomeType.runnerUp;
for (let i = runnerUpsStartIndex + 1; i < scoreResultsList.length; i++) {
if (scoreResultsList[i].score === scoreResultsList[i - 1].score) {
scoreResultsList[i].results.outcome = OutcomeType.runnerUp;
}
else {
} else {
return i;
}
}
return scoreResultsList.length;
}
function updateScoreListWithLosses(scoreResultsList: ScoredResultsWithOutcome[], losersStartIndex: number) {
function updateScoreListWithLosses(
scoreResultsList: ScoredResultsWithOutcome[],
losersStartIndex: number
) {
for (let i = losersStartIndex; i < scoreResultsList.length; i++) {
scoreResultsList[i].results.outcome = OutcomeType.loss;
}
@@ -170,11 +201,18 @@ function sortDescendingByScore(playerScoreList: ScoredResults[]) {
return playerScoreList.sort((a, b) => b.score - a.score);
}
async function updateStatsForIndividualPlayers(playerScoreListWithOutcomes: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): Promise<void> {
async function updateStatsForIndividualPlayers(
playerScoreListWithOutcomes: ScoredResultsWithOutcome[],
rulesetUsed: Ruleset
): Promise<void> {
for (const scoredResults of playerScoreListWithOutcomes) {
await PlayerCollection().updateStatsForPlayer(
scoredResults.results.playerId,
{...scoredResults.results, outcome: scoredResults.results.outcome},
rulesetUsed);
{
...scoredResults.results,
outcome: scoredResults.results.outcome,
},
rulesetUsed
);
}
}

View File

@@ -1,7 +1,12 @@
import { CredentialsTakenError } from "../errors";
import bcrypt from "bcrypt";
import { SupportedLang } from "../enums";
import {AccountStatsMongoData, defaultAccountStatsMongoData, OutcomeType, PlayerGameResults} from "../Objects/DefaultStatsMongoData";
import {
AccountStatsMongoData,
defaultAccountStatsMongoData,
OutcomeType,
PlayerGameResults,
} from "../Objects/DefaultStatsMongoData";
import { getMongoObjectCollection } from "../database";
import KadiUser, { LoginDetails } from "../Objects/KadiUser";
import { ActiveRecordId, OrId } from "../Objects/ActiveRecord";
@@ -29,6 +34,15 @@ export interface KadiUserMongoData {
savedGames: ActiveRecordId[];
}
export interface StatsListing {
accStats: AccountStats;
pStats: {
nick: string;
playerId: ActiveRecordId;
stats: PlayerStats;
}[];
}
class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> {
private static instance?: KadiUserCollection;
private constructor() {
@@ -43,16 +57,11 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
}
async init() {
this.mongoDbClientCollection = getMongoObjectCollection("users")
this.mongoDbClientCollection = getMongoObjectCollection("users");
}
private kadiUserFrom(data: KadiUserMongoData): KadiUser {
return new KadiUser(
data.id,
data.username,
data.email,
data.password,
data.lang);
return new KadiUser(data.id, data.username, data.email, data.password, data.lang);
}
async read(id: string): Promise<KadiUser> {
@@ -64,8 +73,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
const foundUser = await this.mongoFindByAttribute("email", emailQuery);
if (foundUser) {
return this.kadiUserFrom(foundUser);
}
else {
} else {
return null;
}
}
@@ -75,9 +83,8 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
const emailTaken = await this.userWithEmailExists(loginDetails.email);
if (usernameTaken || emailTaken) {
throw new CredentialsTakenError(usernameTaken, emailTaken);
}
else {
return this.addNewUser({...loginDetails})
} else {
return this.addNewUser({ ...loginDetails });
}
}
@@ -116,15 +123,20 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
const newGuest = await PlayerCollection().create(newGuestNick);
await this.mongoDbClientCollection!.findOneAndUpdate(
{ _id: this.idFromRecordOrId(userOrId) },
{$push: {guests: newGuest.getId()}});
{ $push: { guests: newGuest.getId() } }
);
return newGuest;
}
async deleteGuestFromUser(userOrId: OrId<KadiUser>, guestOrGuestId: OrId<Player>): Promise<Player> {
async deleteGuestFromUser(
userOrId: OrId<KadiUser>,
guestOrGuestId: OrId<Player>
): Promise<Player> {
const deletedGuest = await PlayerCollection().delete(this.idFromRecordOrId(guestOrGuestId));
await this.mongoDbClientCollection!.findOneAndUpdate(
{ _id: this.idFromRecordOrId(userOrId) },
{$pull: {guests: this.idFromRecordOrId(deletedGuest)}});
{ $pull: { guests: this.idFromRecordOrId(deletedGuest) } }
);
return deletedGuest;
}
@@ -133,8 +145,8 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
return Promise.all<Player>(
guestIdList.map(async (guestId) => {
return await PlayerCollection().read(guestId);
}
));
})
);
}
async getMainPlayerForUser(userOrId: OrId<KadiUser>): Promise<Player> {
@@ -147,50 +159,56 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
return Promise.all<SavedGame>(
savedGameIds.map(async (savedGameId) => {
return await SavedGameCollection().read(savedGameId);
}
));
})
);
}
async addGameForUser(userOrId: OrId<KadiUser>, submission: ProcessedGameSubmission): Promise<SavedGame> {
async addGameForUser(
userOrId: OrId<KadiUser>,
submission: ProcessedGameSubmission
): Promise<SavedGame> {
const newGame = await SavedGameCollection().create(submission);
await this.mongoDbClientCollection!.findOneAndUpdate(
{ _id: this.idFromRecordOrId(userOrId) },
{$push: {savedGames: newGame.getId()}});
{ $push: { savedGames: newGame.getId() } }
);
return newGame;
}
async updateAccountStats(userOrId: OrId<KadiUser>, gameResults: (PlayerGameResults & {outcome: OutcomeType})[], rulesetUsedOrId: OrId<Ruleset>): Promise<void> {
async updateAccountStats(
userOrId: OrId<KadiUser>,
gameResults: (PlayerGameResults & { outcome: OutcomeType })[],
rulesetUsedOrId: OrId<Ruleset>
): Promise<void> {
const userId = this.idFromRecordOrId(userOrId);
const rulesetUsed = rulesetUsedOrId instanceof Ruleset ? rulesetUsedOrId : await RulesetCollection().read(rulesetUsedOrId);
const rulesetUsed =
rulesetUsedOrId instanceof Ruleset
? rulesetUsedOrId
: await RulesetCollection().read(rulesetUsedOrId);
const accountStatsMongoData = (await this.mongoRead(userId)).accountStats;
const accountStatsObject = new AccountStats(accountStatsMongoData);
accountStatsObject.updateStats(gameResults, rulesetUsed);
this.mongoUpdate({
id: userId,
accountStats: accountStatsObject.getData()
accountStats: accountStatsObject.getData(),
});
}
async getAllStatsForUser(userOrId: OrId<KadiUser>): Promise<StatsListing> {
const players = [...(await this.getAllGuestsForUser(userOrId)), await this.getMainPlayerForUser(userOrId)];
const playerStats = players.map(player => ({
const players = [
...(await this.getAllGuestsForUser(userOrId)),
await this.getMainPlayerForUser(userOrId),
];
const playerStats = players.map((player) => ({
nick: player.getNick(),
playerId: player.getId(),
stats: player.getStats()
stats: player.getStats(),
}));
const accountStatsMongoData = (await this.mongoRead(this.idFromRecordOrId(userOrId))).accountStats;
const accountStatsMongoData = (await this.mongoRead(this.idFromRecordOrId(userOrId)))
.accountStats;
const accountStats = new AccountStats(accountStatsMongoData);
return { pStats: playerStats, accStats: accountStats };
}
}
export interface StatsListing {
accStats: AccountStats;
pStats: {
nick: string,
playerId: ActiveRecordId,
stats: PlayerStats
}[];
}
export default KadiUserCollection.getInstance;

View File

@@ -2,6 +2,7 @@ import mongo from "mongodb";
import {tryQuery} from "../database";
import {InvalidIdError, MongoError} from "../errors";
import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord";
import {FilterQuery} from "mongoose";
abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}> {
@@ -37,6 +38,22 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
});
}
protected async mongoFindBy(query: FilterQuery<any>): Promise<IRawData[]> {
return tryQuery(async () => {
const results = await this.mongoDbClientCollection!.find(query).toArray();
if (results.length > 0) {
results.forEach(result => {
result.id = result._id;
result._id = undefined;
});
return results;
}
else {
return [];
}
});
}
protected async mongoFindByAttribute(attribute: string, value: any): Promise<IRawData | null> {
return tryQuery(async () => {
const result = await this.mongoDbClientCollection!.findOne({[attribute]: value});

View File

@@ -68,5 +68,4 @@ class PlayerCollection extends MongoStoredObjectCollection<PlayerMongoData> {
}
}
export default PlayerCollection.getInstance;

View File

@@ -1,5 +1,9 @@
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
import {DEFAULT_RULESET, DEFAULT_RULESET_NAME, RulesetSchema} from "../rulesets";
import {
DEFAULT_RULESET,
DEFAULT_RULESET_NAME,
RulesetSchema,
} from "../rulesets";
import { getMongoObjectCollection } from "../database";
import Ruleset from "../Objects/Ruleset";
import { ActiveRecordId } from "../Objects/ActiveRecord";
@@ -23,19 +27,33 @@ class RulesetCollection extends MongoStoredObjectCollection<RulesetMongoData> {
this.mongoDbClientCollection = getMongoObjectCollection("rulesets");
}
private async rulesetFrom(data: RulesetMongoData): Promise<Ruleset> {
private rulesetFrom(data: RulesetMongoData): Ruleset {
return new Ruleset(data.id, data);
}
async create(rulesetSchema: RulesetSchema): Promise<Ruleset> {
const newRuleset = await this.mongoCreate(rulesetSchema);
return this.rulesetFrom(newRuleset);
}
async read(id: ActiveRecordId): Promise<Ruleset> {
if (id === DEFAULT_RULESET_NAME) {
return new Ruleset(DEFAULT_RULESET_NAME, DEFAULT_RULESET);
}
else {
} else {
const foundRuleset = await this.mongoRead(id);
return this.rulesetFrom(foundRuleset);
}
}
async getAllRulesets(): Promise<Ruleset[]> {
const rulesets = await this.mongoFindBy({});
return [
...rulesets.map((ruleset) =>
this.rulesetFrom(ruleset as RulesetMongoData)
),
await this.read(DEFAULT_RULESET_NAME),
];
}
}
export default RulesetCollection.getInstance;

View File

@@ -1,35 +1,22 @@
import { RulesetSchema } from "../rulesets";
import { AccountStatsMongoData, OutcomeType, PlayerGameResults } from "./DefaultStatsMongoData";
import { UpdateError } from "../errors";
import StatsUpdater from "./StatsUpdater";
import StatsUpdater, { PlayerGameResultsWithOutcomes } from "./StatsUpdater";
import Ruleset from "./Ruleset";
class AccountStats {
private data: AccountStatsMongoData;
private readonly updater: StatsUpdater;
private readonly data: AccountStatsMongoData;
constructor(data: AccountStatsMongoData) {
this.data = data;
this.updater = new StatsUpdater();
this.updater.use(data);
}
use(data: AccountStatsMongoData) {
this.data = data;
this.updater.use(data);
}
updateStats(playerGameResults: (PlayerGameResults & {outcome: OutcomeType})[], ruleset: Ruleset): void {
if (this.data) {
updateStats(playerGameResults: PlayerGameResultsWithOutcomes[], ruleset: Ruleset): void {
const updater = new StatsUpdater(this.data, ruleset);
for (const playerGameResult of playerGameResults) {
this.updater.updateStats(playerGameResult, ruleset);
updater.updateStats(playerGameResult);
}
this.data.gamesPlayed += 1;
}
else {
throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data
to analyse.`);
}
}
getData(): AccountStatsMongoData {
return this.data;

View File

@@ -22,9 +22,14 @@ export interface RulesetStatsMongoData {
losses: number;
grandTotal: TotalFieldStatsMongoData;
}
export interface BlockStatsMongoData {
export type BlockStatsMongoData = BonusBlockStatsMongoData | NoBonusBlockStatsMongoData;
export interface BonusBlockStatsMongoData {
cellStats: Record<string, CellStatsMongoData>;
timesHadBonus: number;
total: TotalFieldStatsMongoData;
}
export interface NoBonusBlockStatsMongoData {
cellStats: Record<string, CellStatsMongoData>;
timesHadBonus?: number;
total: TotalFieldStatsMongoData;
}
export interface BaseCellStatsMongoData {
@@ -133,7 +138,7 @@ function defaultBlockStatsMongoData(cellSchemas: Record<string, CellDef>, hasBon
}
}
function defaultRulesetStatsMongoData(ruleset: RulesetSchema): RulesetStatsMongoData {
export function defaultRulesetStatsMongoData(ruleset: RulesetSchema): RulesetStatsMongoData {
const blockStatsRecord: Record<string, BlockStatsMongoData> = {};
for (const blockLabel in ruleset.blocks) {
blockStatsRecord[blockLabel] = defaultBlockStatsMongoData(ruleset.blocks[blockLabel].cells, ruleset.blocks[blockLabel].hasBonus);

View File

@@ -1,24 +1,16 @@
import {RulesetSchema} from "../rulesets";
import {OutcomeType, PlayerGameResults, PlayerStatsMongoData} from "./DefaultStatsMongoData";
import StatsUpdater from "./StatsUpdater";
import {PlayerStatsMongoData} from "./DefaultStatsMongoData";
import StatsUpdater, {PlayerGameResultsWithOutcomes} from "./StatsUpdater";
import Ruleset from "./Ruleset";
class PlayerStats {
private data: PlayerStatsMongoData;
private readonly updater: StatsUpdater;
private readonly data: PlayerStatsMongoData;
constructor(data: PlayerStatsMongoData) {
this.data = data;
this.updater = new StatsUpdater();
this.updater.use(data);
}
use(data: PlayerStatsMongoData) {
this.data = data;
this.updater.use(data);
}
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
this.updater.updateStats(playerGameResults, ruleset);
updateStats(playerGameResults: PlayerGameResultsWithOutcomes, ruleset: Ruleset): void {
const updater = new StatsUpdater(this.data, ruleset);
updater.updateStats(playerGameResults);
this.data.gamesPlayed += 1;
}

View File

@@ -1,5 +1,7 @@
import {BlockDef, CellDef, RulesetSchema} from "../rulesets";
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
import {FieldType} from "../enums";
import {CellLocation} from "./ScoreCalculator";
export class Ruleset implements ActiveRecord {
constructor(
@@ -27,9 +29,13 @@ export class Ruleset implements ActiveRecord {
return Object.assign({}, this.schema.blocks[blockId].cells);
}
getSchemaJSON(): RulesetSchema {
getSchema(): RulesetSchema {
return Object.assign({}, this.schema);
}
getCellFieldTypeByLocation(cellLocation: CellLocation): FieldType {
return this.getCellsInBlock(cellLocation.blockId)[cellLocation.cellId].fieldType;
}
}
export default Ruleset;

View File

@@ -1,49 +1,59 @@
import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator";
import ScoreCalculator, { CellLocation, ScoreCardJSONRepresentation } from "./ScoreCalculator";
import { UpdateError } from "../errors";
import { FieldType } from "../enums";
import {
BaseCellStatsMongoData,
BaseStatsMongoData,
BestableFieldStatsMongoData,
BlockStatsMongoData,
BonusBlockStatsMongoData,
BoolFieldStatsMongoData,
CellStatsMongoData,
defaultRulesetStatsMongoData,
OutcomeType,
PlayerGameResults,
RulesetStatsMongoData,
TotalFieldStatsMongoData
TotalFieldStatsMongoData,
} from "./DefaultStatsMongoData";
import Ruleset from "./Ruleset";
import { BonusBlockDef } from "../rulesets";
export type PlayerGameResultsWithOutcomes = PlayerGameResults & { outcome: OutcomeType };
class StatsUpdater {
private data?: BaseStatsMongoData;
private validationRuleset?: Ruleset;
private calculator?: ScoreCalculator;
private currentStatsObject?: RulesetStatsMongoData;
constructor() {
private data: BaseStatsMongoData;
private validationRuleset: Ruleset;
private calculator: ScoreCalculator;
private statsByRuleset: RulesetStatsMongoData;
constructor(data: BaseStatsMongoData, validationRuleset: Ruleset) {
this.data = data;
this.validationRuleset = validationRuleset;
this.calculator = new ScoreCalculator(validationRuleset);
this.statsByRuleset = this.getStatsByRuleset();
}
private getStatsByRuleset(): RulesetStatsMongoData {
if (!this.data.statsByRuleset[this.validationRuleset.getId().toString()]) {
this.data.statsByRuleset[
this.validationRuleset.getId().toString()
] = defaultRulesetStatsMongoData(this.validationRuleset.getSchema());
}
return this.data.statsByRuleset[this.validationRuleset.getId().toString()];
}
use(data: BaseStatsMongoData) {
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.getId().toString()];
for (const blockId in ruleset.getBlocks()) {
updateStats(playerGameResults: PlayerGameResultsWithOutcomes): void {
for (const blockId in this.validationRuleset.getBlocks()) {
this.updateBlockStats(blockId);
}
this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal());
this.currentStatsObject.wins += Number(playerGameResults.outcome === OutcomeType.win);
this.currentStatsObject.draws += Number(playerGameResults.outcome === OutcomeType.draw);
this.currentStatsObject.runnerUps += Number(playerGameResults.outcome === OutcomeType.runnerUp);
this.currentStatsObject.losses += Number(playerGameResults.outcome === OutcomeType.loss);
}
else {
throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data
to analyse.`);
}
this.updateTotalFieldStats(this.statsByRuleset.grandTotal, this.calculator.getTotal());
this.statsByRuleset.wins += Number(playerGameResults.outcome === OutcomeType.win);
this.statsByRuleset.draws += Number(playerGameResults.outcome === OutcomeType.draw);
this.statsByRuleset.runnerUps += Number(playerGameResults.outcome === OutcomeType.runnerUp);
this.statsByRuleset.losses += Number(playerGameResults.outcome === OutcomeType.loss);
}
private updateTotalFieldStats(statsObject: TotalFieldStatsMongoData, total: number) {
@@ -53,42 +63,54 @@ class StatsUpdater {
}
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!.getBlocks()[blockId].cells) {
this.updateCellStatsByIds({cellId, blockId});
const blockStats = this.statsByRuleset.blockStats[blockId];
this.updateTotalFieldStats(blockStats.total, this.calculator.getBlockSubTotalById(blockId));
if (this.isBonusBlockStats(blockStats)) {
blockStats.timesHadBonus += 1;
}
for (const cellId in this.validationRuleset.getBlocks()[blockId].cells) {
this.updateCellStatsByLocation({ cellId, blockId });
}
}
private updateCellStatsByIds(ids: {cellId: string, blockId: string}) {
const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.getId().toString()});
const cellFieldType = this.validationRuleset?.getBlocks()[ids.blockId].cells[ids.cellId].fieldType;
const cellScore = this.calculator!.getCellScoreByLocation({...ids});
private updateCellStatsByLocation(location: CellLocation) {
const cellStats = this.getCellStatsByLocation(location);
const cellFieldType = this.validationRuleset.getCellFieldTypeByLocation(location);
const cellScore = this.calculator.getCellScoreByLocation(location);
cellStats.runningTotal += cellScore;
if (cellScore > 0 && cellFieldType === FieldType.bool) {
(cellStats as BoolFieldStatsMongoData).total += 1;
}
else if (cellFieldType === FieldType.multiplier || FieldType.superkadi || FieldType.number) {
const bestableStats = (cellStats as BestableFieldStatsMongoData);
if (bestableStats.best < cellScore) {
bestableStats.best = cellScore;
}
else if (bestableStats.worst > cellScore) {
bestableStats.worst = cellScore
} else if (this.isBestableCell(cellStats, cellFieldType)) {
if (cellStats.best < cellScore) {
cellStats.best = cellScore;
} else if (cellStats.worst > cellScore) {
cellStats.worst = cellScore;
}
}
if (this.calculator!.cellAtLocationIsStruck({...ids})) {
if (this.calculator.cellAtLocationIsStruck({ ...location })) {
cellStats.timesStruck += 1;
}
}
private getCellStatsByIds(ids: {cellId: string, blockId: string, rulesetId: string}): CellStatsMongoData {
return this.data!.statsByRuleset[ids.rulesetId].blockStats[ids.blockId].cellStats[ids.cellId];
private getCellStatsByLocation(location: CellLocation): CellStatsMongoData {
return this.statsByRuleset.blockStats[location.blockId].cellStats[location.cellId];
}
private isBonusBlockStats(
blockStats: BlockStatsMongoData
): blockStats is BonusBlockStatsMongoData {
return blockStats.hasOwnProperty("timesHadBonus");
}
private isBestableCell(
cellStats: BaseCellStatsMongoData,
fieldType: FieldType
): cellStats is BestableFieldStatsMongoData {
return (
fieldType === FieldType.multiplier ||
fieldType === FieldType.superkadi ||
fieldType === FieldType.number
);
}
}

View File

@@ -25,5 +25,7 @@ router.post("/games", requireAuthenticated, statsController.saveGame);
//Stats
router.get("/stats", requireAuthenticated, statsController.getStats);
router.get("/ruleset/:id", rulesetController.getRuleset);
router.post("/ruleset/", requireAuthenticated, rulesetController.addRuleset);
router.get("/rulesets/", requireAuthenticated, rulesetController.getAllRulesets);
export default router;