I think it's done?

This commit is contained in:
Daniel Ledda
2020-07-17 22:23:05 +02:00
parent 4542036b77
commit 42b7560d6d
21 changed files with 443 additions and 272 deletions

View File

@@ -1,14 +1,20 @@
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
import mongo from "mongodb";
import {CredentialsTakenError, GenericPersistenceError} from "../errors";
import StoredPlayers from "../ObjectCollections/PlayerCollection";
import {SupportedLang} from "../enums";
import {AccountStatsMongoData, defaultAccountStatsMongoData} from "../Objects/DefaultStatsMongoData";
import mongo from "mongodb";
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} from "../Objects/ActiveRecord";
import {SavedGameData} from "../Objects/savedGame";
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 {GameSubmission} from "../controllers/statsController";
import Ruleset from "../Objects/Ruleset";
import RulesetCollection from "./RulesetCollection";
import AccountStats from "../Objects/AccountStats";
export interface KadiUserMongoData {
id: string;
@@ -20,7 +26,7 @@ export interface KadiUserMongoData {
player: ActiveRecordId;
guests: ActiveRecordId[];
accountStats: AccountStatsMongoData;
savedGames: SavedGameData[];
savedGames: ActiveRecordId[];
}
class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> {
@@ -28,7 +34,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
super(collectionClient);
}
private storedUserFrom(data: KadiUserMongoData): KadiUser {
private kadiUserFrom(data: KadiUserMongoData): KadiUser {
return new KadiUser(
data.id,
data.username,
@@ -37,20 +43,15 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
data.lang);
}
async read(id: string): Promise<KadiUser | null> {
async read(id: string): Promise<KadiUser> {
const foundUser = await this.mongoRead(id);
if (foundUser) {
return this.storedUserFrom(foundUser);
}
else {
return null;
}
return this.kadiUserFrom(foundUser);
}
async findByEmail(emailQuery: string): Promise<KadiUser | null> {
const foundUser = await this.mongoFindByAttribute("email", emailQuery);
if (foundUser) {
return this.storedUserFrom(foundUser);
return this.kadiUserFrom(foundUser);
}
else {
return null;
@@ -69,7 +70,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
}
private async addNewUser(loginDetails: LoginDetails): Promise<KadiUser> {
const newPlayer = await StoredPlayers.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,
@@ -82,7 +83,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
guests: [],
savedGames: [],
});
return this.storedUserFrom(newUser);
return this.kadiUserFrom(newUser);
}
async userWithEmailExists(email: string): Promise<boolean> {
@@ -98,7 +99,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
async getSerializedAuthUser(id: string): Promise<KadiUser> {
const foundUser = await this.mongoRead(id);
if (foundUser) {
return this.storedUserFrom(foundUser);
return this.kadiUserFrom(foundUser);
}
else {
throw new GenericPersistenceError("User not found!");
@@ -108,6 +109,61 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
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(
{_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(
{_id: this.idFromRecordOrId(userOrId)},
{$pull: {guests: this.idFromRecordOrId(guestOrGuestId)}});
return deletedGuest;
}
async getAllGuestsForUser(userOrId: OrId<KadiUser>): Promise<Promise<Player>[]> {
const guestIdList = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.guests;
return 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);
}
async getSavedGamesForUser(userOrId: OrId<KadiUser>): Promise<Promise<SavedGame>[]> {
const savedGameIds = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.savedGames;
return 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(
{_id: this.idFromRecordOrId(userOrId)},
{$push: {savedGames: newGame.getId()}});
return newGame;
}
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);
accountStatsObject.updateStats(gameResults, rulesetUsed);
this.mongoUpdate({
id: this.idFromRecordOrId(userId),
accountStats: accountStatsObject.getData()
});
}
}
const KadiUserCollectionSingleton = new KadiUserCollection(getMongoObjectCollection("users"));

View File

@@ -1,6 +1,6 @@
import mongo from "mongodb";
import {tryQuery} from "./database";
import {MongoError} from "../errors";
import {tryQuery} from "../database";
import {InvalidIdError, MongoError} from "../errors";
import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord";
@@ -19,10 +19,15 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
});
}
protected async mongoRead(id: string): Promise<IRawData | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({_id: id})
);
protected async mongoRead(id: string): Promise<IRawData> {
return tryQuery(async () => {
const result = await this.mongoDbClientCollection.findOne({_id: id});
if (result) {
return result;
} else {
throw new InvalidIdError(`Object in collection "${typeof this}" with id ${JSON.stringify(id)} not found!`);
}
});
}
protected async mongoFindByAttribute(attribute: string, value: any): Promise<IRawData | null> {
@@ -31,7 +36,7 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
);
}
protected async mongoDelete(objectId: ActiveRecordId, returnObject?: boolean): Promise<IRawData | null | void> {
protected async mongoDelete(objectId: ActiveRecordId, returnObject?: boolean): Promise<IRawData | void> {
let deletedObject;
if (returnObject ?? true) {
deletedObject = await this.mongoRead(objectId);
@@ -40,15 +45,19 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
if (deleteWriteOpResult.result.ok === 1) {
return deletedObject;
} else {
throw new MongoError(`Error deleting the object with id: ${JSON.stringify(objectId)}`);
throw new MongoError(`Error deleting the object in collection "${typeof this}" with id: ${JSON.stringify(objectId)}`);
}
}
protected async mongoSave(object: IRawData) {
protected async mongoUpdate(object: Partial<IRawData> & {id: ActiveRecordId}) {
await tryQuery(() =>
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();
}
}

View File

@@ -1,30 +1,62 @@
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
import mongo from "mongodb";
import {defaultPlayerStatsMongoData, PlayerStatsMongoData} from "../Objects/DefaultStatsMongoData";
import {getMongoObjectCollection} from "./database";
import {
defaultPlayerStatsMongoData,
OutcomeType,
PlayerGameResults,
PlayerStatsMongoData
} from "../Objects/DefaultStatsMongoData";
import {getMongoObjectCollection} from "../database";
import Player from "../Objects/Player";
import PlayerStats from "../Objects/PlayerStats";
import {ActiveRecordId, OrId} from "../Objects/ActiveRecord";
import Ruleset from "../Objects/Ruleset";
import RulesetCollection from "./RulesetCollection";
export interface MongoStoredPlayerData {
export interface PlayerMongoData {
id: string;
nick: string;
stats?: PlayerStatsMongoData;
}
class PlayerCollection extends MongoStoredObjectCollection<MongoStoredPlayerData> {
class PlayerCollection extends MongoStoredObjectCollection<PlayerMongoData> {
constructor(collectionClient: mongo.Collection) {
super(collectionClient);
}
private storedPlayerFrom(data: MongoStoredPlayerData): Player {
private playerFrom(data: PlayerMongoData): Player {
return new Player(data.id, data.nick, data.stats ? new PlayerStats(data.stats) : undefined);
}
async create(nick: string): Promise<Player> {
const newPlayer = {nick, stats: defaultPlayerStatsMongoData()};
return this.storedPlayerFrom(await this.mongoCreate(newPlayer));
return this.playerFrom(await this.mongoCreate(newPlayer));
}
async read(id: ActiveRecordId): Promise<Player> {
return this.playerFrom(await this.mongoRead(id));
}
async delete(id: ActiveRecordId): Promise<Player> {
return this.playerFrom(await this.mongoDelete(id, true) as PlayerMongoData);
}
async save(player: Player): Promise<Player> {
await this.mongoUpdate({id: player.getId(), nick: player.getNick(), stats: player.getStats()?.getData()});
return player;
}
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);
this.mongoUpdate({
id: this.idFromRecordOrId(playerOrId),
stats: playerOrId.getStats()?.getData()
});
}
}
const StoredPlayers = new PlayerCollection(getMongoObjectCollection("players"));
export default StoredPlayers;
const PlayerCollectionSingleton = new PlayerCollection(getMongoObjectCollection("players"));
export default PlayerCollectionSingleton;

View File

@@ -0,0 +1,30 @@
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";
type RulesetMongoData = RulesetSchema;
class RulesetCollection extends MongoStoredObjectCollection<RulesetMongoData> {
constructor(collectionClient: mongo.Collection) {
super(collectionClient);
}
private async rulesetFrom(data: RulesetMongoData): Promise<Ruleset> {
return new Ruleset(data.id, data);
}
async read(id: string): Promise<Ruleset> {
if (id === DEFAULT_RULESET_NAME) {
return new Ruleset(DEFAULT_RULESET_NAME, DEFAULT_RULESET);
}
else {
const foundRuleset = await this.mongoRead(id);
return this.rulesetFrom(foundRuleset);
}
}
}
const RulesetCollectionSingleton = new RulesetCollection(getMongoObjectCollection("users"));
export default RulesetCollectionSingleton;

View File

@@ -0,0 +1,54 @@
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";
export interface SavedGameMongoData {
id: string;
rulesetUsed: ActiveRecordId;
players: ActiveRecordId[];
results: PlayerGameResults[];
}
class SavedGameCollection extends MongoStoredObjectCollection<SavedGameMongoData> {
constructor(collectionClient: mongo.Collection) {
super(collectionClient);
}
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);
playerList.push({name: player.getNick(), id: playerId})
}
const rulesetUsed = await RulesetCollection.read(data.rulesetUsed);
return new SavedGame(
data.id,
{name: rulesetUsed.getName(), id: data.rulesetUsed},
playerList,
data.results);
}
async read(id: string): 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);
return this.savedGameFrom(
await this.mongoCreate({
rulesetUsed: gameSubmission.rulesetId,
players: pids,
results: gameSubmission.results})
);
}
}
const SavedGameCollectionSingleton = new SavedGameCollection(getMongoObjectCollection("users"));
export default SavedGameCollectionSingleton;

