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 {CredentialsTakenError, GenericPersistenceError} from "../errors";
import StoredPlayers from "../ObjectCollections/PlayerCollection"; import mongo from "mongodb";
import {SupportedLang} from "../enums";
import {AccountStatsMongoData, defaultAccountStatsMongoData} from "../Objects/DefaultStatsMongoData";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import {SupportedLang} from "../enums";
import {AccountStatsMongoData, defaultAccountStatsMongoData, OutcomeType, PlayerGameResults} from "../Objects/DefaultStatsMongoData";
import {getMongoObjectCollection} from "../database"; import {getMongoObjectCollection} from "../database";
import KadiUser, {LoginDetails} from "../Objects/KadiUser"; import KadiUser, {LoginDetails} from "../Objects/KadiUser";
import {ActiveRecordId} from "../Objects/ActiveRecord"; import {ActiveRecordId, OrId} from "../Objects/ActiveRecord";
import {SavedGameData} from "../Objects/savedGame"; 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 { export interface KadiUserMongoData {
id: string; id: string;
@@ -20,7 +26,7 @@ export interface KadiUserMongoData {
player: ActiveRecordId; player: ActiveRecordId;
guests: ActiveRecordId[]; guests: ActiveRecordId[];
accountStats: AccountStatsMongoData; accountStats: AccountStatsMongoData;
savedGames: SavedGameData[]; savedGames: ActiveRecordId[];
} }
class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> { class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> {
@@ -28,7 +34,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
super(collectionClient); super(collectionClient);
} }
private storedUserFrom(data: KadiUserMongoData): KadiUser { private kadiUserFrom(data: KadiUserMongoData): KadiUser {
return new KadiUser( return new KadiUser(
data.id, data.id,
data.username, data.username,
@@ -37,20 +43,15 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
data.lang); data.lang);
} }
async read(id: string): Promise<KadiUser | null> { async read(id: string): Promise<KadiUser> {
const foundUser = await this.mongoRead(id); const foundUser = await this.mongoRead(id);
if (foundUser) { return this.kadiUserFrom(foundUser);
return this.storedUserFrom(foundUser);
}
else {
return null;
}
} }
async findByEmail(emailQuery: string): Promise<KadiUser | null> { async findByEmail(emailQuery: string): Promise<KadiUser | null> {
const foundUser = await this.mongoFindByAttribute("email", emailQuery); const foundUser = await this.mongoFindByAttribute("email", emailQuery);
if (foundUser) { if (foundUser) {
return this.storedUserFrom(foundUser); return this.kadiUserFrom(foundUser);
} }
else { else {
return null; return null;
@@ -69,7 +70,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
} }
private async addNewUser(loginDetails: LoginDetails): Promise<KadiUser> { 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 securePassword = await this.makePasswordSecure(loginDetails.password);
const newUser = await this.mongoCreate({ const newUser = await this.mongoCreate({
username: loginDetails.username, username: loginDetails.username,
@@ -82,7 +83,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
guests: [], guests: [],
savedGames: [], savedGames: [],
}); });
return this.storedUserFrom(newUser); return this.kadiUserFrom(newUser);
} }
async userWithEmailExists(email: string): Promise<boolean> { async userWithEmailExists(email: string): Promise<boolean> {
@@ -98,7 +99,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
async getSerializedAuthUser(id: string): Promise<KadiUser> { async getSerializedAuthUser(id: string): Promise<KadiUser> {
const foundUser = await this.mongoRead(id); const foundUser = await this.mongoRead(id);
if (foundUser) { if (foundUser) {
return this.storedUserFrom(foundUser); return this.kadiUserFrom(foundUser);
} }
else { else {
throw new GenericPersistenceError("User not found!"); throw new GenericPersistenceError("User not found!");
@@ -108,6 +109,61 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
async makePasswordSecure(password: string): Promise<string> { async makePasswordSecure(password: string): Promise<string> {
return bcrypt.hash(password, 10); 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")); const KadiUserCollectionSingleton = new KadiUserCollection(getMongoObjectCollection("users"));

View File

@@ -1,6 +1,6 @@
import mongo from "mongodb"; import mongo from "mongodb";
import {tryQuery} from "./database"; import {tryQuery} from "../database";
import {MongoError} from "../errors"; import {InvalidIdError, MongoError} from "../errors";
import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord"; 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> { protected async mongoRead(id: string): Promise<IRawData> {
return tryQuery(async () => return tryQuery(async () => {
await this.mongoDbClientCollection.findOne({_id: id}) 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> { 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; let deletedObject;
if (returnObject ?? true) { if (returnObject ?? true) {
deletedObject = await this.mongoRead(objectId); deletedObject = await this.mongoRead(objectId);
@@ -40,15 +45,19 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
if (deleteWriteOpResult.result.ok === 1) { if (deleteWriteOpResult.result.ok === 1) {
return deletedObject; return deletedObject;
} else { } 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(() => 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();
}
} }

View File

@@ -1,30 +1,62 @@
import MongoStoredObjectCollection from "./MongoStoredObjectCollection"; import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
import mongo from "mongodb"; import mongo from "mongodb";
import {defaultPlayerStatsMongoData, PlayerStatsMongoData} from "../Objects/DefaultStatsMongoData"; import {
import {getMongoObjectCollection} from "./database"; defaultPlayerStatsMongoData,
OutcomeType,
PlayerGameResults,
PlayerStatsMongoData
} from "../Objects/DefaultStatsMongoData";
import {getMongoObjectCollection} from "../database";
import Player from "../Objects/Player"; import Player from "../Objects/Player";
import PlayerStats from "../Objects/PlayerStats"; 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; id: string;
nick: string; nick: string;
stats?: PlayerStatsMongoData; stats?: PlayerStatsMongoData;
} }
class PlayerCollection extends MongoStoredObjectCollection<MongoStoredPlayerData> { class PlayerCollection extends MongoStoredObjectCollection<PlayerMongoData> {
constructor(collectionClient: mongo.Collection) { constructor(collectionClient: mongo.Collection) {
super(collectionClient); 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); return new Player(data.id, data.nick, data.stats ? new PlayerStats(data.stats) : undefined);
} }
async create(nick: string): Promise<Player> { async create(nick: string): Promise<Player> {
const newPlayer = {nick, stats: defaultPlayerStatsMongoData()}; 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 {AccountStatsMongoData, OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
import {UpdateError} from "../errors"; import {UpdateError} from "../errors";
import StatsUpdater from "./StatsUpdater"; import StatsUpdater from "./StatsUpdater";
import Ruleset from "./Ruleset";
class AccountStats { class AccountStats {
private data?: AccountStatsMongoData; private data: AccountStatsMongoData;
private readonly updater: StatsUpdater; private readonly updater: StatsUpdater;
constructor(data?: AccountStatsMongoData) { constructor(data: AccountStatsMongoData) {
if (data) {
this.data = data; this.data = data;
}
this.updater = new StatsUpdater(); this.updater = new StatsUpdater();
} }
@@ -18,16 +17,22 @@ class AccountStats {
this.updater.use(data); this.updater.use(data);
} }
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void { updateStats(playerGameResults: (PlayerGameResults & {outcome: OutcomeType})[], ruleset: Ruleset): void {
if (this.data) { if (this.data) {
this.updater.updateStats(playerGameResults, ruleset); for (const playerGameResult of playerGameResults) {
this.updater.updateStats(playerGameResult, ruleset);
this.data.gamesPlayed += 1; this.data.gamesPlayed += 1;
} }
}
else { else {
throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data
to analyse.`); to analyse.`);
} }
} }
getData(): AccountStatsMongoData {
return this.data;
}
} }
export default AccountStats; export default AccountStats;

View File

@@ -1,4 +1,5 @@
export type ActiveRecordId = string; export type ActiveRecordId = string;
export type OrId<T extends ActiveRecord> = T | ActiveRecordId;
interface ActiveRecord { interface ActiveRecord {
getId(): ActiveRecordId; 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"; import {FieldType} from "../enums";
export type OutcomeType = "win" | "loss" | "runnerUp" | "draw"; export enum OutcomeType {
export interface PlayerStatsMongoData extends BaseStatsMongoData {} win,
export interface AccountStatsMongoData extends BaseStatsMongoData { loss,
timesNoWinner: number; runnerUp,
draw,
} }
export interface PlayerStatsMongoData extends BaseStatsMongoData {}
export interface AccountStatsMongoData extends BaseStatsMongoData {}
export interface BaseStatsMongoData { export interface BaseStatsMongoData {
statsByRuleset: Record<string, RulesetStatsMongoData> statsByRuleset: Record<string, RulesetStatsMongoData>
gamesPlayed: number; 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> = {}; const blockStatsRecord: Record<string, BlockStatsMongoData> = {};
for (const blockLabel in ruleset.blocks) { for (const blockLabel in ruleset.blocks) {
blockStatsRecord[blockLabel] = defaultBlockStatsMongoData(ruleset.blocks[blockLabel].cells, ruleset.blocks[blockLabel].hasBonus); blockStatsRecord[blockLabel] = defaultBlockStatsMongoData(ruleset.blocks[blockLabel].cells, ruleset.blocks[blockLabel].hasBonus);
@@ -155,7 +159,7 @@ function defaultBaseStatsMongoData(): BaseStatsMongoData {
} }
export function defaultAccountStatsMongoData(): AccountStatsMongoData { export function defaultAccountStatsMongoData(): AccountStatsMongoData {
return {...defaultBaseStatsMongoData(), timesNoWinner: 0}; return defaultBaseStatsMongoData();
} }
export function defaultPlayerStatsMongoData(): PlayerStatsMongoData { export function defaultPlayerStatsMongoData(): PlayerStatsMongoData {

View File

@@ -1,16 +1,17 @@
import {CellValue} from "../controllers/statsController"; import {CellValue} from "../controllers/statsController";
import {Ruleset} from "../rulesets"; import {RulesetSchema} from "../rulesets";
import {ActiveRecordId} from "./ActiveRecord"; import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
import {UpdateError} from "../errors"; import {UpdateError} from "../errors";
import {OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData"; import {OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
import PlayerStats from "./PlayerStats"; import PlayerStats from "./PlayerStats";
import Ruleset from "./Ruleset";
export interface CellDetails { export interface CellDetails {
id: string; id: string;
value: CellValue; value: CellValue;
} }
export class Player { export class Player implements ActiveRecord {
constructor( constructor(
private id: ActiveRecordId, private id: ActiveRecordId,
private nick: string, private nick: string,
@@ -25,11 +26,15 @@ export class Player {
return this.nick; return this.nick;
} }
async setNick(newNick: string): Promise<void> { setNick(newNick: string): void {
this.nick = newNick; 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) { if (this.stats) {
this.stats.updateStats(playerGameResults, ruleset); 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 {OutcomeType, PlayerGameResults, PlayerStatsMongoData} from "./DefaultStatsMongoData";
import StatsUpdater from "./StatsUpdater"; import StatsUpdater from "./StatsUpdater";
import Ruleset from "./Ruleset";
class PlayerStats { class PlayerStats {
private data?: PlayerStatsMongoData; private data: PlayerStatsMongoData;
private readonly updater: StatsUpdater; private readonly updater: StatsUpdater;
constructor(data?: PlayerStatsMongoData) { constructor(data: PlayerStatsMongoData) {
if (data) {
this.data = data; this.data = data;
}
this.updater = new StatsUpdater(); this.updater = new StatsUpdater();
} }
@@ -20,6 +19,10 @@ class PlayerStats {
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void { updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
this.updater.updateStats(playerGameResults, ruleset); this.updater.updateStats(playerGameResults, ruleset);
} }
getData(): PlayerStatsMongoData {
return this.data;
}
} }
export default PlayerStats; 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 ScoreBlockCalculator, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlockCalculator";
import Ruleset from "./Ruleset";
export type CellLocation = { blockId: string, cellId: string }; export type CellLocation = { blockId: string, cellId: string };
@@ -11,7 +12,7 @@ class ScoreCalculator {
private readonly blocks: ScoreBlockCalculator[]; private readonly blocks: ScoreBlockCalculator[];
constructor(gameSchema: Ruleset) { constructor(gameSchema: Ruleset) {
this.blocks = ScoreCalculator.generateBlocks(gameSchema.blocks); this.blocks = ScoreCalculator.generateBlocks(gameSchema.getBlocks());
} }
hydrateWithJSON(jsonRep: ScoreCardJSONRepresentation): void { hydrateWithJSON(jsonRep: ScoreCardJSONRepresentation): void {

View File

@@ -1,4 +1,4 @@
import {Ruleset} from "../rulesets"; import {RulesetSchema} from "../rulesets";
import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator"; import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator";
import {UpdateError} from "../errors"; import {UpdateError} from "../errors";
import {FieldType} from "../enums"; import {FieldType} from "../enums";
@@ -11,6 +11,7 @@ import {
PlayerGameResults, RulesetStatsMongoData, PlayerGameResults, RulesetStatsMongoData,
TotalFieldStatsMongoData TotalFieldStatsMongoData
} from "./DefaultStatsMongoData"; } from "./DefaultStatsMongoData";
import Ruleset from "./Ruleset";
class StatsUpdater { class StatsUpdater {
private data?: BaseStatsMongoData; private data?: BaseStatsMongoData;
@@ -29,8 +30,8 @@ class StatsUpdater {
this.validationRuleset = ruleset; this.validationRuleset = ruleset;
this.calculator = new ScoreCalculator(ruleset); this.calculator = new ScoreCalculator(ruleset);
this.calculator.hydrateWithJSON(playerGameResults as ScoreCardJSONRepresentation); this.calculator.hydrateWithJSON(playerGameResults as ScoreCardJSONRepresentation);
this.currentStatsObject = this.data.statsByRuleset[ruleset.id]; this.currentStatsObject = this.data.statsByRuleset[ruleset.getId()];
for (const blockId in ruleset.blocks) { for (const blockId in ruleset.getBlocks()) {
this.updateBlockStats(blockId); this.updateBlockStats(blockId);
} }
this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal()); this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal());
@@ -58,15 +59,15 @@ class StatsUpdater {
if (this.calculator!.blockWithIdHasBonus(blockId)) { if (this.calculator!.blockWithIdHasBonus(blockId)) {
blockStats.timesHadBonus! += 1; 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}); this.updateCellStatsByIds({cellId, blockId});
} }
} }
} }
private updateCellStatsByIds(ids: {cellId: string, blockId: string}) { private updateCellStatsByIds(ids: {cellId: string, blockId: string}) {
const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.id}); const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.getId()});
const cellFieldType = this.validationRuleset?.blocks[ids.blockId].cells[ids.cellId].fieldType; const cellFieldType = this.validationRuleset?.getBlocks()[ids.blockId].cells[ids.cellId].fieldType;
const cellScore = this.calculator!.getCellScoreByLocation({...ids}); const cellScore = this.calculator!.getCellScoreByLocation({...ids});
if (cellScore > 0 && cellFieldType === FieldType.bool) { if (cellScore > 0 && cellFieldType === FieldType.bool) {
(cellStats as BoolFieldStatsMongoData).total += 1; (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 KadiUser from "../Objects/KadiUser";
import Player from "../Objects/Player"; import Player from "../Objects/Player";
import KadiUserCollection from "../ObjectCollections/KadiUserCollection"; import KadiUserCollection from "../ObjectCollections/KadiUserCollection";
import PlayerCollection from "../ObjectCollections/PlayerCollection";
export const currentUserDetails: RequestHandler = async (req, res) => { export const currentUserDetails: RequestHandler = async (req, res) => {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
@@ -26,7 +27,7 @@ export const changeLang: RequestHandler = async (req, res) => {
export const addGuest: RequestHandler = async (req, res) => { export const addGuest: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser); const user = (req.user as KadiUser);
if (req.body.guestName) { 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({ res.send({
username: user.getUsername(), username: user.getUsername(),
userId: user.getId(), userId: user.getId(),
@@ -46,7 +47,9 @@ export const updateGuest: RequestHandler = async (req, res) => {
const {id: guestId} = req.params; const {id: guestId} = req.params;
if (req.body.newName) { if (req.body.newName) {
const {newName} = req.body; 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({ res.status(200).send({
userId: user.getId(), userId: user.getId(),
username: user.getUsername(), username: user.getUsername(),
@@ -64,7 +67,7 @@ export const updateGuest: RequestHandler = async (req, res) => {
export const getGuest: RequestHandler = async (req, res) => { export const getGuest: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser); const user = (req.user as KadiUser);
const {id: guestId} = req.params; const {id: guestId} = req.params;
const guest = await KadiUserCollection.getGuestForAccount(guestId, user.getId()); const guest = await PlayerCollection.read(guestId);
res.status(200).send({ res.status(200).send({
userId: user.getId(), userId: user.getId(),
username: user.getUsername(), username: user.getUsername(),
@@ -75,7 +78,7 @@ export const getGuest: RequestHandler = async (req, res) => {
export const deleteGuest: RequestHandler = async (req, res) => { export const deleteGuest: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser); const user = (req.user as KadiUser);
const {id: guestId} = req.params; const {id: guestId} = req.params;
const deletedGuest = await KadiUserCollection.deleteGuestForAccount(guestId, user.getId()); const deletedGuest = await KadiUserCollection.deleteGuestFromUser(user, guestId);
res.status(200).send({ res.status(200).send({
userId: user.getId(), userId: user.getId(),
username: user.getUsername(), username: user.getUsername(),
@@ -85,7 +88,7 @@ export const deleteGuest: RequestHandler = async (req, res) => {
export const getGuests: RequestHandler = async (req, res) => { export const getGuests: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser); const user = (req.user as KadiUser);
const guests = await KadiUserCollection.getGuestsForAccount(user.getId()); const guests = await KadiUserCollection.getAllGuestsForUser(user);
res.status(200).send({ res.status(200).send({
userId: user.getId(), userId: user.getId(),
username: user.getUsername(), username: user.getUsername(),
@@ -95,7 +98,7 @@ export const getGuests: RequestHandler = async (req, res) => {
export const getAllPlayersAssociatedWithAccount: RequestHandler = async (req, res) => { export const getAllPlayersAssociatedWithAccount: RequestHandler = async (req, res) => {
const user = (req.user as KadiUser); const user = (req.user as KadiUser);
const guests = await KadiUserCollection.getAllGuestsForAccount(user.getId()); const guests = await KadiUserCollection.getAllGuestsForUser(user);
const mainPlayer = await KadiUserCollection.getMainPlayerForAccount(user.getId()); const mainPlayer = await KadiUserCollection.getMainPlayerForUser(user);
res.status(200).send({guests, mainPlayer}); res.status(200).send({guests, mainPlayer});
}; };

View File

@@ -1,50 +1,24 @@
import DbUser, { IDbUser } from "../models/dbUser_old"; import KadiUserCollection from "../ObjectCollections/KadiUserCollection";
import { RequestHandler } from "express"; import PlayerCollection from "../ObjectCollections/PlayerCollection";
import Player, { IPlayer } from "../models/StoredPlayer"; import Player from "../Objects/Player";
import {RequestHandler} from "express";
const DEFAULT_RULESET = "DEFAULT_RULESET"; import ScoreCalculator from "../Objects/ScoreCalculator";
const UPPER_BONUS_THRESHOLD = 63; import KadiUser from "../Objects/KadiUser";
const UPPER_BONUS = 35; import {CellFlag} from "../enums";
const FULL_HOUSE_SCORE = 25; import RulesetCollection from "../ObjectCollections/RulesetCollection";
const SML_STRAIGHT_SCORE = 30; import Ruleset from "../Objects/Ruleset";
const LG_STRAIGHT_SCORE = 40; import {OutcomeType} from "../Objects/DefaultStatsMongoData";
const YAHTZEE_SCORE = 50;
export interface GameSubmission { export interface GameSubmission {
ruleset: string; rulesetId: string;
players: { id: string; nick: string }[]; players: { id: string; nick: string }[];
results: PlayerGameResult[]; results: PlayerGameResult[];
} }
interface ScoredResult extends ScoreTotalFields, PlayerGameResult {} type PlayerGameResult = { playerId: string; blocks: Record<string, Block> };
type Block = { cells: Record<string, Cell> };
interface ScoreTotalFields { type Cell = { value: CellValue };
topBonus: boolean; export type CellValue = number | boolean | CellFlag.strike;
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";
enum ResultType { enum ResultType {
winner, winner,
drawn, drawn,
@@ -52,15 +26,14 @@ enum ResultType {
loser, loser,
} }
type ScoredResults = {score: number, results: PlayerGameResult};
type ScoredResultsWithOutcome = {score: number, results: PlayerGameResult & {outcome: OutcomeType}};
export const listGames: RequestHandler = async (req, res) => { export const listGames: RequestHandler = async (req, res) => {
const user = req.user as IDbUser; const user = req.user as KadiUser;
const dbUser = await DbUser.findById(user.id, { const gamesList = await KadiUserCollection.getSavedGamesForUser(user);
"savedGames._id": 1, if (gamesList) {
"savedGames.results": 1, res.json({ games: gamesList });
"savedGames.createdAt": 1,
});
if (dbUser) {
res.json({ games: dbUser.savedGames });
} }
else { else {
res.sendStatus(404); res.sendStatus(404);
@@ -68,172 +41,118 @@ export const listGames: RequestHandler = async (req, res) => {
}; };
export const saveGame: 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 submission = req.body as GameSubmission;
const newGuests: IPlayer[] = await addNewGuests(submission, user); const newGuests: Player[] = await addNewGuests(submission, user);
if (newGuests.length > 0) { if (newGuests.length > 0) {
fillOutSubmissionWithNewIds(submission, newGuests); fillOutSubmissionWithNewIds(submission, newGuests);
} }
const newGame = await user.addGame(submission); const newGame = await KadiUserCollection.addGameForUser(user, submission);
if (submission.ruleset === DEFAULT_RULESET) { processStats(await RulesetCollection.read(submission.rulesetId), submission.results, user);
processStandardStatistics(submission.results, user);
}
res.send({ message: "Game submitted successfully!", newGame: newGame }); res.send({ message: "Game submitted successfully!", newGame: newGame });
}; };
async function addNewGuests(submission: GameSubmission, user: IDbUser): Promise<IPlayer[]> { async function addNewGuests(submission: GameSubmission, user: KadiUser): Promise<Player[]> {
const newGuestIds: IPlayer[] = []; const newGuestIds: Player[] = [];
for (const playerDetails of submission.players) { for (const playerDetails of submission.players) {
const isNewPlayer = playerDetails.id === playerDetails.nick; const isNewPlayer = playerDetails.id === playerDetails.nick;
if (isNewPlayer) { if (isNewPlayer) {
const newGuest: IPlayer = await user.addGuest(playerDetails.nick); const newGuest: Player = await KadiUserCollection.addGuestForUser(user, playerDetails.nick);
newGuestIds.push(newGuest); newGuestIds.push(newGuest);
} }
} }
return newGuestIds; return newGuestIds;
} }
function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: IPlayer[]): GameSubmission { function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: Player[]): GameSubmission {
for (const newGuest of newGuestList) { for (const newGuest of newGuestList) {
const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.nick); const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.getNick());
if (gameResultsFromNewGuest) { 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) { if (playerEntryForNewGuest) {
playerEntryForNewGuest.id = newGuest.id; playerEntryForNewGuest.id = newGuest.getId();
} }
} }
return submission; return submission;
} }
function processStandardStatistics(results: PlayerGameResult[], account: IDbUser) { async function processStats(ruleset: Ruleset, results: PlayerGameResult[], account: KadiUser) {
let scoredResults: ScoredResult[] = []; const calc = new ScoreCalculator(ruleset);
let playerScoreList: ScoredResults[] = [];
for (const result of results) { for (const result of results) {
const scoredResult = { calc.hydrateWithJSON(result);
...result, playerScoreList.push({
...getStandardScoreFields(result), score: calc.getTotal(),
}; results: result
scoredResults.push(scoredResult); });
updatePlayerStats(result.playerId, scoredResult);
} }
const { wasDraw } = incrementPlayerPlacings(scoredResults); const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList, ruleset);
if (wasDraw) { updateStatsForIndividualPlayers(playerScoreListWithOutcomes, ruleset);
DbUser.incrementTimesNoWinner(account.id); const gameResults = playerScoreListWithOutcomes.map(scoredResults => scoredResults.results);
} await KadiUserCollection.updateAccountStats(account.getId(), gameResults, ruleset);
DbUser.incrementGamesPlayed(account.id);
} }
async function updatePlayerStats(playerId: string, result: ScoredResult) { function updateScoreListWithOutcomes(playerScoreList: ScoredResults[], rulesetUsed: Ruleset): ScoredResultsWithOutcome[] {
const player: IPlayer = await Player.findById(playerId) as IPlayer; playerScoreList = sortDescendingByScore(playerScoreList);
for (const blockId in result.blocks) { const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = playerScoreList.map(scoredResults => {
const cells = result.blocks[blockId as BlockName].cells; const newResults = {...scoredResults.results, outcome: OutcomeType.loss};
for (const cellId in cells) { return {...scoredResults, results: newResults};
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 };
let runnerUpsStart: number; let runnerUpsStart: number;
if (scoredResults[0].total !== scoredResults[1].total) { if (playerScoreListWithOutcomes[0].score !== playerScoreListWithOutcomes[1].score) {
Player.incrementWinFor(scoredResults[0].playerId); playerScoreListWithOutcomes[0].results.outcome = OutcomeType.win;
runnerUpsStart = 1; runnerUpsStart = 1;
} }
else { else {
runnerUpsStart = icrmtPlayerDrawsTilScoreChange(scoredResults); runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes, rulesetUsed);
placingFacts.wasDraw = true;
} }
const losersStart = icrmtPlayerRunnerUpsTilScoreChange( const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes.slice(runnerUpsStart), rulesetUsed);
scoredResults.slice(runnerUpsStart) updateScoreListWithLosses(playerScoreListWithOutcomes.slice(losersStart), rulesetUsed);
); return playerScoreListWithOutcomes;
icrmtPlayerLosses(scoredResults.slice(losersStart));
return placingFacts;
} }
function icrmtPlayerDrawsTilScoreChange(scoredResults: ScoredResult[]): number { function updateScoreListWithDraws(playerScoreList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): number {
for (let i = 0; i < scoredResults.length; i++) { for (let i = 0; i < playerScoreList.length; i++) {
if (scoredResults[i].total === scoredResults[0].total) { if (playerScoreList[i].score === playerScoreList[0].score) {
Player.incrementDrawFor(scoredResults[i].playerId); playerScoreList[i].results.outcome = OutcomeType.draw;
} }
else { else {
return i; return i;
} }
} }
return scoredResults.length; return playerScoreList.length;
} }
function icrmtPlayerRunnerUpsTilScoreChange(scoredResults: ScoredResult[]): number { function updateScoreListWithRunnerUps(playerScoreList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): number {
for (let i = 0; i < scoredResults.length; i++) { for (let i = 0; i < playerScoreList.length; i++) {
if (scoredResults[i].total === scoredResults[0].total) { if (playerScoreList[i].score === playerScoreList[0].score) {
Player.incrementRunnerUpFor(scoredResults[i].playerId); playerScoreList[i].results.outcome = OutcomeType.runnerUp;
} }
else { else {
return i; return i;
} }
} }
return scoredResults.length; return playerScoreList.length;
} }
function icrmtPlayerLosses(scoredResults: ScoredResult[]): void { function updateScoreListWithLosses(scoreResultsList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset) {
for (const scoredResult of scoredResults) { for (const lostPlayerResults of scoreResultsList) {
Player.incrementLossFor(scoredResult.playerId); lostPlayerResults.results.outcome = OutcomeType.loss;
} }
} }
function sortDescendingByScore(scoredResults: ScoredResult[]) { function sortDescendingByScore(playerScoreList: ScoredResults[]) {
return scoredResults.sort((a, b) => b.total - a.total); return playerScoreList.sort((a, b) => b.score - a.score);
} }
function updateStatsForIndividualPlayers(playerScoreListWithOutcomes: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): void {
function getStandardScoreFields(result: PlayerGameResult): ScoreTotalFields { for (const scoredResults of playerScoreListWithOutcomes) {
const scoreFields: ScoreTotalFields = { topBonus: false, topSubtotal: 0, top: 0, bottom: 0, total: 0 }; PlayerCollection.updateStatsForPlayer(
scoreFields.topSubtotal = topSubtotal(result.blocks.top.cells); scoredResults.results.playerId,
scoreFields.top = scoreFields.topSubtotal; {...scoredResults.results, outcome: scoredResults.results.outcome},
if (scoreFields.topSubtotal >= UPPER_BONUS_THRESHOLD) { rulesetUsed);
scoreFields.topBonus = true;
scoreFields.top += UPPER_BONUS;
} }
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"; 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 express from "express";
import {requireAuthenticated} from "../passport-config";
import * as statsController from "../controllers/statsController"; 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(); const router = express.Router();
// Basic User Settings // Basic User Settings
router.get("/user", dbUserController.currentUserDetails); router.get("/user", KadiUserController.currentUserDetails);
router.put("/lang", requireAuthenticated, dbUserController.changeLang); router.put("/lang", requireAuthenticated, KadiUserController.changeLang);
// Guests // Guests
router.get("/players", requireAuthenticated, dbUserController.getAllPlayersAssociatedWithAccount); router.get("/players", requireAuthenticated, KadiUserController.getAllPlayersAssociatedWithAccount);
router.get("/guests", requireAuthenticated, dbUserController.getGuests); router.get("/guests", requireAuthenticated, KadiUserController.getGuests);
router.get("/guest/:id", requireAuthenticated, dbUserController.getGuest); router.get("/guest/:id", requireAuthenticated, KadiUserController.getGuest);
router.put("/guest/:id", requireAuthenticated, dbUserController.updateGuest); router.put("/guest/:id", requireAuthenticated, KadiUserController.updateGuest);
router.post("/guests", requireAuthenticated, dbUserController.addGuest); router.post("/guests", requireAuthenticated, KadiUserController.addGuest);
router.delete("/guest/:id", requireAuthenticated, dbUserController.deleteGuest); router.delete("/guest/:id", requireAuthenticated, KadiUserController.deleteGuest);
// Games // Games
router.get("/games", requireAuthenticated, statsController.listGames); router.get("/games", requireAuthenticated, statsController.listGames);

View File

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