Big update

This commit is contained in:
Daniel Ledda
2020-08-13 15:10:55 +02:00
parent 42b7560d6d
commit 5f747142e5
28 changed files with 310 additions and 179 deletions

View File

@@ -27,7 +27,7 @@ export const changeLang: RequestHandler = async (req, res) => {
export const addGuest: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser);
if (req.body.guestName) {
const newGuest: Player = await KadiUserCollection.addGuestForUser(user, req.body.guestName);
const newGuest: Player = await KadiUserCollection().addGuestForUser(user, req.body.guestName);
res.send({
username: user.getUsername(),
userId: user.getId(),
@@ -47,9 +47,9 @@ export const updateGuest: RequestHandler = async (req, res) => {
const {id: guestId} = req.params;
if (req.body.newName) {
const {newName} = req.body;
const guest = await PlayerCollection.read(guestId);
const guest = await PlayerCollection().read(guestId);
guest.setNick(newName);
const updatedGuest = await PlayerCollection.save(guest);
const updatedGuest = await PlayerCollection().save(guest);
res.status(200).send({
userId: user.getId(),
username: user.getUsername(),
@@ -67,7 +67,7 @@ export const updateGuest: RequestHandler = async (req, res) => {
export const getGuest: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser);
const {id: guestId} = req.params;
const guest = await PlayerCollection.read(guestId);
const guest = await PlayerCollection().read(guestId);
res.status(200).send({
userId: user.getId(),
username: user.getUsername(),
@@ -78,7 +78,7 @@ export const getGuest: RequestHandler = async (req, res) => {
export const deleteGuest: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser);
const {id: guestId} = req.params;
const deletedGuest = await KadiUserCollection.deleteGuestFromUser(user, guestId);
const deletedGuest = await KadiUserCollection().deleteGuestFromUser(user, guestId);
res.status(200).send({
userId: user.getId(),
username: user.getUsername(),
@@ -88,7 +88,7 @@ export const deleteGuest: RequestHandler = async (req, res) => {
export const getGuests: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser);
const guests = await KadiUserCollection.getAllGuestsForUser(user);
const guests = await KadiUserCollection().getAllGuestsForUser(user);
res.status(200).send({
userId: user.getId(),
username: user.getUsername(),
@@ -98,7 +98,7 @@ export const getGuests: RequestHandler = async (req, res) => {
export const getAllPlayersAssociatedWithAccount: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser);
const guests = await KadiUserCollection.getAllGuestsForUser(user);
const mainPlayer = await KadiUserCollection.getMainPlayerForUser(user);
const guests = await KadiUserCollection().getAllGuestsForUser(user);
const mainPlayer = await KadiUserCollection().getMainPlayerForUser(user);
res.status(200).send({guests, mainPlayer});
};

View File

@@ -0,0 +1,7 @@
import {RequestHandler} from "express";
import RulesetCollection from "../ObjectCollections/RulesetCollection";
export const getRuleset: RequestHandler = async (req, res) => {
const ruleset = await RulesetCollection().read(req.params.id);
res.json(ruleset.getSchemaJSON());
};

View File

@@ -24,7 +24,7 @@ export const showRegistrationPage: RequestHandler = (req, res) => {
export const registerNewUser: RequestHandler = async (req, res) => {
try {
const loginDetails: LoginDetails = req.body as LoginDetails;
const newUser = await KadiUserCollection.registerUser(loginDetails);
const newUser = await KadiUserCollection().registerUser(loginDetails);
req.login(newUser, (err) => {
if (err) {
throw err;
@@ -54,7 +54,7 @@ export const logoutUser: RequestHandler = (req, res) => {
};
export const authenticateKadiUser: VerifyFunction = async (email, password, done) => {
const user = await KadiUserCollection.findByEmail(email);
const user = await KadiUserCollection().findByEmail(email);
if (!user) {
return done(null, false, { message: "A user with that email does not exist."} );
}
@@ -75,6 +75,6 @@ export async function serializeKadiUser(user: KadiUser, done: (err: any, id?: un
}
export async function deserializeKadiUser(id: string, done: (err: any, id?: unknown) => void): Promise<void> {
const user: KadiUser | null = await KadiUserCollection.getSerializedAuthUser(id);
const user: KadiUser | null = await KadiUserCollection().read(id);
done(null, user);
}

View File

@@ -9,12 +9,18 @@ import RulesetCollection from "../ObjectCollections/RulesetCollection";
import Ruleset from "../Objects/Ruleset";
import {OutcomeType} from "../Objects/DefaultStatsMongoData";
export interface GameSubmission {
rulesetId: string;
interface GameSubmission {
ruleset: string;
players: { id: string; nick: string }[];
results: PlayerGameResult[];
}
export interface ProcessedGameSubmission {
ruleset: string;
players: {id: string, nick: string}[];
results: ScoredResultsWithOutcome[];
}
type PlayerGameResult = { playerId: string; blocks: Record<string, Block> };
type Block = { cells: Record<string, Cell> };
type Cell = { value: CellValue };
@@ -27,11 +33,11 @@ enum ResultType {
}
type ScoredResults = {score: number, results: PlayerGameResult};
type ScoredResultsWithOutcome = {score: number, results: PlayerGameResult & {outcome: OutcomeType}};
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);
const gamesList = await KadiUserCollection().getSavedGamesForUser(user);
if (gamesList) {
res.json({ games: gamesList });
}
@@ -47,17 +53,32 @@ export const saveGame: RequestHandler = async (req, res) => {
if (newGuests.length > 0) {
fillOutSubmissionWithNewIds(submission, newGuests);
}
const newGame = await KadiUserCollection.addGameForUser(user, submission);
processStats(await RulesetCollection.read(submission.rulesetId), submission.results, user);
const rulesetUsed = await RulesetCollection().read(submission.ruleset);
const scoredResultsWithOutcomes = await processStats(rulesetUsed, submission.results, user);
const newGame = await KadiUserCollection().addGameForUser(user, {...submission, results: scoredResultsWithOutcomes});
res.send({ message: "Game submitted successfully!", newGame: newGame });
};
export const getStats: RequestHandler = async (req, res) => {
const user = req.user as KadiUser;
const stats = await KadiUserCollection().getAllStatsForUser(user);
if (stats) {
res.json({
pStats: stats.pStats.map(pStats => ({...pStats, stats: pStats.stats.getData()})),
accStats: stats.accStats.getData()
});
}
else {
res.sendStatus(404);
}
};
async function addNewGuests(submission: GameSubmission, user: KadiUser): Promise<Player[]> {
const newGuestIds: Player[] = [];
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);
}
}
@@ -68,17 +89,17 @@ function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: P
for (const newGuest of newGuestList) {
const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.getNick());
if (gameResultsFromNewGuest) {
gameResultsFromNewGuest.playerId = newGuest.getId();
gameResultsFromNewGuest.playerId = newGuest.getId().toString();
}
const playerEntryForNewGuest = submission.players.find((player) => player.id === newGuest.getNick());
if (playerEntryForNewGuest) {
playerEntryForNewGuest.id = newGuest.getId();
playerEntryForNewGuest.id = newGuest.getId().toString();
}
}
return submission;
}
async function processStats(ruleset: Ruleset, results: PlayerGameResult[], account: KadiUser) {
async function processStats(ruleset: Ruleset, results: PlayerGameResult[], account: KadiUser): Promise<ScoredResultsWithOutcome[]> {
const calc = new ScoreCalculator(ruleset);
let playerScoreList: ScoredResults[] = [];
for (const result of results) {
@@ -88,59 +109,60 @@ async function processStats(ruleset: Ruleset, results: PlayerGameResult[], accou
results: result
});
}
const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList, ruleset);
updateStatsForIndividualPlayers(playerScoreListWithOutcomes, ruleset);
const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList);
await updateStatsForIndividualPlayers(playerScoreListWithOutcomes, ruleset);
const gameResults = playerScoreListWithOutcomes.map(scoredResults => scoredResults.results);
await KadiUserCollection.updateAccountStats(account.getId(), gameResults, ruleset);
await KadiUserCollection().updateAccountStats(account.getId(), gameResults, ruleset);
return playerScoreListWithOutcomes;
}
function updateScoreListWithOutcomes(playerScoreList: ScoredResults[], rulesetUsed: Ruleset): ScoredResultsWithOutcome[] {
playerScoreList = sortDescendingByScore(playerScoreList);
const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = playerScoreList.map(scoredResults => {
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};
});
let runnerUpsStart: number;
if (playerScoreListWithOutcomes[0].score !== playerScoreListWithOutcomes[1].score) {
playerScoreListWithOutcomes[0].results.outcome = OutcomeType.win;
runnerUpsStart = 1;
}
else {
runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes, rulesetUsed);
runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes);
}
const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes.slice(runnerUpsStart), rulesetUsed);
updateScoreListWithLosses(playerScoreListWithOutcomes.slice(losersStart), rulesetUsed);
const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes, runnerUpsStart);
updateScoreListWithLosses(playerScoreListWithOutcomes, losersStart);
return playerScoreListWithOutcomes;
}
function updateScoreListWithDraws(playerScoreList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): number {
for (let i = 0; i < playerScoreList.length; i++) {
if (playerScoreList[i].score === playerScoreList[0].score) {
playerScoreList[i].results.outcome = OutcomeType.draw;
function updateScoreListWithDraws(scoreResultsList: ScoredResultsWithOutcome[]): number {
for (let i = 0; i < scoreResultsList.length; i++) {
if (scoreResultsList[i].score === scoreResultsList[0].score) {
scoreResultsList[i].results.outcome = OutcomeType.draw;
}
else {
return i;
}
}
return playerScoreList.length;
return scoreResultsList.length;
}
function updateScoreListWithRunnerUps(playerScoreList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): number {
for (let i = 0; i < playerScoreList.length; i++) {
if (playerScoreList[i].score === playerScoreList[0].score) {
playerScoreList[i].results.outcome = OutcomeType.runnerUp;
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 {
return i;
}
}
return playerScoreList.length;
return scoreResultsList.length;
}
function updateScoreListWithLosses(scoreResultsList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset) {
for (const lostPlayerResults of scoreResultsList) {
lostPlayerResults.results.outcome = OutcomeType.loss;
function updateScoreListWithLosses(scoreResultsList: ScoredResultsWithOutcome[], losersStartIndex: number) {
for (let i = losersStartIndex; i < scoreResultsList.length; i++) {
scoreResultsList[i].results.outcome = OutcomeType.loss;
}
}
@@ -148,9 +170,9 @@ function sortDescendingByScore(playerScoreList: ScoredResults[]) {
return playerScoreList.sort((a, b) => b.score - a.score);
}
function updateStatsForIndividualPlayers(playerScoreListWithOutcomes: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): void {
async function updateStatsForIndividualPlayers(playerScoreListWithOutcomes: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): Promise<void> {
for (const scoredResults of playerScoreListWithOutcomes) {
PlayerCollection.updateStatsForPlayer(
await PlayerCollection().updateStatsForPlayer(
scoredResults.results.playerId,
{...scoredResults.results, outcome: scoredResults.results.outcome},
rulesetUsed);

View File

@@ -0,0 +1 @@

View File

@@ -1,5 +1,4 @@
import {CredentialsTakenError, GenericPersistenceError} from "../errors";
import mongo from "mongodb";
import {CredentialsTakenError} from "../errors";
import bcrypt from "bcrypt";
import {SupportedLang} from "../enums";
import {AccountStatsMongoData, defaultAccountStatsMongoData, OutcomeType, PlayerGameResults} from "../Objects/DefaultStatsMongoData";
@@ -11,13 +10,14 @@ import SavedGameCollection from "./SavedGameCollection";
import SavedGame from "../Objects/SavedGame";
import PlayerCollection from "../ObjectCollections/PlayerCollection";
import Player from "../Objects/Player";
import {GameSubmission} from "../controllers/statsController";
import {ProcessedGameSubmission} from "../Controllers/statsController";
import Ruleset from "../Objects/Ruleset";
import RulesetCollection from "./RulesetCollection";
import AccountStats from "../Objects/AccountStats";
import PlayerStats from "../Objects/PlayerStats";
export interface KadiUserMongoData {
id: string;
id: ActiveRecordId;
username: string;
email: string;
password: string;
@@ -30,8 +30,20 @@ export interface KadiUserMongoData {
}
class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> {
constructor(collectionClient: mongo.Collection) {
super(collectionClient);
private static instance?: KadiUserCollection;
private constructor() {
super();
}
static getInstance(): KadiUserCollection {
if (KadiUserCollection.instance === undefined) {
KadiUserCollection.instance = new KadiUserCollection();
}
return KadiUserCollection.instance;
}
async init() {
this.mongoDbClientCollection = getMongoObjectCollection("users")
}
private kadiUserFrom(data: KadiUserMongoData): KadiUser {
@@ -70,7 +82,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
}
private async addNewUser(loginDetails: LoginDetails): Promise<KadiUser> {
const newPlayer = await PlayerCollection.create(loginDetails.username);
const newPlayer = await PlayerCollection().create(loginDetails.username);
const securePassword = await this.makePasswordSecure(loginDetails.password);
const newUser = await this.mongoCreate({
username: loginDetails.username,
@@ -96,58 +108,52 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
return object !== null;
}
async getSerializedAuthUser(id: string): Promise<KadiUser> {
const foundUser = await this.mongoRead(id);
if (foundUser) {
return this.kadiUserFrom(foundUser);
}
else {
throw new GenericPersistenceError("User not found!");
}
}
async makePasswordSecure(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
async addGuestForUser(userOrId: OrId<KadiUser>, newGuestNick: string): Promise<Player> {
const newGuest = await PlayerCollection.create(newGuestNick);
await this.mongoDbClientCollection.findOneAndUpdate(
const newGuest = await PlayerCollection().create(newGuestNick);
await this.mongoDbClientCollection!.findOneAndUpdate(
{_id: this.idFromRecordOrId(userOrId)},
{$push: {guests: newGuest.getId()}});
return newGuest;
}
async deleteGuestFromUser(userOrId: OrId<KadiUser>, guestOrGuestId: OrId<Player>): Promise<Player> {
const deletedGuest = await PlayerCollection.delete(this.idFromRecordOrId(guestOrGuestId));
await this.mongoDbClientCollection.findOneAndUpdate(
const deletedGuest = await PlayerCollection().delete(this.idFromRecordOrId(guestOrGuestId));
await this.mongoDbClientCollection!.findOneAndUpdate(
{_id: this.idFromRecordOrId(userOrId)},
{$pull: {guests: this.idFromRecordOrId(guestOrGuestId)}});
{$pull: {guests: this.idFromRecordOrId(deletedGuest)}});
return deletedGuest;
}
async getAllGuestsForUser(userOrId: OrId<KadiUser>): Promise<Promise<Player>[]> {
async getAllGuestsForUser(userOrId: OrId<KadiUser>): Promise<Player[]> {
const guestIdList = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.guests;
return guestIdList.map(async (guestId) => {
return await PlayerCollection.read(guestId);
});
return Promise.all<Player>(
guestIdList.map(async (guestId) => {
return await PlayerCollection().read(guestId);
}
));
}
async getMainPlayerForUser(userOrId: OrId<KadiUser>): Promise<Player> {
const userData = await this.mongoRead(this.idFromRecordOrId(userOrId));
return PlayerCollection.read(userData?.player);
return PlayerCollection().read(userData?.player);
}
async getSavedGamesForUser(userOrId: OrId<KadiUser>): Promise<Promise<SavedGame>[]> {
async getSavedGamesForUser(userOrId: OrId<KadiUser>): Promise<SavedGame[]> {
const savedGameIds = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.savedGames;
return savedGameIds.map(async (savedGameId) => {
return await SavedGameCollection.read(savedGameId);
});
return Promise.all<SavedGame>(
savedGameIds.map(async (savedGameId) => {
return await SavedGameCollection().read(savedGameId);
}
));
}
async addGameForUser(userOrId: OrId<KadiUser>, gameSubmission: GameSubmission): Promise<SavedGame> {
const newGame = await SavedGameCollection.create(gameSubmission);
await this.mongoDbClientCollection.findOneAndUpdate(
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()}});
return newGame;
@@ -155,16 +161,36 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
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 accountStatsMongoData = await this.mongoRead(userId);
const accountStatsObject = new AccountStats(accountStatsMongoData.accountStats);
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: this.idFromRecordOrId(userId),
id: userId,
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 => ({
nick: player.getNick(),
playerId: player.getId(),
stats: player.getStats()
}));
const accountStatsMongoData = (await this.mongoRead(this.idFromRecordOrId(userOrId))).accountStats;
const accountStats = new AccountStats(accountStatsMongoData);
return {pStats: playerStats, accStats: accountStats};
}
}
const KadiUserCollectionSingleton = new KadiUserCollection(getMongoObjectCollection("users"));
export default KadiUserCollectionSingleton;
export interface StatsListing {
accStats: AccountStats;
pStats: {
nick: string,
playerId: ActiveRecordId,
stats: PlayerStats
}[];
}
export default KadiUserCollection.getInstance;

View File

@@ -5,35 +5,47 @@ import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord";
abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}> {
protected mongoDbClientCollection?: mongo.Collection;
protected constructor() {}
protected constructor(protected mongoDbClientCollection: mongo.Collection) {}
abstract init(): Promise<void>;
protected async mongoCreate(objectData: Omit<IRawData, "id">): Promise<IRawData> {
return tryQuery(async () => {
const insertOneWriteOpResult = await this.mongoDbClientCollection.insertOne(objectData);
const insertOneWriteOpResult = await this.mongoDbClientCollection!.insertOne(objectData);
if (insertOneWriteOpResult.result.ok === 1) {
return insertOneWriteOpResult.ops[0]
const newObject = insertOneWriteOpResult.ops[0];
newObject.id = newObject._id;
newObject._id = undefined;
return insertOneWriteOpResult.ops[0];
} else {
throw new MongoError(`Error creating the object: ${JSON.stringify(objectData)}`);
}
});
}
protected async mongoRead(id: string): Promise<IRawData> {
protected async mongoRead(id: ActiveRecordId): Promise<IRawData> {
return tryQuery(async () => {
const result = await this.mongoDbClientCollection.findOne({_id: id});
const result = await this.mongoDbClientCollection!.findOne({_id: new mongo.ObjectID(id)});
if (result) {
result.id = result._id;
result._id = undefined;
return result;
} else {
throw new InvalidIdError(`Object in collection "${typeof this}" with id ${JSON.stringify(id)} not found!`);
throw new InvalidIdError(`Object in collection "${this.constructor.name}" with id ${JSON.stringify(id)} not found!`);
}
});
}
protected async mongoFindByAttribute(attribute: string, value: any): Promise<IRawData | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({[attribute]: value})
);
return tryQuery(async () => {
const result = await this.mongoDbClientCollection!.findOne({[attribute]: value});
if (result) {
result.id = result._id;
result._id = undefined;
}
return result;
});
}
protected async mongoDelete(objectId: ActiveRecordId, returnObject?: boolean): Promise<IRawData | void> {
@@ -41,7 +53,7 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
if (returnObject ?? true) {
deletedObject = await this.mongoRead(objectId);
}
const deleteWriteOpResult = await this.mongoDbClientCollection.deleteOne({_id: objectId});
const deleteWriteOpResult = await this.mongoDbClientCollection!.deleteOne({_id: objectId});
if (deleteWriteOpResult.result.ok === 1) {
return deletedObject;
} else {
@@ -49,16 +61,17 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
}
}
protected async mongoUpdate(object: Partial<IRawData> & {id: ActiveRecordId}) {
protected async mongoUpdate(object: Partial<IRawData>) {
await tryQuery(() =>
this.mongoDbClientCollection.findOneAndUpdate({_id: object.id}, {$set: {...object, id: undefined}})
this.mongoDbClientCollection!.findOneAndUpdate({_id: object.id}, {$set: {...object, id: undefined}})
);
}
protected idFromRecordOrId<T extends ActiveRecord>(recordOrRecordId: T | ActiveRecordId): ActiveRecordId {
return typeof recordOrRecordId === "string" ? recordOrRecordId : recordOrRecordId.getId();
return recordOrRecordId instanceof mongo.ObjectId || typeof recordOrRecordId === "string" ?
recordOrRecordId :
recordOrRecordId.getId();
}
}
export default MongoStoredObjectCollection;

View File

@@ -1,5 +1,4 @@
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
import mongo from "mongodb";
import {
defaultPlayerStatsMongoData,
OutcomeType,
@@ -14,18 +13,30 @@ import Ruleset from "../Objects/Ruleset";
import RulesetCollection from "./RulesetCollection";
export interface PlayerMongoData {
id: string;
id: ActiveRecordId;
nick: string;
stats?: PlayerStatsMongoData;
stats: PlayerStatsMongoData;
}
class PlayerCollection extends MongoStoredObjectCollection<PlayerMongoData> {
constructor(collectionClient: mongo.Collection) {
super(collectionClient);
private static instance?: PlayerCollection;
constructor() {
super();
}
static getInstance(): PlayerCollection {
if (PlayerCollection.instance === undefined) {
PlayerCollection.instance = new PlayerCollection();
}
return PlayerCollection.instance;
}
async init() {
this.mongoDbClientCollection = getMongoObjectCollection("players");
}
private playerFrom(data: PlayerMongoData): Player {
return new Player(data.id, data.nick, data.stats ? new PlayerStats(data.stats) : undefined);
return new Player(data.id, data.nick, new PlayerStats(data.stats));
}
async create(nick: string): Promise<Player> {
@@ -47,16 +58,15 @@ class PlayerCollection extends MongoStoredObjectCollection<PlayerMongoData> {
}
async updateStatsForPlayer(playerOrId: OrId<Player>, gameResults: PlayerGameResults & {outcome: OutcomeType}, rulesetUsedOrId: OrId<Ruleset>): Promise<void> {
playerOrId = playerOrId instanceof Player ? playerOrId : await this.read(playerOrId);
rulesetUsedOrId = rulesetUsedOrId instanceof Ruleset ? rulesetUsedOrId : await RulesetCollection.read(rulesetUsedOrId);
playerOrId.updateStats(gameResults, rulesetUsedOrId);
const player = playerOrId instanceof Player ? playerOrId : await this.read(playerOrId);
const ruleset = rulesetUsedOrId instanceof Ruleset ? rulesetUsedOrId : await RulesetCollection().read(rulesetUsedOrId);
player.updateStats(gameResults, ruleset);
this.mongoUpdate({
id: this.idFromRecordOrId(playerOrId),
stats: playerOrId.getStats()?.getData()
id: this.idFromRecordOrId(player),
stats: player.getStats()?.getData()
});
}
}
const PlayerCollectionSingleton = new PlayerCollection(getMongoObjectCollection("players"));
export default PlayerCollectionSingleton;
export default PlayerCollection.getInstance;

View File

@@ -1,21 +1,33 @@
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
import mongo from "mongodb";
import {DEFAULT_RULESET, DEFAULT_RULESET_NAME, RulesetSchema} from "../rulesets";
import {getMongoObjectCollection} from "../database";
import Ruleset from "../Objects/Ruleset";
import {ActiveRecordId} from "../Objects/ActiveRecord";
type RulesetMongoData = RulesetSchema;
type RulesetMongoData = RulesetSchema & {id: ActiveRecordId};
class RulesetCollection extends MongoStoredObjectCollection<RulesetMongoData> {
constructor(collectionClient: mongo.Collection) {
super(collectionClient);
private static instance?: RulesetCollection;
constructor() {
super();
}
static getInstance(): RulesetCollection {
if (RulesetCollection.instance === undefined) {
RulesetCollection.instance = new RulesetCollection();
}
return RulesetCollection.instance;
}
async init() {
this.mongoDbClientCollection = getMongoObjectCollection("rulesets");
}
private async rulesetFrom(data: RulesetMongoData): Promise<Ruleset> {
return new Ruleset(data.id, data);
}
async read(id: string): Promise<Ruleset> {
async read(id: ActiveRecordId): Promise<Ruleset> {
if (id === DEFAULT_RULESET_NAME) {
return new Ruleset(DEFAULT_RULESET_NAME, DEFAULT_RULESET);
}
@@ -26,5 +38,4 @@ class RulesetCollection extends MongoStoredObjectCollection<RulesetMongoData> {
}
}
const RulesetCollectionSingleton = new RulesetCollection(getMongoObjectCollection("users"));
export default RulesetCollectionSingleton;
export default RulesetCollection.getInstance;

View File

@@ -1,54 +1,63 @@
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
import mongo from "mongodb";
import {ActiveRecordId} from "../Objects/ActiveRecord";
import PlayerCollection from "./PlayerCollection";
import SavedGame from "../Objects/SavedGame";
import {getMongoObjectCollection} from "../database";
import {PlayerGameResults} from "../Objects/DefaultStatsMongoData";
import RulesetCollection from "./RulesetCollection";
import {GameSubmission} from "../controllers/statsController";
import {ProcessedGameSubmission, ScoredResultsWithOutcome} from "../Controllers/statsController";
export interface SavedGameMongoData {
id: string;
rulesetUsed: ActiveRecordId;
ruleset: ActiveRecordId;
players: ActiveRecordId[];
results: PlayerGameResults[];
results: ScoredResultsWithOutcome[];
}
class SavedGameCollection extends MongoStoredObjectCollection<SavedGameMongoData> {
constructor(collectionClient: mongo.Collection) {
super(collectionClient);
private static instance?: SavedGameCollection;
constructor() {
super();
}
static getInstance(): SavedGameCollection {
if (SavedGameCollection.instance === undefined) {
SavedGameCollection.instance = new SavedGameCollection();
}
return SavedGameCollection.instance;
}
async init() {
this.mongoDbClientCollection = getMongoObjectCollection("savedGames");
}
private async savedGameFrom(data: SavedGameMongoData): Promise<SavedGame> {
const playerList: {name: string, id: ActiveRecordId}[] = [];
for (const playerId of data.players) {
const player = await PlayerCollection.read(playerId);
const player = await PlayerCollection().read(playerId);
playerList.push({name: player.getNick(), id: playerId})
}
const rulesetUsed = await RulesetCollection.read(data.rulesetUsed);
const rulesetUsed = await RulesetCollection().read(data.ruleset);
return new SavedGame(
data.id,
{name: rulesetUsed.getName(), id: data.rulesetUsed},
{name: rulesetUsed.getName(), id: data.ruleset},
playerList,
data.results);
}
async read(id: string): Promise<SavedGame> {
async read(id: ActiveRecordId): Promise<SavedGame> {
const foundGame = await this.mongoRead(id);
return this.savedGameFrom(foundGame);
}
async create(gameSubmission: GameSubmission): Promise<SavedGame> {
const pids = gameSubmission.players.map(playerIdAndNick => playerIdAndNick.id);
async create(submission: ProcessedGameSubmission): Promise<SavedGame> {
const pids = submission.players.map(playerIdAndNick => playerIdAndNick.id);
return this.savedGameFrom(
await this.mongoCreate({
rulesetUsed: gameSubmission.rulesetId,
ruleset: submission.ruleset,
players: pids,
results: gameSubmission.results})
results: submission.results})
);
}
}
const SavedGameCollectionSingleton = new SavedGameCollection(getMongoObjectCollection("users"));
export default SavedGameCollectionSingleton;
export default SavedGameCollection.getInstance;

View File

@@ -10,6 +10,7 @@ class AccountStats {
constructor(data: AccountStatsMongoData) {
this.data = data;
this.updater = new StatsUpdater();
this.updater.use(data);
}
use(data: AccountStatsMongoData) {
@@ -21,8 +22,8 @@ class AccountStats {
if (this.data) {
for (const playerGameResult of playerGameResults) {
this.updater.updateStats(playerGameResult, ruleset);
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

View File

@@ -1,4 +1,6 @@
export type ActiveRecordId = string;
import mongo from "mongodb";
export type ActiveRecordId = mongo.ObjectId | string;
export type OrId<T extends ActiveRecord> = T | ActiveRecordId;
interface ActiveRecord {

View File

@@ -1,12 +1,11 @@
import {CellDef, DEFAULT_RULESET, DEFAULT_RULESET_NAME, RulesetSchema} from "../rulesets";
import {FieldType} from "../enums";
export enum OutcomeType {
win,
loss,
runnerUp,
draw,
win = "win",
loss = "loss",
runnerUp = "runnerUp",
draw = "draw",
}
export interface PlayerStatsMongoData extends BaseStatsMongoData {}

View File

@@ -5,7 +5,7 @@ export type LoginDetails = { username: string, email: string, password: string }
class KadiUser implements ActiveRecord {
constructor(
private id: string,
private id: ActiveRecordId,
private username: string,
private email: string,
private password: string,

View File

@@ -1,21 +1,14 @@
import {CellValue} from "../controllers/statsController";
import {RulesetSchema} from "../rulesets";
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
import {UpdateError} from "../errors";
import {OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
import PlayerStats from "./PlayerStats";
import Ruleset from "./Ruleset";
export interface CellDetails {
id: string;
value: CellValue;
}
export class Player implements ActiveRecord {
constructor(
private id: ActiveRecordId,
private nick: string,
private stats?: PlayerStats
private stats: PlayerStats
) {}
getId(): ActiveRecordId {
@@ -30,7 +23,7 @@ export class Player implements ActiveRecord {
this.nick = newNick;
}
getStats(): PlayerStats | undefined {
getStats(): PlayerStats {
return this.stats;
}

View File

@@ -9,6 +9,7 @@ class PlayerStats {
constructor(data: PlayerStatsMongoData) {
this.data = data;
this.updater = new StatsUpdater();
this.updater.use(data);
}
use(data: PlayerStatsMongoData) {
@@ -18,6 +19,7 @@ class PlayerStats {
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
this.updater.updateStats(playerGameResults, ruleset);
this.data.gamesPlayed += 1;
}
getData(): PlayerStatsMongoData {

View File

@@ -26,6 +26,10 @@ export class Ruleset implements ActiveRecord {
getCellsInBlock(blockId: string): Record<string, CellDef> {
return Object.assign({}, this.schema.blocks[blockId].cells);
}
getSchemaJSON(): RulesetSchema {
return Object.assign({}, this.schema);
}
}
export default Ruleset;

View File

@@ -1,12 +1,12 @@
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
import {PlayerGameResults} from "./DefaultStatsMongoData";
import {ScoredResultsWithOutcome} from "../Controllers/statsController";
class SavedGame implements ActiveRecord {
constructor(
private id: string,
private rulesetUsed: {name: string, id: ActiveRecordId},
private players: {name: string, id: ActiveRecordId}[],
private results: PlayerGameResults[],
private results: ScoredResultsWithOutcome[],
) {}
getId() {

View File

@@ -60,6 +60,7 @@ abstract class ScoreCellCalculator {
}
hydrateWithJSON(jsonRep: ScoreCellJSONRepresentation): void {
this.reset();
if (jsonRep.value === CellFlag.strike) {
this.struck = true;
}
@@ -67,6 +68,11 @@ abstract class ScoreCellCalculator {
this.value = jsonRep.value;
}
}
reset(): void {
this.struck = false;
this.value = 0;
}
}
class NumberScoreCell extends ScoreCellCalculator {
@@ -78,9 +84,14 @@ class NumberScoreCell extends ScoreCellCalculator {
}
getScore(): number {
if (this.isStruck()) {
return 0;
}
else {
return this.value as number;
}
}
}
class BoolScoreCell extends ScoreCellCalculator {
protected static readonly fieldType = FieldType.bool;
@@ -101,6 +112,11 @@ class BoolScoreCell extends ScoreCellCalculator {
return 0;
}
}
reset(): void {
super.reset();
this.value = false;
}
}
class SuperkadiScoreCell extends ScoreCellCalculator {

View File

@@ -1,4 +1,3 @@
import {RulesetSchema} from "../rulesets";
import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator";
import {UpdateError} from "../errors";
import {FieldType} from "../enums";
@@ -8,7 +7,8 @@ import {
BoolFieldStatsMongoData,
CellStatsMongoData,
OutcomeType,
PlayerGameResults, RulesetStatsMongoData,
PlayerGameResults,
RulesetStatsMongoData,
TotalFieldStatsMongoData
} from "./DefaultStatsMongoData";
import Ruleset from "./Ruleset";
@@ -30,15 +30,15 @@ class StatsUpdater {
this.validationRuleset = ruleset;
this.calculator = new ScoreCalculator(ruleset);
this.calculator.hydrateWithJSON(playerGameResults as ScoreCardJSONRepresentation);
this.currentStatsObject = this.data.statsByRuleset[ruleset.getId()];
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 === "win");
this.currentStatsObject.draws += Number(playerGameResults.outcome === "draw");
this.currentStatsObject.runnerUps += Number(playerGameResults.outcome === "runnerUp");
this.currentStatsObject.losses += Number(playerGameResults.outcome === "loss");
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
@@ -66,9 +66,10 @@ class StatsUpdater {
}
private updateCellStatsByIds(ids: {cellId: string, blockId: string}) {
const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.getId()});
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});
cellStats.runningTotal += cellScore;
if (cellScore > 0 && cellFieldType === FieldType.bool) {
(cellStats as BoolFieldStatsMongoData).total += 1;
}

View File

@@ -1,7 +1,8 @@
import express from "express";
import * as statsController from "../controllers/statsController";
import * as KadiUserController from "../controllers/kadiUserController"
import * as statsController from "../Controllers/statsController";
import * as KadiUserController from "../Controllers/kadiUserController"
import {requireAuthenticated} from "./routerMiddleware";
import * as rulesetController from "../Controllers/rulesetController";
const router = express.Router();
@@ -21,4 +22,8 @@ router.delete("/guest/:id", requireAuthenticated, KadiUserController.deleteGuest
router.get("/games", requireAuthenticated, statsController.listGames);
router.post("/games", requireAuthenticated, statsController.saveGame);
//Stats
router.get("/stats", requireAuthenticated, statsController.getStats);
router.get("/ruleset/:id", rulesetController.getRuleset);
export default router;

View File

@@ -1,4 +1,4 @@
import express, {NextFunction} from "express";
import express from "express";
import routers from "./routers";
import {LoginDetails} from "../Objects/KadiUser";
import {requireAuthenticated} from "./routerMiddleware";
@@ -22,6 +22,7 @@ router.get("/**", requireAuthenticated, (req, res) => {
});
const topLevelErrorHandler: express.ErrorRequestHandler = (err, req, res, next) => {
console.log(err.message);
if (err instanceof GenericPersistenceError) {
res.status(500).send({message: "An internal error occurred accessing the database."});
}

View File

@@ -1,17 +1,13 @@
import express from "express";
import {requireAuthenticated, requireNotAuthenticated} from "../passport-config";
import * as signup from "../controllers/signupController";
import * as signup from "../Controllers/signupController";
import {requireAuthenticated, requireNotAuthenticated} from "./routerMiddleware";
const router = express.Router();
router.get("/login", requireNotAuthenticated, signup.showLoginPage);
router.post("/login", requireNotAuthenticated, signup.loginUser);
router.get("/register", requireNotAuthenticated, signup.showRegistrationPage);
router.post("/register", requireNotAuthenticated, signup.registerNewUser);
router.get("/logout", requireAuthenticated, signup.logoutUser);
export default router;

View File

@@ -1,6 +1,10 @@
import {MongoClient, Db} from "mongodb";
import Settings from "./server-config.json";
import {GenericPersistenceError, MongoError} from "./errors";
import KadiUserCollection from "./ObjectCollections/KadiUserCollection";
import PlayerCollection from "./ObjectCollections/PlayerCollection";
import RulesetCollection from "./ObjectCollections/RulesetCollection";
import SavedGameCollection from "./ObjectCollections/SavedGameCollection";
let SessionDbClient: Db;
@@ -21,6 +25,13 @@ export function getMongoObjectCollection(collectionName: string) {
}
}
export async function initCollections() {
KadiUserCollection().init();
PlayerCollection().init();
SavedGameCollection().init();
RulesetCollection().init();
}
type CallbackWrapper = <T>(query: () => T) => Promise<T>;
export const tryQuery: CallbackWrapper = async (cb) => {
try {

View File

@@ -1,15 +1,16 @@
import express, {NextFunction, Request, Response} from "express";
import express from "express";
import Settings from "./server-config.json";
import flash from "express-flash";
import passport from "passport";
import session from "express-session";
import MainRouter from "./routers/mainRouter";
import {initMongoSessionClient} from "./database";
import MainRouter from "./Routers/mainRouter";
import {initCollections, initMongoSessionClient} from "./database";
import {Strategy as LocalStrategy} from "passport-local";
import {authenticateKadiUser, deserializeKadiUser, serializeKadiUser} from "./controllers/signupController";
import {authenticateKadiUser, deserializeKadiUser, serializeKadiUser} from "./Controllers/signupController";
async function startApp() {
await initMongoSessionClient();
await initCollections();
passport.use(new LocalStrategy({ usernameField: "email" }, authenticateKadiUser));
passport.serializeUser(serializeKadiUser);
passport.deserializeUser(deserializeKadiUser);

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */