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": { "prettier": {
"tabWidth": 4, "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) => { export const getRuleset: RequestHandler = async (req, res) => {
const ruleset = await RulesetCollection().read(req.params.id); 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 { export interface ProcessedGameSubmission {
ruleset: string; ruleset: string;
players: {id: string, nick: string}[]; players: { id: string; nick: string }[];
results: ScoredResultsWithOutcome[]; results: ScoredResultsWithOutcome[];
} }
@@ -32,16 +32,18 @@ enum ResultType {
loser, loser,
} }
type ScoredResults = {score: number, results: PlayerGameResult}; type ScoredResults = { score: number; results: PlayerGameResult };
export type ScoredResultsWithOutcome = {score: number, results: PlayerGameResult & {outcome: OutcomeType}}; export type ScoredResultsWithOutcome = {
score: number;
results: PlayerGameResult & { outcome: OutcomeType };
};
export const listGames: RequestHandler = async (req, res) => { export const listGames: RequestHandler = async (req, res) => {
const user = req.user as KadiUser; const user = req.user as KadiUser;
const gamesList = await KadiUserCollection().getSavedGamesForUser(user); const gamesList = await KadiUserCollection().getSavedGamesForUser(user);
if (gamesList) { if (gamesList) {
res.json({ games: gamesList }); res.json({ games: gamesList });
} } else {
else {
res.sendStatus(404); res.sendStatus(404);
} }
}; };
@@ -55,7 +57,10 @@ export const saveGame: RequestHandler = async (req, res) => {
} }
const rulesetUsed = await RulesetCollection().read(submission.ruleset); const rulesetUsed = await RulesetCollection().read(submission.ruleset);
const scoredResultsWithOutcomes = await processStats(rulesetUsed, submission.results, user); 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 }); 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); const stats = await KadiUserCollection().getAllStatsForUser(user);
if (stats) { if (stats) {
res.json({ res.json({
pStats: stats.pStats.map(pStats => ({...pStats, stats: pStats.stats.getData()})), pStats: stats.pStats.map((pStats) => ({
accStats: stats.accStats.getData() ...pStats,
stats: pStats.stats.getData(),
})),
accStats: stats.accStats.getData(),
}); });
} } else {
else {
res.sendStatus(404); res.sendStatus(404);
} }
}; };
@@ -78,20 +85,30 @@ async function addNewGuests(submission: GameSubmission, user: KadiUser): Promise
for (const playerDetails of submission.players) { for (const playerDetails of submission.players) {
const isNewPlayer = playerDetails.id === playerDetails.nick; const isNewPlayer = playerDetails.id === playerDetails.nick;
if (isNewPlayer) { if (isNewPlayer) {
const newGuest: Player = await KadiUserCollection().addGuestForUser(user, playerDetails.nick); const newGuest: Player = await KadiUserCollection().addGuestForUser(
user,
playerDetails.nick
);
newGuestIds.push(newGuest); newGuestIds.push(newGuest);
} }
} }
return newGuestIds; return newGuestIds;
} }
function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: Player[]): GameSubmission { function fillOutSubmissionWithNewIds(
submission: GameSubmission,
newGuestList: Player[]
): GameSubmission {
for (const newGuest of newGuestList) { 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) { if (gameResultsFromNewGuest) {
gameResultsFromNewGuest.playerId = newGuest.getId().toString(); 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) { if (playerEntryForNewGuest) {
playerEntryForNewGuest.id = newGuest.getId().toString(); playerEntryForNewGuest.id = newGuest.getId().toString();
} }
@@ -99,35 +116,45 @@ function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: P
return submission; 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); const calc = new ScoreCalculator(ruleset);
let playerScoreList: ScoredResults[] = []; let playerScoreList: ScoredResults[] = [];
for (const result of results) { for (const result of results) {
calc.hydrateWithJSON(result); calc.hydrateWithJSON(result);
playerScoreList.push({ playerScoreList.push({
score: calc.getTotal(), score: calc.getTotal(),
results: result results: result,
}); });
} }
const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList); const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList);
await updateStatsForIndividualPlayers(playerScoreListWithOutcomes, ruleset); 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); await KadiUserCollection().updateAccountStats(account.getId(), gameResults, ruleset);
return playerScoreListWithOutcomes; return playerScoreListWithOutcomes;
} }
function updateScoreListWithOutcomes(scoreResultsList: ScoredResults[]): ScoredResultsWithOutcome[] { function updateScoreListWithOutcomes(
scoreResultsList: ScoredResults[]
): ScoredResultsWithOutcome[] {
scoreResultsList = sortDescendingByScore(scoreResultsList); scoreResultsList = sortDescendingByScore(scoreResultsList);
const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = scoreResultsList.map(scoredResults => { const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = scoreResultsList.map(
const newResults = {...scoredResults.results, outcome: OutcomeType.loss}; (scoredResults) => {
const newResults = {
...scoredResults.results,
outcome: OutcomeType.loss,
};
return { ...scoredResults, results: newResults }; return { ...scoredResults, results: newResults };
}); }
);
let runnerUpsStart: number; let runnerUpsStart: number;
if (playerScoreListWithOutcomes[0].score !== playerScoreListWithOutcomes[1].score) { if (playerScoreListWithOutcomes[0].score !== playerScoreListWithOutcomes[1].score) {
playerScoreListWithOutcomes[0].results.outcome = OutcomeType.win; playerScoreListWithOutcomes[0].results.outcome = OutcomeType.win;
runnerUpsStart = 1; runnerUpsStart = 1;
} } else {
else {
runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes); runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes);
} }
const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes, runnerUpsStart); const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes, runnerUpsStart);
@@ -139,28 +166,32 @@ function updateScoreListWithDraws(scoreResultsList: ScoredResultsWithOutcome[]):
for (let i = 0; i < scoreResultsList.length; i++) { for (let i = 0; i < scoreResultsList.length; i++) {
if (scoreResultsList[i].score === scoreResultsList[0].score) { if (scoreResultsList[i].score === scoreResultsList[0].score) {
scoreResultsList[i].results.outcome = OutcomeType.draw; scoreResultsList[i].results.outcome = OutcomeType.draw;
} } else {
else {
return i; return i;
} }
} }
return scoreResultsList.length; return scoreResultsList.length;
} }
function updateScoreListWithRunnerUps(scoreResultsList: ScoredResultsWithOutcome[], runnerUpsStartIndex: number): number { function updateScoreListWithRunnerUps(
scoreResultsList: ScoredResultsWithOutcome[],
runnerUpsStartIndex: number
): number {
scoreResultsList[runnerUpsStartIndex].results.outcome = OutcomeType.runnerUp; scoreResultsList[runnerUpsStartIndex].results.outcome = OutcomeType.runnerUp;
for (let i = runnerUpsStartIndex + 1; i < scoreResultsList.length; i++) { for (let i = runnerUpsStartIndex + 1; i < scoreResultsList.length; i++) {
if (scoreResultsList[i].score === scoreResultsList[i - 1].score) { if (scoreResultsList[i].score === scoreResultsList[i - 1].score) {
scoreResultsList[i].results.outcome = OutcomeType.runnerUp; scoreResultsList[i].results.outcome = OutcomeType.runnerUp;
} } else {
else {
return i; return i;
} }
} }
return scoreResultsList.length; return scoreResultsList.length;
} }
function updateScoreListWithLosses(scoreResultsList: ScoredResultsWithOutcome[], losersStartIndex: number) { function updateScoreListWithLosses(
scoreResultsList: ScoredResultsWithOutcome[],
losersStartIndex: number
) {
for (let i = losersStartIndex; i < scoreResultsList.length; i++) { for (let i = losersStartIndex; i < scoreResultsList.length; i++) {
scoreResultsList[i].results.outcome = OutcomeType.loss; scoreResultsList[i].results.outcome = OutcomeType.loss;
} }
@@ -170,11 +201,18 @@ function sortDescendingByScore(playerScoreList: ScoredResults[]) {
return playerScoreList.sort((a, b) => b.score - a.score); 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) { for (const scoredResults of playerScoreListWithOutcomes) {
await PlayerCollection().updateStatsForPlayer( await PlayerCollection().updateStatsForPlayer(
scoredResults.results.playerId, 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 { CredentialsTakenError } from "../errors";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { SupportedLang } from "../enums"; 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 { getMongoObjectCollection } from "../database";
import KadiUser, { LoginDetails } from "../Objects/KadiUser"; import KadiUser, { LoginDetails } from "../Objects/KadiUser";
import { ActiveRecordId, OrId } from "../Objects/ActiveRecord"; import { ActiveRecordId, OrId } from "../Objects/ActiveRecord";
@@ -29,6 +34,15 @@ export interface KadiUserMongoData {
savedGames: ActiveRecordId[]; savedGames: ActiveRecordId[];
} }
export interface StatsListing {
accStats: AccountStats;
pStats: {
nick: string;
playerId: ActiveRecordId;
stats: PlayerStats;
}[];
}
class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> { class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> {
private static instance?: KadiUserCollection; private static instance?: KadiUserCollection;
private constructor() { private constructor() {
@@ -43,16 +57,11 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
} }
async init() { async init() {
this.mongoDbClientCollection = getMongoObjectCollection("users") this.mongoDbClientCollection = getMongoObjectCollection("users");
} }
private kadiUserFrom(data: KadiUserMongoData): KadiUser { private kadiUserFrom(data: KadiUserMongoData): KadiUser {
return new KadiUser( return new KadiUser(data.id, data.username, data.email, data.password, data.lang);
data.id,
data.username,
data.email,
data.password,
data.lang);
} }
async read(id: string): Promise<KadiUser> { async read(id: string): Promise<KadiUser> {
@@ -64,8 +73,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
const foundUser = await this.mongoFindByAttribute("email", emailQuery); const foundUser = await this.mongoFindByAttribute("email", emailQuery);
if (foundUser) { if (foundUser) {
return this.kadiUserFrom(foundUser); return this.kadiUserFrom(foundUser);
} } else {
else {
return null; return null;
} }
} }
@@ -75,9 +83,8 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
const emailTaken = await this.userWithEmailExists(loginDetails.email); const emailTaken = await this.userWithEmailExists(loginDetails.email);
if (usernameTaken || emailTaken) { if (usernameTaken || emailTaken) {
throw new CredentialsTakenError(usernameTaken, emailTaken); throw new CredentialsTakenError(usernameTaken, emailTaken);
} } else {
else { return this.addNewUser({ ...loginDetails });
return this.addNewUser({...loginDetails})
} }
} }
@@ -116,15 +123,20 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
const newGuest = await PlayerCollection().create(newGuestNick); const newGuest = await PlayerCollection().create(newGuestNick);
await this.mongoDbClientCollection!.findOneAndUpdate( await this.mongoDbClientCollection!.findOneAndUpdate(
{ _id: this.idFromRecordOrId(userOrId) }, { _id: this.idFromRecordOrId(userOrId) },
{$push: {guests: newGuest.getId()}}); { $push: { guests: newGuest.getId() } }
);
return newGuest; 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)); const deletedGuest = await PlayerCollection().delete(this.idFromRecordOrId(guestOrGuestId));
await this.mongoDbClientCollection!.findOneAndUpdate( await this.mongoDbClientCollection!.findOneAndUpdate(
{ _id: this.idFromRecordOrId(userOrId) }, { _id: this.idFromRecordOrId(userOrId) },
{$pull: {guests: this.idFromRecordOrId(deletedGuest)}}); { $pull: { guests: this.idFromRecordOrId(deletedGuest) } }
);
return deletedGuest; return deletedGuest;
} }
@@ -133,8 +145,8 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
return Promise.all<Player>( return Promise.all<Player>(
guestIdList.map(async (guestId) => { guestIdList.map(async (guestId) => {
return await PlayerCollection().read(guestId); return await PlayerCollection().read(guestId);
} })
)); );
} }
async getMainPlayerForUser(userOrId: OrId<KadiUser>): Promise<Player> { async getMainPlayerForUser(userOrId: OrId<KadiUser>): Promise<Player> {
@@ -147,50 +159,56 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
return Promise.all<SavedGame>( return Promise.all<SavedGame>(
savedGameIds.map(async (savedGameId) => { savedGameIds.map(async (savedGameId) => {
return await SavedGameCollection().read(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); const newGame = await SavedGameCollection().create(submission);
await this.mongoDbClientCollection!.findOneAndUpdate( await this.mongoDbClientCollection!.findOneAndUpdate(
{ _id: this.idFromRecordOrId(userOrId) }, { _id: this.idFromRecordOrId(userOrId) },
{$push: {savedGames: newGame.getId()}}); { $push: { savedGames: newGame.getId() } }
);
return newGame; 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 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 accountStatsMongoData = (await this.mongoRead(userId)).accountStats;
const accountStatsObject = new AccountStats(accountStatsMongoData); const accountStatsObject = new AccountStats(accountStatsMongoData);
accountStatsObject.updateStats(gameResults, rulesetUsed); accountStatsObject.updateStats(gameResults, rulesetUsed);
this.mongoUpdate({ this.mongoUpdate({
id: userId, id: userId,
accountStats: accountStatsObject.getData() accountStats: accountStatsObject.getData(),
}); });
} }
async getAllStatsForUser(userOrId: OrId<KadiUser>): Promise<StatsListing> { async getAllStatsForUser(userOrId: OrId<KadiUser>): Promise<StatsListing> {
const players = [...(await this.getAllGuestsForUser(userOrId)), await this.getMainPlayerForUser(userOrId)]; const players = [
const playerStats = players.map(player => ({ ...(await this.getAllGuestsForUser(userOrId)),
await this.getMainPlayerForUser(userOrId),
];
const playerStats = players.map((player) => ({
nick: player.getNick(), nick: player.getNick(),
playerId: player.getId(), 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); 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; export default KadiUserCollection.getInstance;

View File

@@ -2,6 +2,7 @@ import mongo from "mongodb";
import {tryQuery} from "../database"; import {tryQuery} from "../database";
import {InvalidIdError, MongoError} from "../errors"; import {InvalidIdError, MongoError} from "../errors";
import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord"; import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord";
import {FilterQuery} from "mongoose";
abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}> { 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> { protected async mongoFindByAttribute(attribute: string, value: any): Promise<IRawData | null> {
return tryQuery(async () => { return tryQuery(async () => {
const result = await this.mongoDbClientCollection!.findOne({[attribute]: value}); const result = await this.mongoDbClientCollection!.findOne({[attribute]: value});

View File

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

View File

@@ -1,5 +1,9 @@
import MongoStoredObjectCollection from "./MongoStoredObjectCollection"; 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 { getMongoObjectCollection } from "../database";
import Ruleset from "../Objects/Ruleset"; import Ruleset from "../Objects/Ruleset";
import { ActiveRecordId } from "../Objects/ActiveRecord"; import { ActiveRecordId } from "../Objects/ActiveRecord";
@@ -23,19 +27,33 @@ class RulesetCollection extends MongoStoredObjectCollection<RulesetMongoData> {
this.mongoDbClientCollection = getMongoObjectCollection("rulesets"); this.mongoDbClientCollection = getMongoObjectCollection("rulesets");
} }
private async rulesetFrom(data: RulesetMongoData): Promise<Ruleset> { private rulesetFrom(data: RulesetMongoData): Ruleset {
return new Ruleset(data.id, data); 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> { async read(id: ActiveRecordId): Promise<Ruleset> {
if (id === DEFAULT_RULESET_NAME) { if (id === DEFAULT_RULESET_NAME) {
return new Ruleset(DEFAULT_RULESET_NAME, DEFAULT_RULESET); return new Ruleset(DEFAULT_RULESET_NAME, DEFAULT_RULESET);
} } else {
else {
const foundRuleset = await this.mongoRead(id); const foundRuleset = await this.mongoRead(id);
return this.rulesetFrom(foundRuleset); 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; export default RulesetCollection.getInstance;

View File

@@ -1,35 +1,22 @@
import { RulesetSchema } from "../rulesets"; import { RulesetSchema } from "../rulesets";
import { AccountStatsMongoData, OutcomeType, PlayerGameResults } from "./DefaultStatsMongoData"; import { AccountStatsMongoData, OutcomeType, PlayerGameResults } from "./DefaultStatsMongoData";
import { UpdateError } from "../errors"; import { UpdateError } from "../errors";
import StatsUpdater from "./StatsUpdater"; import StatsUpdater, { PlayerGameResultsWithOutcomes } from "./StatsUpdater";
import Ruleset from "./Ruleset"; import Ruleset from "./Ruleset";
class AccountStats { class AccountStats {
private data: AccountStatsMongoData; private readonly data: AccountStatsMongoData;
private readonly updater: StatsUpdater;
constructor(data: AccountStatsMongoData) { constructor(data: AccountStatsMongoData) {
this.data = data; this.data = data;
this.updater = new StatsUpdater();
this.updater.use(data);
} }
use(data: AccountStatsMongoData) { updateStats(playerGameResults: PlayerGameResultsWithOutcomes[], ruleset: Ruleset): void {
this.data = data; const updater = new StatsUpdater(this.data, ruleset);
this.updater.use(data);
}
updateStats(playerGameResults: (PlayerGameResults & {outcome: OutcomeType})[], ruleset: Ruleset): void {
if (this.data) {
for (const playerGameResult of playerGameResults) { for (const playerGameResult of playerGameResults) {
this.updater.updateStats(playerGameResult, ruleset); updater.updateStats(playerGameResult);
} }
this.data.gamesPlayed += 1; 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 { getData(): AccountStatsMongoData {
return this.data; return this.data;

View File

@@ -22,9 +22,14 @@ export interface RulesetStatsMongoData {
losses: number; losses: number;
grandTotal: TotalFieldStatsMongoData; 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>; cellStats: Record<string, CellStatsMongoData>;
timesHadBonus?: number;
total: TotalFieldStatsMongoData; total: TotalFieldStatsMongoData;
} }
export interface BaseCellStatsMongoData { 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> = {}; const blockStatsRecord: Record<string, BlockStatsMongoData> = {};
for (const blockLabel in ruleset.blocks) { for (const blockLabel in ruleset.blocks) {
blockStatsRecord[blockLabel] = defaultBlockStatsMongoData(ruleset.blocks[blockLabel].cells, ruleset.blocks[blockLabel].hasBonus); blockStatsRecord[blockLabel] = defaultBlockStatsMongoData(ruleset.blocks[blockLabel].cells, ruleset.blocks[blockLabel].hasBonus);

View File

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

View File

@@ -1,5 +1,7 @@
import {BlockDef, CellDef, RulesetSchema} from "../rulesets"; import {BlockDef, CellDef, RulesetSchema} from "../rulesets";
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord"; import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
import {FieldType} from "../enums";
import {CellLocation} from "./ScoreCalculator";
export class Ruleset implements ActiveRecord { export class Ruleset implements ActiveRecord {
constructor( constructor(
@@ -27,9 +29,13 @@ export class Ruleset implements ActiveRecord {
return Object.assign({}, this.schema.blocks[blockId].cells); return Object.assign({}, this.schema.blocks[blockId].cells);
} }
getSchemaJSON(): RulesetSchema { getSchema(): RulesetSchema {
return Object.assign({}, this.schema); return Object.assign({}, this.schema);
} }
getCellFieldTypeByLocation(cellLocation: CellLocation): FieldType {
return this.getCellsInBlock(cellLocation.blockId)[cellLocation.cellId].fieldType;
}
} }
export default Ruleset; 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 { UpdateError } from "../errors";
import { FieldType } from "../enums"; import { FieldType } from "../enums";
import { import {
BaseCellStatsMongoData,
BaseStatsMongoData, BaseStatsMongoData,
BestableFieldStatsMongoData, BestableFieldStatsMongoData,
BlockStatsMongoData,
BonusBlockStatsMongoData,
BoolFieldStatsMongoData, BoolFieldStatsMongoData,
CellStatsMongoData, CellStatsMongoData,
defaultRulesetStatsMongoData,
OutcomeType, OutcomeType,
PlayerGameResults, PlayerGameResults,
RulesetStatsMongoData, RulesetStatsMongoData,
TotalFieldStatsMongoData TotalFieldStatsMongoData,
} from "./DefaultStatsMongoData"; } from "./DefaultStatsMongoData";
import Ruleset from "./Ruleset"; import Ruleset from "./Ruleset";
import { BonusBlockDef } from "../rulesets";
export type PlayerGameResultsWithOutcomes = PlayerGameResults & { outcome: OutcomeType };
class StatsUpdater { class StatsUpdater {
private data?: BaseStatsMongoData; private data: BaseStatsMongoData;
private validationRuleset?: Ruleset; private validationRuleset: Ruleset;
private calculator?: ScoreCalculator; private calculator: ScoreCalculator;
private currentStatsObject?: RulesetStatsMongoData; private statsByRuleset: RulesetStatsMongoData;
constructor() { 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) { use(data: BaseStatsMongoData) {
this.data = data; this.data = data;
} }
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void { updateStats(playerGameResults: PlayerGameResultsWithOutcomes): void {
if (this.data) { for (const blockId in this.validationRuleset.getBlocks()) {
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.updateBlockStats(blockId);
} }
this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal()); this.updateTotalFieldStats(this.statsByRuleset.grandTotal, this.calculator.getTotal());
this.currentStatsObject.wins += Number(playerGameResults.outcome === OutcomeType.win); this.statsByRuleset.wins += Number(playerGameResults.outcome === OutcomeType.win);
this.currentStatsObject.draws += Number(playerGameResults.outcome === OutcomeType.draw); this.statsByRuleset.draws += Number(playerGameResults.outcome === OutcomeType.draw);
this.currentStatsObject.runnerUps += Number(playerGameResults.outcome === OutcomeType.runnerUp); this.statsByRuleset.runnerUps += Number(playerGameResults.outcome === OutcomeType.runnerUp);
this.currentStatsObject.losses += Number(playerGameResults.outcome === OutcomeType.loss); this.statsByRuleset.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.`);
}
} }
private updateTotalFieldStats(statsObject: TotalFieldStatsMongoData, total: number) { private updateTotalFieldStats(statsObject: TotalFieldStatsMongoData, total: number) {
@@ -53,42 +63,54 @@ class StatsUpdater {
} }
private updateBlockStats(blockId: string) { private updateBlockStats(blockId: string) {
if (this.currentStatsObject) { const blockStats = this.statsByRuleset.blockStats[blockId];
const blockStats = this.currentStatsObject.blockStats[blockId]; this.updateTotalFieldStats(blockStats.total, this.calculator.getBlockSubTotalById(blockId));
this.updateTotalFieldStats(blockStats.total, this.calculator!.getBlockSubTotalById(blockId)); if (this.isBonusBlockStats(blockStats)) {
if (this.calculator!.blockWithIdHasBonus(blockId)) { blockStats.timesHadBonus += 1;
blockStats.timesHadBonus! += 1;
}
for (const cellId in this.validationRuleset!.getBlocks()[blockId].cells) {
this.updateCellStatsByIds({cellId, blockId});
} }
for (const cellId in this.validationRuleset.getBlocks()[blockId].cells) {
this.updateCellStatsByLocation({ cellId, blockId });
} }
} }
private updateCellStatsByIds(ids: {cellId: string, blockId: string}) { private updateCellStatsByLocation(location: CellLocation) {
const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.getId().toString()}); const cellStats = this.getCellStatsByLocation(location);
const cellFieldType = this.validationRuleset?.getBlocks()[ids.blockId].cells[ids.cellId].fieldType; const cellFieldType = this.validationRuleset.getCellFieldTypeByLocation(location);
const cellScore = this.calculator!.getCellScoreByLocation({...ids}); const cellScore = this.calculator.getCellScoreByLocation(location);
cellStats.runningTotal += cellScore; cellStats.runningTotal += cellScore;
if (cellScore > 0 && cellFieldType === FieldType.bool) { if (cellScore > 0 && cellFieldType === FieldType.bool) {
(cellStats as BoolFieldStatsMongoData).total += 1; (cellStats as BoolFieldStatsMongoData).total += 1;
} } else if (this.isBestableCell(cellStats, cellFieldType)) {
else if (cellFieldType === FieldType.multiplier || FieldType.superkadi || FieldType.number) { if (cellStats.best < cellScore) {
const bestableStats = (cellStats as BestableFieldStatsMongoData); cellStats.best = cellScore;
if (bestableStats.best < cellScore) { } else if (cellStats.worst > cellScore) {
bestableStats.best = cellScore; cellStats.worst = cellScore;
}
else if (bestableStats.worst > cellScore) {
bestableStats.worst = cellScore
} }
} }
if (this.calculator!.cellAtLocationIsStruck({...ids})) { if (this.calculator.cellAtLocationIsStruck({ ...location })) {
cellStats.timesStruck += 1; cellStats.timesStruck += 1;
} }
} }
private getCellStatsByIds(ids: {cellId: string, blockId: string, rulesetId: string}): CellStatsMongoData { private getCellStatsByLocation(location: CellLocation): CellStatsMongoData {
return this.data!.statsByRuleset[ids.rulesetId].blockStats[ids.blockId].cellStats[ids.cellId]; 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 //Stats
router.get("/stats", requireAuthenticated, statsController.getStats); router.get("/stats", requireAuthenticated, statsController.getStats);
router.get("/ruleset/:id", rulesetController.getRuleset); router.get("/ruleset/:id", rulesetController.getRuleset);
router.post("/ruleset/", requireAuthenticated, rulesetController.addRuleset);
router.get("/rulesets/", requireAuthenticated, rulesetController.getAllRulesets);
export default router; export default router;