View File

@@ -1,15 +1,14 @@
import {Ruleset} from "../rulesets";
import {RulesetSchema} from "../rulesets";
import {AccountStatsMongoData, OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
import {UpdateError} from "../errors";
import StatsUpdater from "./StatsUpdater";
import Ruleset from "./Ruleset";
class AccountStats {
private data?: AccountStatsMongoData;
private data: AccountStatsMongoData;
private readonly updater: StatsUpdater;
constructor(data?: AccountStatsMongoData) {
if (data) {
constructor(data: AccountStatsMongoData) {
this.data = data;
}
this.updater = new StatsUpdater();
}
@@ -18,16 +17,22 @@ class AccountStats {
this.updater.use(data);
}
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
updateStats(playerGameResults: (PlayerGameResults & {outcome: OutcomeType})[], ruleset: Ruleset): void {
if (this.data) {
this.updater.updateStats(playerGameResults, ruleset);
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.`);
}
}
getData(): AccountStatsMongoData {
return this.data;
}
}
export default AccountStats;

View File

@@ -1,4 +1,5 @@
export type ActiveRecordId = string;
export type OrId<T extends ActiveRecord> = T | ActiveRecordId;
interface ActiveRecord {
getId(): ActiveRecordId;

View File

@@ -1,12 +1,16 @@
import {CellDef, DEFAULT_RULESET, DEFAULT_RULESET_NAME, Ruleset} from "../rulesets";
import {CellDef, DEFAULT_RULESET, DEFAULT_RULESET_NAME, RulesetSchema} from "../rulesets";
import {FieldType} from "../enums";
export type OutcomeType = "win" | "loss" | "runnerUp" | "draw";
export interface PlayerStatsMongoData extends BaseStatsMongoData {}
export interface AccountStatsMongoData extends BaseStatsMongoData {
timesNoWinner: number;
export enum OutcomeType {
win,
loss,
runnerUp,
draw,
}
export interface PlayerStatsMongoData extends BaseStatsMongoData {}
export interface AccountStatsMongoData extends BaseStatsMongoData {}
export interface BaseStatsMongoData {
statsByRuleset: Record<string, RulesetStatsMongoData>
gamesPlayed: number;
@@ -130,7 +134,7 @@ function defaultBlockStatsMongoData(cellSchemas: Record<string, CellDef>, hasBon
}
}
function defaultRulesetStatsMongoData(ruleset: Ruleset): RulesetStatsMongoData {
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);
@@ -155,7 +159,7 @@ function defaultBaseStatsMongoData(): BaseStatsMongoData {
}
export function defaultAccountStatsMongoData(): AccountStatsMongoData {
return {...defaultBaseStatsMongoData(), timesNoWinner: 0};
return defaultBaseStatsMongoData();
}
export function defaultPlayerStatsMongoData(): PlayerStatsMongoData {

View File

@@ -1,16 +1,17 @@
import {CellValue} from "../controllers/statsController";
import {Ruleset} from "../rulesets";
import {ActiveRecordId} from "./ActiveRecord";
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 {
export class Player implements ActiveRecord {
constructor(
private id: ActiveRecordId,
private nick: string,
@@ -25,11 +26,15 @@ export class Player {
return this.nick;
}
async setNick(newNick: string): Promise<void> {
setNick(newNick: string): void {
this.nick = newNick;
}
async updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset) {
getStats(): PlayerStats | undefined {
return this.stats;
}
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
if (this.stats) {
this.stats.updateStats(playerGameResults, ruleset);
}

View File

@@ -1,14 +1,13 @@
import {Ruleset} from "../rulesets";
import {RulesetSchema} from "../rulesets";
import {OutcomeType, PlayerGameResults, PlayerStatsMongoData} from "./DefaultStatsMongoData";
import StatsUpdater from "./StatsUpdater";
import Ruleset from "./Ruleset";
class PlayerStats {
private data?: PlayerStatsMongoData;
private data: PlayerStatsMongoData;
private readonly updater: StatsUpdater;
constructor(data?: PlayerStatsMongoData) {
if (data) {
constructor(data: PlayerStatsMongoData) {
this.data = data;
}
this.updater = new StatsUpdater();
}
@@ -20,6 +19,10 @@ class PlayerStats {
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
this.updater.updateStats(playerGameResults, ruleset);
}
getData(): PlayerStatsMongoData {
return this.data;
}
}
export default PlayerStats;

31
src/Objects/Ruleset.ts Normal file
View File

@@ -0,0 +1,31 @@
import {BlockDef, CellDef, RulesetSchema} from "../rulesets";
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
export class Ruleset implements ActiveRecord {
constructor(
private id: ActiveRecordId,
private schema: RulesetSchema,
) {}
getId(): ActiveRecordId {
return this.id;
}
getName(): string {
return this.schema.label;
}
rename(newName: string) {
this.schema.label = newName;
}
getBlocks(): Record<string, BlockDef> {
return Object.assign({}, this.schema.blocks);
}
getCellsInBlock(blockId: string): Record<string, CellDef> {
return Object.assign({}, this.schema.blocks[blockId].cells);
}
}
export default Ruleset;

29
src/Objects/SavedGame.ts Executable file
View File

@@ -0,0 +1,29 @@
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
import {PlayerGameResults} from "./DefaultStatsMongoData";
class SavedGame implements ActiveRecord {
constructor(
private id: string,
private rulesetUsed: {name: string, id: ActiveRecordId},
private players: {name: string, id: ActiveRecordId}[],
private results: PlayerGameResults[],
) {}
getId() {
return this.id;
}
getPlayers() {
return this.players;
}
getResults() {
return this.results;
}
getRulesetUsed() {
return this.rulesetUsed;
}
}
export default SavedGame;

View File

@@ -1,5 +1,6 @@
import {BlockDef, Ruleset} from "../rulesets";
import {BlockDef, RulesetSchema} from "../rulesets";
import ScoreBlockCalculator, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlockCalculator";
import Ruleset from "./Ruleset";
export type CellLocation = { blockId: string, cellId: string };
@@ -11,7 +12,7 @@ class ScoreCalculator {
private readonly blocks: ScoreBlockCalculator[];
constructor(gameSchema: Ruleset) {
this.blocks = ScoreCalculator.generateBlocks(gameSchema.blocks);
this.blocks = ScoreCalculator.generateBlocks(gameSchema.getBlocks());
}
hydrateWithJSON(jsonRep: ScoreCardJSONRepresentation): void {

View File

@@ -1,4 +1,4 @@
import {Ruleset} from "../rulesets";
import {RulesetSchema} from "../rulesets";
import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator";
import {UpdateError} from "../errors";
import {FieldType} from "../enums";
@@ -11,6 +11,7 @@ import {
PlayerGameResults, RulesetStatsMongoData,
TotalFieldStatsMongoData
} from "./DefaultStatsMongoData";
import Ruleset from "./Ruleset";
class StatsUpdater {
private data?: BaseStatsMongoData;
@@ -29,8 +30,8 @@ class StatsUpdater {
this.validationRuleset = ruleset;
this.calculator = new ScoreCalculator(ruleset);
this.calculator.hydrateWithJSON(playerGameResults as ScoreCardJSONRepresentation);
this.currentStatsObject = this.data.statsByRuleset[ruleset.id];
for (const blockId in ruleset.blocks) {
this.currentStatsObject = this.data.statsByRuleset[ruleset.getId()];
for (const blockId in ruleset.getBlocks()) {
this.updateBlockStats(blockId);
}
this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal());
@@ -58,15 +59,15 @@ class StatsUpdater {
if (this.calculator!.blockWithIdHasBonus(blockId)) {
blockStats.timesHadBonus! += 1;
}
for (const cellId in this.validationRuleset!.blocks[blockId].cells) {
for (const cellId in this.validationRuleset!.getBlocks()[blockId].cells) {
this.updateCellStatsByIds({cellId, blockId});
}
}
}
private updateCellStatsByIds(ids: {cellId: string, blockId: string}) {
const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.id});
const cellFieldType = this.validationRuleset?.blocks[ids.blockId].cells[ids.cellId].fieldType;
const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.getId()});
const cellFieldType = this.validationRuleset?.getBlocks()[ids.blockId].cells[ids.cellId].fieldType;
const cellScore = this.calculator!.getCellScoreByLocation({...ids});
if (cellScore > 0 && cellFieldType === FieldType.bool) {
(cellStats as BoolFieldStatsMongoData).total += 1;

View File

@@ -1,17 +0,0 @@
import {GameSubmission} from "../controllers/statsController";
import {StoredObjectCollection, StoredObject, StoredObjectId} from "./utils";
import {PlayerGameResults} from "./Stats";
export interface SavedGameData {
id: string;
rulesetUsed: RulesetData;
players: StoredObjectId[];
results: PlayerGameResults[];
}
export interface StoredSavedGame extends StoredObject {
}
export interface StoredSavedGameCollection extends StoredObjectCollection<StoredSavedGame> {
createFromGameSubmission(submission: GameSubmission): Promise<StoredSavedGame>;
}

View File

@@ -2,6 +2,7 @@ import {RequestHandler} from "express";
import KadiUser from "../Objects/KadiUser";
import Player from "../Objects/Player";
import KadiUserCollection from "../ObjectCollections/KadiUserCollection";
import PlayerCollection from "../ObjectCollections/PlayerCollection";
export const currentUserDetails: RequestHandler = async (req, res) => {
if (req.isAuthenticated()) {
@@ -26,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.addGuestForAccount(req.body.guestName);
const newGuest: Player = await KadiUserCollection.addGuestForUser(user, req.body.guestName);
res.send({
username: user.getUsername(),
userId: user.getId(),
@@ -46,7 +47,9 @@ export const updateGuest: RequestHandler = async (req, res) => {
const {id: guestId} = req.params;
if (req.body.newName) {
const {newName} = req.body;
const updatedGuest = await KadiUserCollection.updateGuestForAccount({id: guestId, newNick: newName});
const guest = await PlayerCollection.read(guestId);
guest.setNick(newName);
const updatedGuest = await PlayerCollection.save(guest);
res.status(200).send({
userId: user.getId(),
username: user.getUsername(),
@@ -64,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 KadiUserCollection.getGuestForAccount(guestId, user.getId());
const guest = await PlayerCollection.read(guestId);
res.status(200).send({
userId: user.getId(),
username: user.getUsername(),
@@ -75,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.deleteGuestForAccount(guestId, user.getId());
const deletedGuest = await KadiUserCollection.deleteGuestFromUser(user, guestId);
res.status(200).send({
userId: user.getId(),
username: user.getUsername(),
@@ -85,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.getGuestsForAccount(user.getId());
const guests = await KadiUserCollection.getAllGuestsForUser(user);
res.status(200).send({
userId: user.getId(),
username: user.getUsername(),
@@ -95,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.getAllGuestsForAccount(user.getId());
const mainPlayer = await KadiUserCollection.getMainPlayerForAccount(user.getId());
const guests = await KadiUserCollection.getAllGuestsForUser(user);
const mainPlayer = await KadiUserCollection.getMainPlayerForUser(user);
res.status(200).send({guests, mainPlayer});
};

View File

@@ -1,50 +1,24 @@
import DbUser, { IDbUser } from "../models/dbUser_old";
import { RequestHandler } from "express";
import Player, { IPlayer } from "../models/StoredPlayer";
const DEFAULT_RULESET = "DEFAULT_RULESET";
const UPPER_BONUS_THRESHOLD = 63;
const UPPER_BONUS = 35;
const FULL_HOUSE_SCORE = 25;
const SML_STRAIGHT_SCORE = 30;
const LG_STRAIGHT_SCORE = 40;
const YAHTZEE_SCORE = 50;
import KadiUserCollection from "../ObjectCollections/KadiUserCollection";
import PlayerCollection from "../ObjectCollections/PlayerCollection";
import Player from "../Objects/Player";
import {RequestHandler} from "express";
import ScoreCalculator from "../Objects/ScoreCalculator";
import KadiUser from "../Objects/KadiUser";
import {CellFlag} from "../enums";
import RulesetCollection from "../ObjectCollections/RulesetCollection";
import Ruleset from "../Objects/Ruleset";
import {OutcomeType} from "../Objects/DefaultStatsMongoData";
export interface GameSubmission {
ruleset: string;
rulesetId: string;
players: { id: string; nick: string }[];
results: PlayerGameResult[];
}
interface ScoredResult extends ScoreTotalFields, PlayerGameResult {}
interface ScoreTotalFields {
topBonus: boolean;
topSubtotal: number;
top: number;
bottom: number;
total: number;
}
type PlayerGameResult = { playerId: string; blocks: Record<BlockName, Block> };
type BlockName = "top" | "bottom";
type Block = { cells: Record<CellName, StandardCell> };
type CellName =
| "aces"
| "twos"
| "threes"
| "fours"
| "fives"
| "sixes"
| "three_kind"
| "four_kind"
| "full_house"
| "sml_straight"
| "lg_straight"
| "yahtzee"
| "chance";
type StandardCell = { value: CellValue };
export type CellValue = number | boolean | "cellFlagStrike";
type PlayerGameResult = { playerId: string; blocks: Record<string, Block> };
type Block = { cells: Record<string, Cell> };
type Cell = { value: CellValue };
export type CellValue = number | boolean | CellFlag.strike;
enum ResultType {
winner,
drawn,
@@ -52,15 +26,14 @@ enum ResultType {
loser,
}
type ScoredResults = {score: number, results: PlayerGameResult};
type ScoredResultsWithOutcome = {score: number, results: PlayerGameResult & {outcome: OutcomeType}};
export const listGames: RequestHandler = async (req, res) => {
const user = req.user as IDbUser;
const dbUser = await DbUser.findById(user.id, {
"savedGames._id": 1,
"savedGames.results": 1,
"savedGames.createdAt": 1,
});
if (dbUser) {
res.json({ games: dbUser.savedGames });
const user = req.user as KadiUser;
const gamesList = await KadiUserCollection.getSavedGamesForUser(user);
if (gamesList) {
res.json({ games: gamesList });
}
else {
res.sendStatus(404);
@@ -68,172 +41,118 @@ export const listGames: RequestHandler = async (req, res) => {
};
export const saveGame: RequestHandler = async (req, res) => {
const user = req.user as IDbUser;
const user = req.user as KadiUser;
const submission = req.body as GameSubmission;
const newGuests: IPlayer[] = await addNewGuests(submission, user);
const newGuests: Player[] = await addNewGuests(submission, user);
if (newGuests.length > 0) {
fillOutSubmissionWithNewIds(submission, newGuests);
}
const newGame = await user.addGame(submission);
if (submission.ruleset === DEFAULT_RULESET) {
processStandardStatistics(submission.results, user);
}
const newGame = await KadiUserCollection.addGameForUser(user, submission);
processStats(await RulesetCollection.read(submission.rulesetId), submission.results, user);
res.send({ message: "Game submitted successfully!", newGame: newGame });
};
async function addNewGuests(submission: GameSubmission, user: IDbUser): Promise<IPlayer[]> {
const newGuestIds: IPlayer[] = [];
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: IPlayer = await user.addGuest(playerDetails.nick);
const newGuest: Player = await KadiUserCollection.addGuestForUser(user, playerDetails.nick);
newGuestIds.push(newGuest);
}
}
return newGuestIds;
}
function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: IPlayer[]): GameSubmission {
function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: Player[]): GameSubmission {
for (const newGuest of newGuestList) {
const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.nick);
const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.getNick());
if (gameResultsFromNewGuest) {
gameResultsFromNewGuest.playerId = newGuest.id;
gameResultsFromNewGuest.playerId = newGuest.getId();
}
const playerEntryForNewGuest = submission.players.find((player) => player.id === newGuest.nick);
const playerEntryForNewGuest = submission.players.find((player) => player.id === newGuest.getNick());
if (playerEntryForNewGuest) {
playerEntryForNewGuest.id = newGuest.id;
playerEntryForNewGuest.id = newGuest.getId();
}
}
return submission;
}
function processStandardStatistics(results: PlayerGameResult[], account: IDbUser) {
let scoredResults: ScoredResult[] = [];
async function processStats(ruleset: Ruleset, results: PlayerGameResult[], account: KadiUser) {
const calc = new ScoreCalculator(ruleset);
let playerScoreList: ScoredResults[] = [];
for (const result of results) {
const scoredResult = {
...result,
...getStandardScoreFields(result),
};
scoredResults.push(scoredResult);
updatePlayerStats(result.playerId, scoredResult);
calc.hydrateWithJSON(result);
playerScoreList.push({
score: calc.getTotal(),
results: result
});
}
const { wasDraw } = incrementPlayerPlacings(scoredResults);
if (wasDraw) {
DbUser.incrementTimesNoWinner(account.id);
}
DbUser.incrementGamesPlayed(account.id);
const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList, ruleset);
updateStatsForIndividualPlayers(playerScoreListWithOutcomes, ruleset);
const gameResults = playerScoreListWithOutcomes.map(scoredResults => scoredResults.results);
await KadiUserCollection.updateAccountStats(account.getId(), gameResults, ruleset);
}
async function updatePlayerStats(playerId: string, result: ScoredResult) {
const player: IPlayer = await Player.findById(playerId) as IPlayer;
for (const blockId in result.blocks) {
const cells = result.blocks[blockId as BlockName].cells;
for (const cellId in cells) {
Player.updateCellStats(player.id, {id: cellId, value: cells[cellId as CellName].value});
}
if (result.topBonus) {
Player.incrementBonus(player.id);
}
}
Player.incrementGamesPlayed(player.id);
}
function incrementPlayerPlacings(scoredResults: ScoredResult[]) {
scoredResults = sortDescendingByScore(scoredResults);
const placingFacts: { wasDraw: boolean } = { wasDraw: false };
function updateScoreListWithOutcomes(playerScoreList: ScoredResults[], rulesetUsed: Ruleset): ScoredResultsWithOutcome[] {
playerScoreList = sortDescendingByScore(playerScoreList);
const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = playerScoreList.map(scoredResults => {
const newResults = {...scoredResults.results, outcome: OutcomeType.loss};
return {...scoredResults, results: newResults};
});
let runnerUpsStart: number;
if (scoredResults[0].total !== scoredResults[1].total) {
Player.incrementWinFor(scoredResults[0].playerId);
if (playerScoreListWithOutcomes[0].score !== playerScoreListWithOutcomes[1].score) {
playerScoreListWithOutcomes[0].results.outcome = OutcomeType.win;
runnerUpsStart = 1;
}
else {
runnerUpsStart = icrmtPlayerDrawsTilScoreChange(scoredResults);
placingFacts.wasDraw = true;
runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes, rulesetUsed);
}
const losersStart = icrmtPlayerRunnerUpsTilScoreChange(
scoredResults.slice(runnerUpsStart)
);
icrmtPlayerLosses(scoredResults.slice(losersStart));
return placingFacts;
const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes.slice(runnerUpsStart), rulesetUsed);
updateScoreListWithLosses(playerScoreListWithOutcomes.slice(losersStart), rulesetUsed);
return playerScoreListWithOutcomes;
}
function icrmtPlayerDrawsTilScoreChange(scoredResults: ScoredResult[]): number {
for (let i = 0; i < scoredResults.length; i++) {
if (scoredResults[i].total === scoredResults[0].total) {
Player.incrementDrawFor(scoredResults[i].playerId);
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;
}
else {
return i;
}
}
return scoredResults.length;
return playerScoreList.length;
}
function icrmtPlayerRunnerUpsTilScoreChange(scoredResults: ScoredResult[]): number {
for (let i = 0; i < scoredResults.length; i++) {
if (scoredResults[i].total === scoredResults[0].total) {
Player.incrementRunnerUpFor(scoredResults[i].playerId);
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;
}
else {
return i;
}
}
return scoredResults.length;
return playerScoreList.length;
}
function icrmtPlayerLosses(scoredResults: ScoredResult[]): void {
for (const scoredResult of scoredResults) {
Player.incrementLossFor(scoredResult.playerId);
function updateScoreListWithLosses(scoreResultsList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset) {
for (const lostPlayerResults of scoreResultsList) {
lostPlayerResults.results.outcome = OutcomeType.loss;
}
}
function sortDescendingByScore(scoredResults: ScoredResult[]) {
return scoredResults.sort((a, b) => b.total - a.total);
function sortDescendingByScore(playerScoreList: ScoredResults[]) {
return playerScoreList.sort((a, b) => b.score - a.score);
}
function getStandardScoreFields(result: PlayerGameResult): ScoreTotalFields {
const scoreFields: ScoreTotalFields = { topBonus: false, topSubtotal: 0, top: 0, bottom: 0, total: 0 };
scoreFields.topSubtotal = topSubtotal(result.blocks.top.cells);
scoreFields.top = scoreFields.topSubtotal;
if (scoreFields.topSubtotal >= UPPER_BONUS_THRESHOLD) {
scoreFields.topBonus = true;
scoreFields.top += UPPER_BONUS;
function updateStatsForIndividualPlayers(playerScoreListWithOutcomes: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): void {
for (const scoredResults of playerScoreListWithOutcomes) {
PlayerCollection.updateStatsForPlayer(
scoredResults.results.playerId,
{...scoredResults.results, outcome: scoredResults.results.outcome},
rulesetUsed);
}
scoreFields.bottom = bottomTotal(result.blocks.bottom.cells);
scoreFields.total = scoreFields.top + scoreFields.bottom;
return scoreFields;
}
function topSubtotal(topResult: Record<CellName, StandardCell>) {
return (
cellScore(topResult.aces) +
cellScore(topResult.twos) * 2 +
cellScore(topResult.threes) * 3 +
cellScore(topResult.fours) * 4 +
cellScore(topResult.fives) * 5 +
cellScore(topResult.sixes) * 6
);
}
function bottomTotal(bottomResult: Record<CellName, StandardCell>) {
return (
cellScore(bottomResult.three_kind) +
cellScore(bottomResult.four_kind) +
cellScore(bottomResult.full_house) * FULL_HOUSE_SCORE +
cellScore(bottomResult.sml_straight) * SML_STRAIGHT_SCORE +
cellScore(bottomResult.lg_straight) * LG_STRAIGHT_SCORE +
cellScore(bottomResult.yahtzee) * YAHTZEE_SCORE +
cellScore(bottomResult.chance)
);
}
function cellScore(cell: StandardCell) {
if (cell.value === "cellFlagStrike" || cell.value === false) {
return 0;
} else if (cell.value === true) {
return 1;
}
return cell.value;
}

View File

@@ -38,3 +38,10 @@ export class CredentialsTakenError extends KadiError {
this.name = "CredentialsTakenError";
}
}
export class InvalidIdError extends GenericPersistenceError {
constructor(message: string) {
super(message);
this.name = "InvalidIdError";
}
}

View File

@@ -1,21 +1,21 @@
import express from "express";
import {requireAuthenticated} from "../passport-config";
import * as statsController from "../controllers/statsController";
import * as dbUserController from "../controllers/kadiUserController"
import * as KadiUserController from "../controllers/kadiUserController"
import {requireAuthenticated} from "./routerMiddleware";
const router = express.Router();
// Basic User Settings
router.get("/user", dbUserController.currentUserDetails);
router.put("/lang", requireAuthenticated, dbUserController.changeLang);
router.get("/user", KadiUserController.currentUserDetails);
router.put("/lang", requireAuthenticated, KadiUserController.changeLang);
// Guests
router.get("/players", requireAuthenticated, dbUserController.getAllPlayersAssociatedWithAccount);
router.get("/guests", requireAuthenticated, dbUserController.getGuests);
router.get("/guest/:id", requireAuthenticated, dbUserController.getGuest);
router.put("/guest/:id", requireAuthenticated, dbUserController.updateGuest);
router.post("/guests", requireAuthenticated, dbUserController.addGuest);
router.delete("/guest/:id", requireAuthenticated, dbUserController.deleteGuest);
router.get("/players", requireAuthenticated, KadiUserController.getAllPlayersAssociatedWithAccount);
router.get("/guests", requireAuthenticated, KadiUserController.getGuests);
router.get("/guest/:id", requireAuthenticated, KadiUserController.getGuest);
router.put("/guest/:id", requireAuthenticated, KadiUserController.updateGuest);
router.post("/guests", requireAuthenticated, KadiUserController.addGuest);
router.delete("/guest/:id", requireAuthenticated, KadiUserController.deleteGuest);
// Games
router.get("/games", requireAuthenticated, statsController.listGames);

View File

@@ -1,6 +1,4 @@
import {FieldType} from "./enums";
import RulesetsPage from "../../frontend/src/Components/RulesetsPage";
import {RulesetStats} from "./models/Stats";
export const defaultCellValues = {
[FieldType.number]: 0,
@@ -12,7 +10,7 @@ export const defaultCellValues = {
[FieldType.multiplier]: 0,
};
export interface Ruleset {
export interface RulesetSchema {
id: string;
label: string;
blocks: Record<string, BlockDef>;
@@ -71,7 +69,7 @@ interface DefaultCellDef {
export const DEFAULT_RULESET_NAME = "DEFAULT_RULESET";
const defaultDiceCount = 5;
export const DEFAULT_RULESET: Ruleset = {
export const DEFAULT_RULESET: RulesetSchema = {
id: DEFAULT_RULESET_NAME,
label: "Standard Kadi Rules (en)",
blocks: {