From 42b7560d6d3070f078e7e9df0d47abf3e74f90fc Mon Sep 17 00:00:00 2001 From: Daniel Ledda Date: Fri, 17 Jul 2020 22:23:05 +0200 Subject: [PATCH] I think it's done? --- src/ObjectCollections/KadiUserCollection.ts | 96 +++++-- .../MongoStoredObjectCollection.ts | 27 +- src/ObjectCollections/PlayerCollection.ts | 48 +++- src/ObjectCollections/RulesetCollection.ts | 30 +++ src/ObjectCollections/SavedGameCollection.ts | 54 ++++ src/ObjectCollections/ruleset.ts | 0 src/Objects/AccountStats.ts | 23 +- src/Objects/ActiveRecord.ts | 1 + src/Objects/DefaultStatsMongoData.ts | 18 +- src/Objects/Player.ts | 15 +- src/Objects/PlayerStats.ts | 15 +- src/Objects/Ruleset.ts | 31 +++ src/Objects/SavedGame.ts | 29 +++ src/Objects/ScoreCalculator.ts | 5 +- src/Objects/StatsUpdater.ts | 13 +- src/Objects/savedGame.ts | 17 -- src/controllers/kadiUserController.ts | 17 +- src/controllers/statsController.ts | 243 ++++++------------ src/errors.ts | 7 + src/routers/apiRouter.ts | 20 +- src/rulesets.ts | 6 +- 21 files changed, 443 insertions(+), 272 deletions(-) create mode 100644 src/ObjectCollections/RulesetCollection.ts create mode 100644 src/ObjectCollections/SavedGameCollection.ts delete mode 100755 src/ObjectCollections/ruleset.ts create mode 100644 src/Objects/Ruleset.ts create mode 100755 src/Objects/SavedGame.ts delete mode 100755 src/Objects/savedGame.ts diff --git a/src/ObjectCollections/KadiUserCollection.ts b/src/ObjectCollections/KadiUserCollection.ts index 3103fd2..ddc8ec0 100644 --- a/src/ObjectCollections/KadiUserCollection.ts +++ b/src/ObjectCollections/KadiUserCollection.ts @@ -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 { @@ -28,7 +34,7 @@ class KadiUserCollection extends MongoStoredObjectCollection 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 data.lang); } - async read(id: string): Promise { + async read(id: string): Promise { const foundUser = await this.mongoRead(id); - if (foundUser) { - return this.storedUserFrom(foundUser); - } - else { - return null; - } + return this.kadiUserFrom(foundUser); } async findByEmail(emailQuery: string): Promise { 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 } private async addNewUser(loginDetails: LoginDetails): Promise { - 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 guests: [], savedGames: [], }); - return this.storedUserFrom(newUser); + return this.kadiUserFrom(newUser); } async userWithEmailExists(email: string): Promise { @@ -98,7 +99,7 @@ class KadiUserCollection extends MongoStoredObjectCollection async getSerializedAuthUser(id: string): Promise { 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 async makePasswordSecure(password: string): Promise { return bcrypt.hash(password, 10); } + + async addGuestForUser(userOrId: OrId, newGuestNick: string): Promise { + const newGuest = await PlayerCollection.create(newGuestNick); + await this.mongoDbClientCollection.findOneAndUpdate( + {_id: this.idFromRecordOrId(userOrId)}, + {$push: {guests: newGuest.getId()}}); + return newGuest; + } + + async deleteGuestFromUser(userOrId: OrId, guestOrGuestId: OrId): Promise { + 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): Promise[]> { + const guestIdList = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.guests; + return guestIdList.map(async (guestId) => { + return await PlayerCollection.read(guestId); + }); + } + + async getMainPlayerForUser(userOrId: OrId): Promise { + const userData = await this.mongoRead(this.idFromRecordOrId(userOrId)); + return PlayerCollection.read(userData?.player); + } + + async getSavedGamesForUser(userOrId: OrId): Promise[]> { + const savedGameIds = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.savedGames; + return savedGameIds.map(async (savedGameId) => { + return await SavedGameCollection.read(savedGameId); + }); + } + + async addGameForUser(userOrId: OrId, gameSubmission: GameSubmission): Promise { + const newGame = await SavedGameCollection.create(gameSubmission); + await this.mongoDbClientCollection.findOneAndUpdate( + {_id: this.idFromRecordOrId(userOrId)}, + {$push: {savedGames: newGame.getId()}}); + return newGame; + } + + async updateAccountStats(userOrId: OrId, gameResults: (PlayerGameResults & {outcome: OutcomeType})[], rulesetUsedOrId: OrId): Promise { + 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")); diff --git a/src/ObjectCollections/MongoStoredObjectCollection.ts b/src/ObjectCollections/MongoStoredObjectCollection.ts index 96c6194..d1c28ff 100644 --- a/src/ObjectCollections/MongoStoredObjectCollection.ts +++ b/src/ObjectCollections/MongoStoredObjectCollection.ts @@ -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 { - return tryQuery(async () => - await this.mongoDbClientCollection.findOne({_id: id}) - ); + protected async mongoRead(id: string): Promise { + 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 { @@ -31,7 +36,7 @@ abstract class MongoStoredObjectCollection { + protected async mongoDelete(objectId: ActiveRecordId, returnObject?: boolean): Promise { let deletedObject; if (returnObject ?? true) { deletedObject = await this.mongoRead(objectId); @@ -40,15 +45,19 @@ abstract class MongoStoredObjectCollection & {id: ActiveRecordId}) { await tryQuery(() => this.mongoDbClientCollection.findOneAndUpdate({_id: object.id}, {$set: {...object, id: undefined}}) ); } + + protected idFromRecordOrId(recordOrRecordId: T | ActiveRecordId): ActiveRecordId { + return typeof recordOrRecordId === "string" ? recordOrRecordId : recordOrRecordId.getId(); + } } diff --git a/src/ObjectCollections/PlayerCollection.ts b/src/ObjectCollections/PlayerCollection.ts index c01c891..9560ad2 100644 --- a/src/ObjectCollections/PlayerCollection.ts +++ b/src/ObjectCollections/PlayerCollection.ts @@ -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 { +class PlayerCollection extends MongoStoredObjectCollection { 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 { const newPlayer = {nick, stats: defaultPlayerStatsMongoData()}; - return this.storedPlayerFrom(await this.mongoCreate(newPlayer)); + return this.playerFrom(await this.mongoCreate(newPlayer)); + } + + async read(id: ActiveRecordId): Promise { + return this.playerFrom(await this.mongoRead(id)); + } + + async delete(id: ActiveRecordId): Promise { + return this.playerFrom(await this.mongoDelete(id, true) as PlayerMongoData); + } + + async save(player: Player): Promise { + await this.mongoUpdate({id: player.getId(), nick: player.getNick(), stats: player.getStats()?.getData()}); + return player; + } + + async updateStatsForPlayer(playerOrId: OrId, gameResults: PlayerGameResults & {outcome: OutcomeType}, rulesetUsedOrId: OrId): Promise { + 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; \ No newline at end of file + +const PlayerCollectionSingleton = new PlayerCollection(getMongoObjectCollection("players")); +export default PlayerCollectionSingleton; \ No newline at end of file diff --git a/src/ObjectCollections/RulesetCollection.ts b/src/ObjectCollections/RulesetCollection.ts new file mode 100644 index 0000000..7e0d66b --- /dev/null +++ b/src/ObjectCollections/RulesetCollection.ts @@ -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 { + constructor(collectionClient: mongo.Collection) { + super(collectionClient); + } + + private async rulesetFrom(data: RulesetMongoData): Promise { + return new Ruleset(data.id, data); + } + + async read(id: string): Promise { + 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; \ No newline at end of file diff --git a/src/ObjectCollections/SavedGameCollection.ts b/src/ObjectCollections/SavedGameCollection.ts new file mode 100644 index 0000000..15d6d63 --- /dev/null +++ b/src/ObjectCollections/SavedGameCollection.ts @@ -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 { + constructor(collectionClient: mongo.Collection) { + super(collectionClient); + } + + private async savedGameFrom(data: SavedGameMongoData): Promise { + 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 { + const foundGame = await this.mongoRead(id); + return this.savedGameFrom(foundGame); + } + + async create(gameSubmission: GameSubmission): Promise { + 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; \ No newline at end of file diff --git a/src/ObjectCollections/ruleset.ts b/src/ObjectCollections/ruleset.ts deleted file mode 100755 index e69de29..0000000 diff --git a/src/Objects/AccountStats.ts b/src/Objects/AccountStats.ts index 618be8a..b89756d 100644 --- a/src/Objects/AccountStats.ts +++ b/src/Objects/AccountStats.ts @@ -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) { - this.data = 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); - this.data.gamesPlayed += 1; + 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; \ No newline at end of file diff --git a/src/Objects/ActiveRecord.ts b/src/Objects/ActiveRecord.ts index 236f2db..1f69067 100644 --- a/src/Objects/ActiveRecord.ts +++ b/src/Objects/ActiveRecord.ts @@ -1,4 +1,5 @@ export type ActiveRecordId = string; +export type OrId = T | ActiveRecordId; interface ActiveRecord { getId(): ActiveRecordId; diff --git a/src/Objects/DefaultStatsMongoData.ts b/src/Objects/DefaultStatsMongoData.ts index 5d6d41f..e942ef9 100755 --- a/src/Objects/DefaultStatsMongoData.ts +++ b/src/Objects/DefaultStatsMongoData.ts @@ -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 gamesPlayed: number; @@ -130,7 +134,7 @@ function defaultBlockStatsMongoData(cellSchemas: Record, hasBon } } -function defaultRulesetStatsMongoData(ruleset: Ruleset): RulesetStatsMongoData { +function defaultRulesetStatsMongoData(ruleset: RulesetSchema): RulesetStatsMongoData { const blockStatsRecord: Record = {}; 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 { diff --git a/src/Objects/Player.ts b/src/Objects/Player.ts index c350d5b..b198b9e 100755 --- a/src/Objects/Player.ts +++ b/src/Objects/Player.ts @@ -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 { + 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); } diff --git a/src/Objects/PlayerStats.ts b/src/Objects/PlayerStats.ts index 8bad20f..0fc9266 100644 --- a/src/Objects/PlayerStats.ts +++ b/src/Objects/PlayerStats.ts @@ -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) { - this.data = 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; \ No newline at end of file diff --git a/src/Objects/Ruleset.ts b/src/Objects/Ruleset.ts new file mode 100644 index 0000000..c1d16f2 --- /dev/null +++ b/src/Objects/Ruleset.ts @@ -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 { + return Object.assign({}, this.schema.blocks); + } + + getCellsInBlock(blockId: string): Record { + return Object.assign({}, this.schema.blocks[blockId].cells); + } +} + +export default Ruleset; \ No newline at end of file diff --git a/src/Objects/SavedGame.ts b/src/Objects/SavedGame.ts new file mode 100755 index 0000000..c9290d1 --- /dev/null +++ b/src/Objects/SavedGame.ts @@ -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; \ No newline at end of file diff --git a/src/Objects/ScoreCalculator.ts b/src/Objects/ScoreCalculator.ts index ae54435..378df62 100755 --- a/src/Objects/ScoreCalculator.ts +++ b/src/Objects/ScoreCalculator.ts @@ -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 { diff --git a/src/Objects/StatsUpdater.ts b/src/Objects/StatsUpdater.ts index d771da1..05342d3 100644 --- a/src/Objects/StatsUpdater.ts +++ b/src/Objects/StatsUpdater.ts @@ -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; diff --git a/src/Objects/savedGame.ts b/src/Objects/savedGame.ts deleted file mode 100755 index abf0a44..0000000 --- a/src/Objects/savedGame.ts +++ /dev/null @@ -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 { - createFromGameSubmission(submission: GameSubmission): Promise; -} \ No newline at end of file diff --git a/src/controllers/kadiUserController.ts b/src/controllers/kadiUserController.ts index ac725c0..5c64769 100755 --- a/src/controllers/kadiUserController.ts +++ b/src/controllers/kadiUserController.ts @@ -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}); }; \ No newline at end of file diff --git a/src/controllers/statsController.ts b/src/controllers/statsController.ts index 7e12586..533307b 100755 --- a/src/controllers/statsController.ts +++ b/src/controllers/statsController.ts @@ -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 }; -type BlockName = "top" | "bottom"; -type Block = { cells: Record }; -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 }; +type Block = { cells: Record }; +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 { - const newGuestIds: IPlayer[] = []; +async function addNewGuests(submission: GameSubmission, user: KadiUser): Promise { + 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) { - 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) { - 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; -} +} \ No newline at end of file diff --git a/src/errors.ts b/src/errors.ts index e0d75b6..aa9b8ca 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -37,4 +37,11 @@ export class CredentialsTakenError extends KadiError { this.emailExists = emailExists; this.name = "CredentialsTakenError"; } +} + +export class InvalidIdError extends GenericPersistenceError { + constructor(message: string) { + super(message); + this.name = "InvalidIdError"; + } } \ No newline at end of file diff --git a/src/routers/apiRouter.ts b/src/routers/apiRouter.ts index 47e2bc7..4f766ed 100755 --- a/src/routers/apiRouter.ts +++ b/src/routers/apiRouter.ts @@ -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); diff --git a/src/rulesets.ts b/src/rulesets.ts index ebe4c1f..ed330eb 100755 --- a/src/rulesets.ts +++ b/src/rulesets.ts @@ -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; @@ -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: {