update
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4,
|
||||
"jsxBracketSameLine": true
|
||||
"jsxBracketSameLine": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<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};
|
||||
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<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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,16 +122,21 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
|
||||
async addGuestForUser(userOrId: OrId<KadiUser>, newGuestNick: string): Promise<Player> {
|
||||
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<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)}});
|
||||
{ _id: this.idFromRecordOrId(userOrId) },
|
||||
{ $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()}});
|
||||
{ _id: this.idFromRecordOrId(userOrId) },
|
||||
{ $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};
|
||||
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;
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -68,5 +68,4 @@ class PlayerCollection extends MongoStoredObjectCollection<PlayerMongoData> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default PlayerCollection.getInstance;
|
||||
@@ -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<RulesetMongoData> {
|
||||
private static instance?: RulesetCollection;
|
||||
@@ -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;
|
||||
export default RulesetCollection.getInstance;
|
||||
|
||||
@@ -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;
|
||||
export default AccountStats;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
export default StatsUpdater;
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user