diff --git a/package.json b/package.json index 20f1613..f8d74c8 100755 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ }, "prettier": { "tabWidth": 4, - "jsxBracketSameLine": true + "jsxBracketSameLine": true, + "printWidth": 100 } } diff --git a/src/Controllers/rulesetController.ts b/src/Controllers/rulesetController.ts index e6aa69f..0527a2c 100644 --- a/src/Controllers/rulesetController.ts +++ b/src/Controllers/rulesetController.ts @@ -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()); -}; \ No newline at end of file + 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; +} \ No newline at end of file diff --git a/src/Controllers/statsController.ts b/src/Controllers/statsController.ts index 3cb8c55..47a9dd8 100755 --- a/src/Controllers/statsController.ts +++ b/src/Controllers/statsController.ts @@ -1,13 +1,13 @@ import KadiUserCollection from "../ObjectCollections/KadiUserCollection"; import PlayerCollection from "../ObjectCollections/PlayerCollection"; import Player from "../Objects/Player"; -import {RequestHandler} from "express"; +import { RequestHandler } from "express"; import ScoreCalculator from "../Objects/ScoreCalculator"; import KadiUser from "../Objects/KadiUser"; -import {CellFlag} from "../enums"; +import { CellFlag } from "../enums"; import RulesetCollection from "../ObjectCollections/RulesetCollection"; import Ruleset from "../Objects/Ruleset"; -import {OutcomeType} from "../Objects/DefaultStatsMongoData"; +import { OutcomeType } from "../Objects/DefaultStatsMongoData"; interface GameSubmission { ruleset: string; @@ -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 { +async function processStats( + ruleset: Ruleset, + results: PlayerGameResult[], + account: KadiUser +): Promise { 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}; - return {...scoredResults, results: newResults}; - }); + 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 { +async function updateStatsForIndividualPlayers( + playerScoreListWithOutcomes: ScoredResultsWithOutcome[], + rulesetUsed: Ruleset +): Promise { 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 + ); } } \ No newline at end of file diff --git a/src/ObjectCollections/KadiUserCollection.ts b/src/ObjectCollections/KadiUserCollection.ts index 6b57122..6b88e54 100644 --- a/src/ObjectCollections/KadiUserCollection.ts +++ b/src/ObjectCollections/KadiUserCollection.ts @@ -1,16 +1,21 @@ -import {CredentialsTakenError} from "../errors"; +import { CredentialsTakenError } from "../errors"; import bcrypt from "bcrypt"; -import {SupportedLang} from "../enums"; -import {AccountStatsMongoData, defaultAccountStatsMongoData, OutcomeType, PlayerGameResults} from "../Objects/DefaultStatsMongoData"; -import {getMongoObjectCollection} from "../database"; -import KadiUser, {LoginDetails} from "../Objects/KadiUser"; -import {ActiveRecordId, OrId} from "../Objects/ActiveRecord"; +import { SupportedLang } from "../enums"; +import { + AccountStatsMongoData, + defaultAccountStatsMongoData, + OutcomeType, + PlayerGameResults, +} from "../Objects/DefaultStatsMongoData"; +import { getMongoObjectCollection } from "../database"; +import KadiUser, { LoginDetails } from "../Objects/KadiUser"; +import { ActiveRecordId, OrId } from "../Objects/ActiveRecord"; import MongoStoredObjectCollection from "./MongoStoredObjectCollection"; import SavedGameCollection from "./SavedGameCollection"; import SavedGame from "../Objects/SavedGame"; import PlayerCollection from "../ObjectCollections/PlayerCollection"; import Player from "../Objects/Player"; -import {ProcessedGameSubmission} from "../Controllers/statsController"; +import { ProcessedGameSubmission } from "../Controllers/statsController"; import Ruleset from "../Objects/Ruleset"; import RulesetCollection from "./RulesetCollection"; import AccountStats from "../Objects/AccountStats"; @@ -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 { private static instance?: KadiUserCollection; private constructor() { @@ -43,16 +57,11 @@ class KadiUserCollection extends MongoStoredObjectCollection } 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 { @@ -64,8 +73,7 @@ class KadiUserCollection extends MongoStoredObjectCollection 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 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 }); } } @@ -115,16 +122,21 @@ class KadiUserCollection extends MongoStoredObjectCollection async addGuestForUser(userOrId: OrId, newGuestNick: string): Promise { const newGuest = await PlayerCollection().create(newGuestNick); await this.mongoDbClientCollection!.findOneAndUpdate( - {_id: this.idFromRecordOrId(userOrId)}, - {$push: {guests: newGuest.getId()}}); + { _id: this.idFromRecordOrId(userOrId) }, + { $push: { guests: newGuest.getId() } } + ); return newGuest; } - async deleteGuestFromUser(userOrId: OrId, guestOrGuestId: OrId): Promise { + async deleteGuestFromUser( + userOrId: OrId, + guestOrGuestId: OrId + ): Promise { const deletedGuest = await PlayerCollection().delete(this.idFromRecordOrId(guestOrGuestId)); await this.mongoDbClientCollection!.findOneAndUpdate( - {_id: this.idFromRecordOrId(userOrId)}, - {$pull: {guests: this.idFromRecordOrId(deletedGuest)}}); + { _id: this.idFromRecordOrId(userOrId) }, + { $pull: { guests: this.idFromRecordOrId(deletedGuest) } } + ); return deletedGuest; } @@ -133,8 +145,8 @@ class KadiUserCollection extends MongoStoredObjectCollection return Promise.all( guestIdList.map(async (guestId) => { return await PlayerCollection().read(guestId); - } - )); + }) + ); } async getMainPlayerForUser(userOrId: OrId): Promise { @@ -147,50 +159,56 @@ class KadiUserCollection extends MongoStoredObjectCollection return Promise.all( savedGameIds.map(async (savedGameId) => { return await SavedGameCollection().read(savedGameId); - } - )); + }) + ); } - async addGameForUser(userOrId: OrId, submission: ProcessedGameSubmission): Promise { + async addGameForUser( + userOrId: OrId, + submission: ProcessedGameSubmission + ): Promise { const newGame = await SavedGameCollection().create(submission); await this.mongoDbClientCollection!.findOneAndUpdate( - {_id: this.idFromRecordOrId(userOrId)}, - {$push: {savedGames: newGame.getId()}}); + { _id: this.idFromRecordOrId(userOrId) }, + { $push: { savedGames: newGame.getId() } } + ); return newGame; } - async updateAccountStats(userOrId: OrId, gameResults: (PlayerGameResults & {outcome: OutcomeType})[], rulesetUsedOrId: OrId): Promise { + async updateAccountStats( + userOrId: OrId, + gameResults: (PlayerGameResults & { outcome: OutcomeType })[], + rulesetUsedOrId: OrId + ): Promise { 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): Promise { - 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}; + return { pStats: playerStats, accStats: accountStats }; } } -export interface StatsListing { - accStats: AccountStats; - pStats: { - nick: string, - playerId: ActiveRecordId, - stats: PlayerStats - }[]; -} - -export default KadiUserCollection.getInstance; \ No newline at end of file +export default KadiUserCollection.getInstance; diff --git a/src/ObjectCollections/MongoStoredObjectCollection.ts b/src/ObjectCollections/MongoStoredObjectCollection.ts index a474093..bd5b800 100644 --- a/src/ObjectCollections/MongoStoredObjectCollection.ts +++ b/src/ObjectCollections/MongoStoredObjectCollection.ts @@ -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 { @@ -37,6 +38,22 @@ abstract class MongoStoredObjectCollection): Promise { + 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 { return tryQuery(async () => { const result = await this.mongoDbClientCollection!.findOne({[attribute]: value}); diff --git a/src/ObjectCollections/PlayerCollection.ts b/src/ObjectCollections/PlayerCollection.ts index 4d7992c..d34ca83 100644 --- a/src/ObjectCollections/PlayerCollection.ts +++ b/src/ObjectCollections/PlayerCollection.ts @@ -68,5 +68,4 @@ class PlayerCollection extends MongoStoredObjectCollection { } } - export default PlayerCollection.getInstance; \ No newline at end of file diff --git a/src/ObjectCollections/RulesetCollection.ts b/src/ObjectCollections/RulesetCollection.ts index 9426656..2d3cb69 100644 --- a/src/ObjectCollections/RulesetCollection.ts +++ b/src/ObjectCollections/RulesetCollection.ts @@ -1,10 +1,14 @@ import MongoStoredObjectCollection from "./MongoStoredObjectCollection"; -import {DEFAULT_RULESET, DEFAULT_RULESET_NAME, RulesetSchema} from "../rulesets"; -import {getMongoObjectCollection} from "../database"; +import { + DEFAULT_RULESET, + DEFAULT_RULESET_NAME, + RulesetSchema, +} from "../rulesets"; +import { getMongoObjectCollection } from "../database"; import Ruleset from "../Objects/Ruleset"; -import {ActiveRecordId} from "../Objects/ActiveRecord"; +import { ActiveRecordId } from "../Objects/ActiveRecord"; -type RulesetMongoData = RulesetSchema & {id: ActiveRecordId}; +type RulesetMongoData = RulesetSchema & { id: ActiveRecordId }; class RulesetCollection extends MongoStoredObjectCollection { private static instance?: RulesetCollection; @@ -23,19 +27,33 @@ class RulesetCollection extends MongoStoredObjectCollection { this.mongoDbClientCollection = getMongoObjectCollection("rulesets"); } - private async rulesetFrom(data: RulesetMongoData): Promise { + private rulesetFrom(data: RulesetMongoData): Ruleset { return new Ruleset(data.id, data); } + async create(rulesetSchema: RulesetSchema): Promise { + const newRuleset = await this.mongoCreate(rulesetSchema); + return this.rulesetFrom(newRuleset); + } + async read(id: ActiveRecordId): Promise { 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 { + const rulesets = await this.mongoFindBy({}); + return [ + ...rulesets.map((ruleset) => + this.rulesetFrom(ruleset as RulesetMongoData) + ), + await this.read(DEFAULT_RULESET_NAME), + ]; + } } -export default RulesetCollection.getInstance; \ No newline at end of file +export default RulesetCollection.getInstance; diff --git a/src/Objects/AccountStats.ts b/src/Objects/AccountStats.ts index a166625..6eef04f 100644 --- a/src/Objects/AccountStats.ts +++ b/src/Objects/AccountStats.ts @@ -1,34 +1,21 @@ -import {RulesetSchema} from "../rulesets"; -import {AccountStatsMongoData, OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData"; -import {UpdateError} from "../errors"; -import StatsUpdater from "./StatsUpdater"; +import { RulesetSchema } from "../rulesets"; +import { AccountStatsMongoData, OutcomeType, PlayerGameResults } from "./DefaultStatsMongoData"; +import { UpdateError } from "../errors"; +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) { - for (const playerGameResult of playerGameResults) { - this.updater.updateStats(playerGameResult, 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.`); + updateStats(playerGameResults: PlayerGameResultsWithOutcomes[], ruleset: Ruleset): void { + const updater = new StatsUpdater(this.data, ruleset); + for (const playerGameResult of playerGameResults) { + updater.updateStats(playerGameResult); } + this.data.gamesPlayed += 1; } getData(): AccountStatsMongoData { @@ -36,4 +23,4 @@ class AccountStats { } } -export default AccountStats; \ No newline at end of file +export default AccountStats; diff --git a/src/Objects/DefaultStatsMongoData.ts b/src/Objects/DefaultStatsMongoData.ts index f5b64f9..a886ea9 100755 --- a/src/Objects/DefaultStatsMongoData.ts +++ b/src/Objects/DefaultStatsMongoData.ts @@ -22,9 +22,14 @@ export interface RulesetStatsMongoData { losses: number; grandTotal: TotalFieldStatsMongoData; } -export interface BlockStatsMongoData { +export type BlockStatsMongoData = BonusBlockStatsMongoData | NoBonusBlockStatsMongoData; +export interface BonusBlockStatsMongoData { + cellStats: Record; + timesHadBonus: number; + total: TotalFieldStatsMongoData; +} +export interface NoBonusBlockStatsMongoData { cellStats: Record; - timesHadBonus?: number; total: TotalFieldStatsMongoData; } export interface BaseCellStatsMongoData { @@ -133,7 +138,7 @@ function defaultBlockStatsMongoData(cellSchemas: Record, hasBon } } -function defaultRulesetStatsMongoData(ruleset: RulesetSchema): RulesetStatsMongoData { +export function defaultRulesetStatsMongoData(ruleset: RulesetSchema): RulesetStatsMongoData { const blockStatsRecord: Record = {}; for (const blockLabel in ruleset.blocks) { blockStatsRecord[blockLabel] = defaultBlockStatsMongoData(ruleset.blocks[blockLabel].cells, ruleset.blocks[blockLabel].hasBonus); diff --git a/src/Objects/PlayerStats.ts b/src/Objects/PlayerStats.ts index 787abd6..6465828 100644 --- a/src/Objects/PlayerStats.ts +++ b/src/Objects/PlayerStats.ts @@ -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; } diff --git a/src/Objects/Ruleset.ts b/src/Objects/Ruleset.ts index 8f1aeab..1009c71 100644 --- a/src/Objects/Ruleset.ts +++ b/src/Objects/Ruleset.ts @@ -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; \ No newline at end of file diff --git a/src/Objects/StatsUpdater.ts b/src/Objects/StatsUpdater.ts index 421269a..3f668e8 100644 --- a/src/Objects/StatsUpdater.ts +++ b/src/Objects/StatsUpdater.ts @@ -1,49 +1,59 @@ -import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator"; -import {UpdateError} from "../errors"; -import {FieldType} from "../enums"; +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()) { - 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.`); + updateStats(playerGameResults: PlayerGameResultsWithOutcomes): void { + for (const blockId in this.validationRuleset.getBlocks()) { + this.updateBlockStats(blockId); } + 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,43 +63,55 @@ 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 + ); } } -export default StatsUpdater; \ No newline at end of file +export default StatsUpdater; diff --git a/src/Routers/apiRouter.ts b/src/Routers/apiRouter.ts index 6019b33..bbaa4b4 100755 --- a/src/Routers/apiRouter.ts +++ b/src/Routers/apiRouter.ts @@ -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; \ No newline at end of file