Big update progress on encapsulating models

This commit is contained in:
Daniel Ledda
2020-07-16 08:05:37 +02:00
parent 0bddaa524b
commit cef6249c09
20 changed files with 522 additions and 515 deletions

View File

@@ -0,0 +1,31 @@
import {Ruleset} from "../rulesets";
import {AccountStats, OutcomeType, PlayerGameResults} from "../models/Stats";
import {UpdateError} from "../errors";
import StatsUpdater from "./StatsUpdater";
export class AccountStatsUpdater {
private data?: AccountStats;
private readonly updater: StatsUpdater;
constructor(data?: AccountStats) {
if (data) {
this.data = data;
}
this.updater = new StatsUpdater();
}
use(data: AccountStats) {
this.data = data;
this.updater.use(data);
}
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
if (this.data) {
this.updater.updateStats(playerGameResults, 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.`);
}
}
}

View File

@@ -0,0 +1,24 @@
import {Ruleset} from "../rulesets";
import StatsUpdater, {OutcomeType, PlayerGameResults, PlayerStats} from "../models/Stats";
class PlayerStatsUpdater {
private data?: PlayerStats;
private readonly updater: StatsUpdater;
constructor(data?: PlayerStats) {
if (data) {
this.data = data;
}
this.updater = new StatsUpdater();
}
use(data: PlayerStats) {
this.data = data;
this.updater.use(data);
}
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
this.updater.updateStats(playerGameResults, ruleset);
}
}
export default PlayerStatsUpdater;

View File

@@ -1,12 +1,12 @@
import ScoreCell, { import ScoreCellCalculator, {
createCellFromDef, createCellFromDef,
ScoreCellValue, ScoreCellValue,
CellState, CellState,
ScoreCellJSONRepresentation ScoreCellJSONRepresentation
} from "./ScoreCell"; } from "./ScoreCellCalculator";
import {CellDef, BlockDef, BonusBlockDef, NoBonusBlockDef } from "../rulesets"; import {CellDef, BlockDef, BonusBlockDef, NoBonusBlockDef } from "../rulesets";
export const createBlockFromDef = (blockId: string, blockDef: BlockDef) : ScoreBlock => { export const createBlockFromDef = (blockId: string, blockDef: BlockDef) : ScoreBlockCalculator => {
if (blockDef.hasBonus) { if (blockDef.hasBonus) {
return new ScoreBlockWithBonus(blockId, blockDef); return new ScoreBlockWithBonus(blockId, blockDef);
} }
@@ -19,16 +19,16 @@ export interface ScoreBlockJSONRepresentation {
cells: Record<string, ScoreCellJSONRepresentation>; cells: Record<string, ScoreCellJSONRepresentation>;
} }
abstract class ScoreBlock { abstract class ScoreBlockCalculator {
protected cells: ScoreCell[]; protected cells: ScoreCellCalculator[];
protected id: string; protected id: string;
protected constructor(blockId: string, blockDef: BlockDef) { protected constructor(blockId: string, blockDef: BlockDef) {
this.cells = ScoreBlock.generateCells(blockDef.cells); this.cells = ScoreBlockCalculator.generateCells(blockDef.cells);
this.id = blockId; this.id = blockId;
} }
private static generateCells(cellDefs: Record<string, CellDef>): ScoreCell[] { private static generateCells(cellDefs: Record<string, CellDef>): ScoreCellCalculator[] {
const cells = []; const cells = [];
for (const cellId in cellDefs) { for (const cellId in cellDefs) {
cells.push(createCellFromDef(cellId, cellDefs[cellId])); cells.push(createCellFromDef(cellId, cellDefs[cellId]));
@@ -56,7 +56,7 @@ abstract class ScoreBlock {
return this.getCellById(cellId).isStruck(); return this.getCellById(cellId).isStruck();
} }
private getCellById(cellId: string): ScoreCell { private getCellById(cellId: string): ScoreCellCalculator {
const foundScoreCell = this.cells.find(cell => cell.getId() === cellId); const foundScoreCell = this.cells.find(cell => cell.getId() === cellId);
if (foundScoreCell !== undefined) { if (foundScoreCell !== undefined) {
return foundScoreCell; return foundScoreCell;
@@ -77,7 +77,7 @@ abstract class ScoreBlock {
} }
} }
class ScoreBlockWithBonus extends ScoreBlock { class ScoreBlockWithBonus extends ScoreBlockCalculator {
protected readonly bonus: number; protected readonly bonus: number;
protected readonly bonusFor: number; protected readonly bonusFor: number;
@@ -97,7 +97,7 @@ class ScoreBlockWithBonus extends ScoreBlock {
} }
} }
class ScoreBlockNoBonus extends ScoreBlock { class ScoreBlockNoBonus extends ScoreBlockCalculator {
constructor(blockId: string, blockDef: NoBonusBlockDef) { constructor(blockId: string, blockDef: NoBonusBlockDef) {
super(blockId, blockDef); super(blockId, blockDef);
} }
@@ -111,4 +111,4 @@ class ScoreBlockNoBonus extends ScoreBlock {
} }
} }
export default ScoreBlock; export default ScoreBlockCalculator;

View File

@@ -1,5 +1,5 @@
import {BlockDef, Ruleset} from "../rulesets"; import {BlockDef, Ruleset} from "../rulesets";
import ScoreBlock, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlock"; import ScoreBlockCalculator, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlockCalculator";
export type CellLocation = { blockId: string, cellId: string }; export type CellLocation = { blockId: string, cellId: string };
@@ -8,7 +8,7 @@ export interface ScoreCardJSONRepresentation {
} }
class ScoreCalculator { class ScoreCalculator {
private readonly blocks: ScoreBlock[]; private readonly blocks: ScoreBlockCalculator[];
constructor(gameSchema: Ruleset) { constructor(gameSchema: Ruleset) {
this.blocks = ScoreCalculator.generateBlocks(gameSchema.blocks); this.blocks = ScoreCalculator.generateBlocks(gameSchema.blocks);
@@ -20,7 +20,7 @@ class ScoreCalculator {
} }
} }
private static generateBlocks(blockDefs: Record<string, BlockDef>): ScoreBlock[] { private static generateBlocks(blockDefs: Record<string, BlockDef>): ScoreBlockCalculator[] {
const blocks = []; const blocks = [];
for (const blockId in blockDefs) { for (const blockId in blockDefs) {
blocks.push(createBlockFromDef(blockId, blockDefs[blockId])); blocks.push(createBlockFromDef(blockId, blockDefs[blockId]));
@@ -56,7 +56,7 @@ class ScoreCalculator {
return this.getBlockById(loc.blockId).cellWithIdIsStruck(loc.cellId); return this.getBlockById(loc.blockId).cellWithIdIsStruck(loc.cellId);
} }
private getBlockById(blockId: string): ScoreBlock { private getBlockById(blockId: string): ScoreBlockCalculator {
const foundScoreBlock = this.blocks.find(block => block.getId() === blockId); const foundScoreBlock = this.blocks.find(block => block.getId() === blockId);
if (foundScoreBlock !== undefined) { if (foundScoreBlock !== undefined) {
return foundScoreBlock; return foundScoreBlock;

View File

@@ -7,7 +7,7 @@ import {
} from "../rulesets"; } from "../rulesets";
import { CellFlag, FieldType } from "../enums"; import { CellFlag, FieldType } from "../enums";
export const createCellFromDef = (cellId: string, cellDef: CellDef): ScoreCell => { export const createCellFromDef = (cellId: string, cellDef: CellDef): ScoreCellCalculator => {
switch (cellDef.fieldType) { switch (cellDef.fieldType) {
case FieldType.number: case FieldType.number:
return new NumberScoreCell(cellId, cellDef); return new NumberScoreCell(cellId, cellDef);
@@ -33,7 +33,7 @@ export interface ScoreCellJSONRepresentation {
value: number | boolean | CellFlag.strike; value: number | boolean | CellFlag.strike;
} }
abstract class ScoreCell { abstract class ScoreCellCalculator {
protected readonly id: string; protected readonly id: string;
protected static readonly fieldType: FieldType; protected static readonly fieldType: FieldType;
protected struck: boolean; protected struck: boolean;
@@ -69,7 +69,7 @@ abstract class ScoreCell {
} }
} }
class NumberScoreCell extends ScoreCell { class NumberScoreCell extends ScoreCellCalculator {
protected static readonly fieldType = FieldType.number; protected static readonly fieldType = FieldType.number;
constructor(cellId: string, cellDef: NumberCellDef) { constructor(cellId: string, cellDef: NumberCellDef) {
@@ -82,7 +82,7 @@ class NumberScoreCell extends ScoreCell {
} }
} }
class BoolScoreCell extends ScoreCell { class BoolScoreCell extends ScoreCellCalculator {
protected static readonly fieldType = FieldType.bool; protected static readonly fieldType = FieldType.bool;
private readonly score: number; private readonly score: number;
protected value: boolean; protected value: boolean;
@@ -103,7 +103,7 @@ class BoolScoreCell extends ScoreCell {
} }
} }
class SuperkadiScoreCell extends ScoreCell { class SuperkadiScoreCell extends ScoreCellCalculator {
protected static readonly fieldType = FieldType.superkadi; protected static readonly fieldType = FieldType.superkadi;
private readonly score: number; private readonly score: number;
protected value: number; protected value: number;
@@ -124,7 +124,7 @@ class SuperkadiScoreCell extends ScoreCell {
} }
} }
class MultiplierScoreCell extends ScoreCell { class MultiplierScoreCell extends ScoreCellCalculator {
protected static readonly fieldType = FieldType.multiplier; protected static readonly fieldType = FieldType.multiplier;
protected readonly multiplier: number; protected readonly multiplier: number;
protected value: number; protected value: number;
@@ -145,4 +145,4 @@ class MultiplierScoreCell extends ScoreCell {
} }
} }
export default ScoreCell; export default ScoreCellCalculator;

119
src/models/stats.ts → src/classes/StatsUpdater.ts Executable file → Normal file
View File

@@ -1,66 +1,18 @@
import {Ruleset} from "../rulesets"; import {Ruleset} from "../rulesets";
import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator";
import {UpdateError} from "../errors";
import {FieldType} from "../enums"; import {FieldType} from "../enums";
import ScoreCalculator, {ScoreCardJSONRepresentation} from "../classes/ScoreCalculator"; import {
BaseStats,
BestableFieldStats,
BoolFieldStats,
CellStats,
OutcomeType,
PlayerGameResults, RulesetStats,
TotalFieldStats
} from "../models/Stats";
class UpdateError extends Error { class StatsUpdater {
constructor(message: string) {
super(message);
this.name = "UpdateError";
}
}
export type OutcomeType = "win" | "loss" | "runnerUp" | "draw";
export interface PlayerStats extends BaseStats {
}
export interface AccountStats extends BaseStats {
timesNoWinner: number;
}
interface BaseStats {
statsByRuleset: Record<string, RulesetStats>
gamesPlayed: number;
}
interface RulesetStats {
blockStats: Record<string, BlockStats>;
wins: number;
runnerUps: number;
draws: number;
losses: number;
grandTotal: TotalFieldStats;
}
interface BlockStats {
cellStats: Record<string, CellStats>;
timesHadBonus?: number;
total: TotalFieldStats;
}
interface BaseCellStats {
runningTotal: number;
}
interface StrikeableFieldStats extends BaseCellStats {
timesStruck: number;
}
interface BestableFieldStats extends BaseCellStats {
best: number;
worst: number;
}
type TotalFieldStats = BestableFieldStats;
type BoolFieldStats = StrikeableFieldStats & { total: number };
type NumberFieldStats = StrikeableFieldStats & BestableFieldStats;
type MultiplierFieldStats = NumberFieldStats;
type SuperkadiFieldStats = NumberFieldStats;
type CellStats = BoolFieldStats | NumberFieldStats | MultiplierFieldStats | SuperkadiFieldStats;
export interface PlayerGameResults {
blocks: Record<string, BlockResults>;
}
interface BlockResults {
cells: Record<string, CellResults>
}
interface CellResults {
value: CellValue;
}
type CellValue = "cellFlagStrike" | number | boolean;
class BaseStatsUpdater {
private data?: BaseStats; private data?: BaseStats;
private validationRuleset?: Ruleset; private validationRuleset?: Ruleset;
private calculator?: ScoreCalculator; private calculator?: ScoreCalculator;
@@ -138,49 +90,4 @@ class BaseStatsUpdater {
} }
} }
export class PlayerStatsUpdater { export default StatsUpdater;
private data?: PlayerStats;
private readonly updater: BaseStatsUpdater;
constructor(data?: PlayerStats) {
if (data) {
this.data = data;
}
this.updater = new BaseStatsUpdater();
}
use(data: PlayerStats) {
this.data = data;
this.updater.use(data);
}
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
this.updater.updateStats(playerGameResults, ruleset);
}
}
export class AccountStatsUpdater {
private data?: AccountStats;
private readonly updater: BaseStatsUpdater;
constructor(data?: AccountStats) {
if (data) {
this.data = data;
}
this.updater = new BaseStatsUpdater();
}
use(data: AccountStats) {
this.data = data;
this.updater.use(data);
}
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
if (this.data) {
this.updater.updateStats(playerGameResults, 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.`);
}
}
}

View File

@@ -1,6 +1,6 @@
import passport from "passport"; import passport from "passport";
import DbUser from "../models/dbUser_old";
import {RequestHandler} from "express"; import {RequestHandler} from "express";
import StoredUsers, {LoginDetails} from "../models/StoredUser";
export const showLoginPage: RequestHandler = (req, res) => { export const showLoginPage: RequestHandler = (req, res) => {
res.render("login.ejs"); res.render("login.ejs");
@@ -20,7 +20,8 @@ export const showRegistrationPage: RequestHandler = (req, res) => {
export const registerNewUser: RequestHandler = async (req, res) => { export const registerNewUser: RequestHandler = async (req, res) => {
try { try {
const newUser = await DbUser.registerUser(req.body.username, req.body.email, req.body.password); const loginDetails: LoginDetails = req.body as LoginDetails;
const newUser = await StoredUsers.registerUser(loginDetails);
req.login(newUser, (err) => { req.login(newUser, (err) => {
if (err) { if (err) {
throw err; throw err;

29
src/errors.ts Normal file
View File

@@ -0,0 +1,29 @@
export class KadiError extends Error {}
export class UpdateError extends KadiError {
constructor(message: string) {
super(message);
this.name = "UpdateError";
}
}
export class GenericPersistenceError extends KadiError {
constructor(message: string) {
super(message);
this.name = "GenericPersistenceError";
}
}
export class MongoError extends GenericPersistenceError {
constructor(message: string) {
super(message);
this.name = "MongoError";
}
}
export class ModelParameterError extends GenericPersistenceError {
constructor(message: string) {
super(message);
this.name = "ModelParameterError";
}
}

View File

@@ -0,0 +1,15 @@
import StoredObject from "./StoredObject";
abstract class MongoStoredObject<RawDataInterface> implements StoredObject {
protected constructor(protected data: {_id: string} & RawDataInterface) {}
id(): string {
return this.data._id;
}
rawData(): RawDataInterface {
return this.data;
}
}
export default MongoStoredObject;

View File

@@ -0,0 +1,84 @@
import StoredObject, {StoredObjectConstructor, StoredObjectId} from "./StoredObject";
import mongo from "mongodb";
import {tryQuery} from "./utils";
import {MongoError} from "../errors";
import StoredObjectCollection from "./StoredObjectCollection";
abstract class MongoStoredObjectCollection<IRawData, IStoredObject extends StoredObject>
implements StoredObjectCollection<IStoredObject> {
protected mongoDbClientCollection: mongo.Collection;
protected StoredObjectConstructor: StoredObjectConstructor<IRawData, IStoredObject>;
protected constructor(
collectionClient: mongo.Collection,
objectConstructor: StoredObjectConstructor<IRawData, IStoredObject>
) {
this.mongoDbClientCollection = collectionClient;
this.StoredObjectConstructor = objectConstructor;
}
private async create(objectData: Omit<IRawData, "_id">): Promise<IRawData> {
return tryQuery(async () => {
const insertOneWriteOpResult = await this.mongoDbClientCollection.insertOne(objectData);
if (insertOneWriteOpResult.result.ok === 1) {
return insertOneWriteOpResult.ops[0]
}
else {
throw new MongoError(`Error creating the object: ${JSON.stringify(objectData)}`);
}
});
}
protected async newObject(objectData: Omit<IRawData, "_id">): Promise<IStoredObject> {
return new this.StoredObjectConstructor(await this.create(objectData));
}
protected async findObjectById(id: string): Promise<IRawData | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({_id: id})
);
}
protected async findObjectByAttribute(attribute: string, value: any): Promise<IRawData | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({[attribute]: value})
);
}
async deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise<IStoredObject | null | void> {
let deletedObject;
if (returnObject ?? true) {
deletedObject = await this.findById(objectId);
}
const deleteWriteOpResult = await this.mongoDbClientCollection.deleteOne({_id: objectId});
if (deleteWriteOpResult.result.ok === 1) {
return deletedObject;
}
else {
throw new MongoError(`Error deleting the object with id: ${JSON.stringify(objectId)}`);
}
}
async findById(id: string): Promise<IStoredObject | null> {
const data = await this.findObjectById(id);
if (data) {
return new this.StoredObjectConstructor(data);
}
else {
return null;
}
};
async save(...objects: IStoredObject[]): Promise<void> {
await tryQuery(async () => {
for (const object of objects) {
await this.mongoDbClientCollection.findOneAndUpdate({_id: object.id()}, {...object.rawData()});
}
});
}
}
export default MongoStoredObjectCollection;

100
src/models/Stats.ts Executable file
View File

@@ -0,0 +1,100 @@
import {DEFAULT_RULESET_NAME, DefaultRulesetStats} from "../rulesets";
export type OutcomeType = "win" | "loss" | "runnerUp" | "draw";
export interface PlayerStats extends BaseStats {}
export interface AccountStats extends BaseStats {
timesNoWinner: number;
}
interface BaseStats {
statsByRuleset: Record<string, RulesetStats> & { [DEFAULT_RULESET_NAME]: DefaultRulesetStats }
gamesPlayed: number;
}
export interface RulesetStats {
blockStats: Record<string, BlockStats>;
wins: number;
runnerUps: number;
draws: number;
losses: number;
grandTotal: TotalFieldStats;
}
interface BlockStats {
cellStats: Record<string, CellStats>;
timesHadBonus?: number;
total: TotalFieldStats;
}
interface BaseCellStats {
runningTotal: number;
}
interface StrikeableFieldStats extends BaseCellStats {
timesStruck: number;
}
interface BestableFieldStats extends BaseCellStats {
best: number;
worst: number;
}
type TotalFieldStats = BestableFieldStats;
type BoolFieldStats = StrikeableFieldStats & { total: number };
type NumberFieldStats = StrikeableFieldStats & BestableFieldStats;
type MultiplierFieldStats = NumberFieldStats;
type SuperkadiFieldStats = NumberFieldStats;
type CellStats = BoolFieldStats | NumberFieldStats | MultiplierFieldStats | SuperkadiFieldStats;
export interface PlayerGameResults {
blocks: Record<string, BlockResults>;
}
interface BlockResults {
cells: Record<string, CellResults>
}
interface CellResults {
value: CellValue;
}
type CellValue = "cellFlagStrike" | number | boolean;
function default
function defaultBoolFieldStats(): BoolFieldStats {
}
function defaultNumberFieldStats(): NumberFieldStats {
}
function defaultMultiplierFieldStats(): MultiplierFieldStats {
}
function defaultSuperkadiFieldStats(): SuperkadiFieldStats {
}
function defaultBaseStatsData(): BaseStats {
return {
statsByRuleset: {
[DEFAULT_RULESET_NAME]: {}
wins: 0,
runnerUps: 0,
draws: 0,
losses: 0,
grandTotal: {},
},
gamesPlayed: 0;
};
}
function defaultAccountStatsData(): AccountStats {
return {...defaultBaseStatsData(), timesNoWinner: 0};
}
function defaultPlayerStatsData(): PlayerStats {
return defaultBaseStatsData();
}
export default {
defaultPlayerStatsData: defaultPlayerStatsData,
defaultAccountStatsData: defaultAccountStatsData,
};

View File

@@ -0,0 +1,9 @@
export type StoredObjectConstructor<Data, Object> = new (data: Data, ...args: any[]) => Object;
export type StoredObjectId = string;
interface StoredObject {
id(): string;
rawData(): any;
}
export default StoredObject;

View File

@@ -0,0 +1,9 @@
import StoredObject, {StoredObjectId} from "./StoredObject";
interface StoredObjectCollection<StoredObjectInterface extends StoredObject> {
findById(id: string): Promise<StoredObjectInterface | null>;
deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise<StoredObjectInterface | null | void>;
save(...objects: StoredObjectInterface[]): Promise<void>;
}
export default StoredObjectCollection;

View File

@@ -1,16 +1,13 @@
import { import Stats, {OutcomeType, PlayerGameResults, PlayerStats} from "./Stats";
OutcomeType, import {getMongoObjectCollection} from "./utils";
PlayerGameResults,
PlayerStats,
PlayerStatsUpdater
} from "./stats";
import {
getMongoObjectCollection,
MongoStoredObject, MongoStoredObjectCollection, StoredObject, StoredObjectCollection, tryQuery,
} from "./utils";
import {CellValue} from "../controllers/statsController"; import {CellValue} from "../controllers/statsController";
import mongo from "mongodb"; import mongo from "mongodb";
import {Ruleset} from "../rulesets"; import {Ruleset} from "../rulesets";
import StoredObjectCollection from "./StoredObjectCollection";
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
import StoredObject from "./StoredObject";
import PlayerStatsUpdater from "../classes/PlayerStatsUpdater";
import MongoStoredObject from "./MongoStoredObject";
export interface CellDetails { export interface CellDetails {
id: string; id: string;
@@ -24,6 +21,7 @@ export interface StoredPlayerData {
} }
interface StoredPlayerCollection extends StoredObjectCollection<StoredPlayer> { interface StoredPlayerCollection extends StoredObjectCollection<StoredPlayer> {
newPlayer(nick: string): Promise<StoredPlayer>;
} }
class MongoStoredPlayerCollection class MongoStoredPlayerCollection
@@ -36,10 +34,9 @@ class MongoStoredPlayerCollection
this.updater = new PlayerStatsUpdater(); this.updater = new PlayerStatsUpdater();
} }
newPlayer(nick: string): Promise<StoredPlayer> { async newPlayer(nick: string): Promise<StoredPlayer> {
return tryQuery(async () => { const newPlayer = {nick, stats: Stats.makeBlankDataFields()};
return this.create({nick}); return this.newObject(newPlayer);
});
} }
} }

View File

@@ -1,9 +1,9 @@
import {SupportedLang} from "../enums"; import {SupportedLang} from "../enums";
import Player, {MongoStoredPlayer, StoredPlayer, StoredPlayerData} from "./StoredPlayer"; import {StoredPlayer} from "./StoredPlayer";
import {AccountStats} from "./stats"; import {AccountStats} from "./Stats";
import {SavedGameData, StoredSavedGame} from "./savedGame"; import {SavedGameData, StoredSavedGame} from "./savedGame";
import { import {
GenericModelError, getMongoObjectCollection, GenericPersistenceError, getMongoObjectCollection,
MongoStoredObject, MongoStoredObject,
MongoStoredObjectCollection, MongoStoredObjectCollection,
StoredObject, StoredObject,
@@ -57,7 +57,7 @@ export interface StoredUser extends StoredObject {
} }
type GuestUpdateParams = { id: string, newNick: string }; type GuestUpdateParams = { id: string, newNick: string };
type LoginDetails = { username: string, email: string, password: string }; export type LoginDetails = { username: string, email: string, password: string };
class MongoStoredUser extends MongoStoredObject<StoredUserData> implements StoredUser { class MongoStoredUser extends MongoStoredObject<StoredUserData> implements StoredUser {
constructor(data: StoredUserData) { constructor(data: StoredUserData) {
@@ -165,17 +165,6 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection<StoredUserDa
); );
} }
private async addNewUser(loginDetails: LoginDetails): Promise<StoredUser> {
const newPlayer = await StoredPlayers.newPlayer(loginDetails.username);
return this.create({
username: loginDetails.username,
email: loginDetails.email,
password: loginDetails.password,
lang: SupportedLang.gb,
player: newPlayer.id()
});
}
async registerUser(loginDetails: LoginDetails): Promise<StoredUser> { async registerUser(loginDetails: LoginDetails): Promise<StoredUser> {
const usernameTaken = await this.userWithUsernameExists(loginDetails.username); const usernameTaken = await this.userWithUsernameExists(loginDetails.username);
const emailTaken = await this.userWithEmailExists(loginDetails.email); const emailTaken = await this.userWithEmailExists(loginDetails.email);
@@ -183,13 +172,23 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection<StoredUserDa
throw new CredentialsTakenError(usernameTaken, emailTaken); throw new CredentialsTakenError(usernameTaken, emailTaken);
} }
else { else {
const hashedPassword = await bcrypt.hash(loginDetails.password, 10); return this.addNewUser({...loginDetails})
return tryQuery(() =>
this.addNewUser({...loginDetails, password: hashedPassword})
);
} }
} }
private async addNewUser(loginDetails: LoginDetails): Promise<StoredUser> {
const newPlayer = await StoredPlayers.newPlayer(loginDetails.username);
return tryQuery(async () =>
this.create({
username: loginDetails.username,
email: loginDetails.email,
password: await this.makePasswordSecure(loginDetails.password),
lang: SupportedLang.gb,
player: newPlayer.id()
})
);
}
async userWithEmailExists(email: string): Promise<boolean> { async userWithEmailExists(email: string): Promise<boolean> {
const object = await this.findObjectByAttribute("email", email); const object = await this.findObjectByAttribute("email", email);
return object !== null; return object !== null;
@@ -206,9 +205,13 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection<StoredUserDa
return dbResult; return dbResult;
} }
else { else {
throw new GenericModelError("User not found!"); throw new GenericPersistenceError("User not found!");
} }
} }
async makePasswordSecure(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
} }
const StoredUsers = new MongoStoredUserCollection(getMongoObjectCollection("users")); const StoredUsers = new MongoStoredUserCollection(getMongoObjectCollection("users"));

View File

@@ -1,15 +1,12 @@
import mongoose, {Types} from "mongoose";
import Player, {IPlayer} from "./StoredPlayer";
import {GameSubmission} from "../controllers/statsController"; import {GameSubmission} from "../controllers/statsController";
import {tryQuery, globalSchemaOptions, StoredObjectCollection, StoredObject} from "./utils"; import {StoredObjectCollection, StoredObject, StoredObjectId} from "./utils";
import DbUser from "./dbUser_old"; import {PlayerGameResults} from "./Stats";
import {Ruleset} from "../rulesets";
export interface SavedGameData { export interface SavedGameData {
id: string; id: string;
rulesetUsed: RulesetData; rulesetUsed: RulesetData;
players: mongoose.Types.ObjectId[]; players: StoredObjectId[];
results: []; results: PlayerGameResults[];
} }
export interface StoredSavedGame extends StoredObject { export interface StoredSavedGame extends StoredObject {

View File

@@ -1,5 +1,6 @@
import mongo, {MongoClient, Db} from "mongodb"; import {MongoClient, Db} from "mongodb";
import Settings from "../server-config.json"; import Settings from "../server-config.json";
import {GenericPersistenceError, MongoError} from "../errors";
let SessionDbClient: Db; let SessionDbClient: Db;
export async function initMongoSessionClient() { export async function initMongoSessionClient() {
@@ -18,121 +19,13 @@ export function getMongoObjectCollection(collectionName: string) {
} }
} }
export type StoredObjectId = string; type CallbackWrapper = <T>(query: () => T) => Promise<T>;
export interface StoredObjectCollection<K> {
findById(id: string): Promise<K | null>;
}
export abstract class MongoStoredObjectCollection<D, K extends StoredObject> implements StoredObjectCollection<K> {
protected mongoDbClientCollection: mongo.Collection;
protected MongoStoredObject: new(data: D, ...args: any[]) => K;
protected constructor(collectionClient: mongo.Collection, objectConstructor: new (data: D, ...args: any[]) => K) {
this.mongoDbClientCollection = collectionClient;
this.MongoStoredObject = objectConstructor;
}
protected async findObjectById(id: string): Promise<any | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({_id: id})
);
}
protected async findObjectByAttribute(attribute: string, value: any): Promise<any | null> {
return tryQuery(async () =>
await this.mongoDbClientCollection.findOne({attribute: value})
);
}
protected async create(objectData: Partial<D>): Promise<K> {
return tryQuery(async () =>
await this.mongoDbClientCollection.insertOne(objectData)
);
}
async deleteById(objectId: StoredObjectId): Promise<K | null> {
const deletedObject = this.findById(objectId);
await this.mongoDbClientCollection.deleteOne({_id: objectId});
return deletedObject;
}
async findById(id: string): Promise<K | null> {
const data = await this.findObjectById(id);
return new this.MongoStoredObject(data);
};
async save(...objects: K[]): Promise<void> {
await tryQuery(async () => {
for (const object of objects) {
await this.mongoDbClientCollection.findOneAndUpdate({_id: object.id()}, {...object.rawData()});
}
});
}
}
export interface StoredObject {
id(): string;
rawData(): any;
}
export abstract class MongoStoredObject<T> implements StoredObject {
protected constructor(protected data: {_id: string} & T) {}
id(): string {
return this.data._id;
}
rawData(): T {
return this.data;
}
}
export class MongoError extends Error {
constructor(message: string) {
super(message);
this.name = "MongoError";
}
}
export class GenericModelError extends MongoError {
constructor(message: string) {
super(message);
this.name = "GenericModelError";
}
}
export class ModelParameterError extends GenericModelError {
constructor(message: string) {
super(message);
this.name = "ModelParameterError";
}
}
type CallbackWrapper = <T>(query: () => T) => Promise<any>;
export const tryQuery: CallbackWrapper = async (cb) => { export const tryQuery: CallbackWrapper = async (cb) => {
try { try {
return cb(); return cb();
} }
catch (err) { catch (err) {
throw new GenericModelError(err); throw new GenericPersistenceError(err);
}
};
export const globalSchemaOptions = {
toObject: {
transform: function (doc: any, ret: any) {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
}
},
toJSON: {
transform: function (doc: any, ret: any) {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
},
} }
}; };

View File

@@ -1,8 +1,8 @@
import passport from "passport"; import passport from "passport";
import {Strategy as LocalStrategy, VerifyFunction} from "passport-local"; import {Strategy as LocalStrategy, VerifyFunction} from "passport-local";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import DbUser, {IDbUser} from "./models/dbUser_old";
import {NextFunction, Request, Response} from "express"; import {NextFunction, Request, Response} from "express";
import StoredUsers, {StoredUser} from "./models/StoredUser";
export const requireAuthenticated = (req: Request, res: Response, next: NextFunction) => { export const requireAuthenticated = (req: Request, res: Response, next: NextFunction) => {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
@@ -23,12 +23,12 @@ export const requireNotAuthenticated = (req: Request, res: Response, next: NextF
}; };
const authenticateUser: VerifyFunction = async (email, password, done) => { const authenticateUser: VerifyFunction = async (email, password, done) => {
const user = await DbUser.findByEmail(email); const user = await StoredUsers.findByEmail(email);
if (!user) { if (!user) {
return done(null, false, { message: "A user with that email does not exist."} ); return done(null, false, { message: "A user with that email does not exist."} );
} }
try { try {
if (await bcrypt.compare(password, user.password)) { if (await bcrypt.compare(password, (await user.getLoginDetails()).password)) {
return done(null, user); return done(null, user);
} else { } else {
return done(null, false, {message: "Password incorrect"}); return done(null, false, {message: "Password incorrect"});
@@ -41,11 +41,11 @@ const authenticateUser: VerifyFunction = async (email, password, done) => {
export const initialisePassport = () => { export const initialisePassport = () => {
passport.use(new LocalStrategy({ usernameField: "email" }, authenticateUser)); passport.use(new LocalStrategy({ usernameField: "email" }, authenticateUser));
passport.serializeUser((user: IDbUser, done) => { passport.serializeUser((user: StoredUser, done) => {
done(null, user.id) done(null, user.id())
}); });
passport.deserializeUser(async (id: string, done) => { passport.deserializeUser(async (id: string, done) => {
const user: IDbUser | null = await DbUser.getSerializedAuthUser(id); const user: StoredUser | null = await StoredUsers.getSerializedAuthUser(id);
done(null, user); done(null, user);
}); });
}; };

View File

@@ -1,8 +1,8 @@
import express from "express"; import express from "express";
import {requireAuthenticated} from "../passport-config"; import {requireAuthenticated} from "../passport-config";
import routers from "./routers"; import routers from "./routers";
import {IDbUser} from "../models/dbUser_old";
import {ModelParameterError} from "../models/utils"; import {ModelParameterError} from "../models/utils";
import {LoginDetails} from "../models/StoredUser";
const router = express.Router(); const router = express.Router();
@@ -10,13 +10,13 @@ router.use("/account", routers.signup);
router.use("/api", routers.api); router.use("/api", routers.api);
router.get("/game", requireAuthenticated, (req, res) => { router.get("/game", requireAuthenticated, (req, res) => {
res.render("gameIndex.ejs", { res.render("gameIndex.ejs", {
username: (req.user as IDbUser).username, username: (req.user as LoginDetails).username,
rootUrl: req.app.locals.rootUrl rootUrl: req.app.locals.rootUrl
}); });
}); });
router.get("/**", requireAuthenticated, (req, res) => { router.get("/**", requireAuthenticated, (req, res) => {
res.render("frontendIndex.ejs", { res.render("frontendIndex.ejs", {
username: (req.user as IDbUser).username, username: (req.user as LoginDetails).username,
rootUrl: req.app.locals.rootUrl rootUrl: req.app.locals.rootUrl
}); });
}); });

View File

@@ -1,36 +1,38 @@
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,
[FieldType.bool]: false, [FieldType.bool]: false,
[FieldType.subtotal]: 0, [FieldType.subtotal]: 0,
[FieldType.total]: 0, [FieldType.total]: 0,
[FieldType.bonus]: 0, [FieldType.bonus]: 0,
[FieldType.superkadi]: 0, [FieldType.superkadi]: 0,
[FieldType.multiplier]: 0, [FieldType.multiplier]: 0,
}; };
export interface Ruleset { export interface Ruleset {
id: string; id: string;
label: string; label: string;
blocks: Record<string, BlockDef>; blocks: Record<string, BlockDef>;
} }
export type BlockDef = BonusBlockDef | NoBonusBlockDef; export type BlockDef = BonusBlockDef | NoBonusBlockDef;
export interface NoBonusBlockDef extends DefaultBlockDef { export interface NoBonusBlockDef extends DefaultBlockDef {
hasBonus: false; hasBonus: false;
} }
export interface BonusBlockDef extends DefaultBlockDef { export interface BonusBlockDef extends DefaultBlockDef {
hasBonus: true; hasBonus: true;
bonusScore: number; bonusScore: number;
bonusFor: number; bonusFor: number;
} }
interface DefaultBlockDef { interface DefaultBlockDef {
label: string; label: string;
cells: Record<string, CellDef>; cells: Record<string, CellDef>;
} }
export type CellDef = export type CellDef =
@@ -40,246 +42,152 @@ export type CellDef =
| SuperkadiCellDef; | SuperkadiCellDef;
export interface BoolCellDef extends DefaultCellDef { export interface BoolCellDef extends DefaultCellDef {
fieldType: FieldType.bool; fieldType: FieldType.bool;
score: number; score: number;
} }
export interface MultiplierCellDef extends DefaultCellDef { export interface MultiplierCellDef extends DefaultCellDef {
fieldType: FieldType.multiplier; fieldType: FieldType.multiplier;
multiplier: number; multiplier: number;
maxMultiples: number; maxMultiples: number;
} }
export interface SuperkadiCellDef extends DefaultCellDef { export interface SuperkadiCellDef extends DefaultCellDef {
fieldType: FieldType.superkadi; fieldType: FieldType.superkadi;
score: number; score: number;
maxSuperkadis: number; maxSuperkadis: number;
} }
export interface NumberCellDef extends DefaultCellDef { export interface NumberCellDef extends DefaultCellDef {
fieldType: FieldType.number; fieldType: FieldType.number;
} }
interface DefaultCellDef { interface DefaultCellDef {
label: string; label: string;
} }
// ----- Predefined sets // ----- Predefined sets
export const DEFAULT_RULESET_NAME = "DEFAULT_RULESET";
const defaultDiceCount = 5; const defaultDiceCount = 5;
const DEFAULT_RULESET = "DEFAULT_RULESET";
const gameSchemas: Ruleset[] = [ const gameSchemas: Ruleset[] = [
{ {
id: DEFAULT_RULESET, id: DEFAULT_RULESET_NAME,
label: "Standard Kadi Rules (en)", label: "Standard Kadi Rules (en)",
blocks: { blocks: {
top: { top: {
label: "Upper", label: "Upper",
hasBonus: true, hasBonus: true,
bonusScore: 35, bonusScore: 35,
bonusFor: 63, bonusFor: 63,
cells: { cells: {
aces: { aces: {
fieldType: FieldType.multiplier, fieldType: FieldType.multiplier,
label: "Aces", label: "Aces",
multiplier: 1, multiplier: 1,
maxMultiples: defaultDiceCount, maxMultiples: defaultDiceCount,
}, },
twos: { twos: {
fieldType: FieldType.multiplier, fieldType: FieldType.multiplier,
label: "Twos", label: "Twos",
multiplier: 2, multiplier: 2,
maxMultiples: defaultDiceCount, maxMultiples: defaultDiceCount,
}, },
threes: { threes: {
fieldType: FieldType.multiplier, fieldType: FieldType.multiplier,
label: "Threes", label: "Threes",
multiplier: 3, multiplier: 3,
maxMultiples: defaultDiceCount, maxMultiples: defaultDiceCount,
}, },
fours: { fours: {
fieldType: FieldType.multiplier, fieldType: FieldType.multiplier,
label: "Fours", label: "Fours",
multiplier: 4, multiplier: 4,
maxMultiples: defaultDiceCount, maxMultiples: defaultDiceCount,
}, },
fives: { fives: {
fieldType: FieldType.multiplier, fieldType: FieldType.multiplier,
label: "Fives", label: "Fives",
multiplier: 5, multiplier: 5,
maxMultiples: defaultDiceCount, maxMultiples: defaultDiceCount,
}, },
sixes: { sixes: {
fieldType: FieldType.multiplier, fieldType: FieldType.multiplier,
label: "Sixes", label: "Sixes",
multiplier: 6, multiplier: 6,
maxMultiples: defaultDiceCount, maxMultiples: defaultDiceCount,
}, },
},
},
bottom: {
label: "Lower",
hasBonus: false,
cells: {
threeKind: {
fieldType: FieldType.number,
label: "Three of a Kind",
},
fourKind: {
fieldType: FieldType.number,
label: "Four of a Kind",
},
fullHouse: {
fieldType: FieldType.bool,
label: "Full House",
score: 25,
},
smlStraight: {
fieldType: FieldType.bool,
label: "Small Straight",
score: 30,
},
lgSraight: {
fieldType: FieldType.bool,
label: "Large Straight",
score: 40,
},
superkadi: {
fieldType: FieldType.superkadi,
label: "Super Kadis",
score: 50,
maxSuperkadis: 5,
},
chance: {
fieldType: FieldType.number,
label: "Chance",
},
},
},
}, },
}, }
bottom: {
label: "Lower",
hasBonus: false,
cells: {
threeKind: {
fieldType: FieldType.number,
label: "Three of a Kind",
},
fourKind: {
fieldType: FieldType.number,
label: "Four of a Kind",
},
fullHouse: {
fieldType: FieldType.bool,
label: "Full House",
score: 25,
},
smlStraight: {
fieldType: FieldType.bool,
label: "Small Straight",
score: 30,
},
lgSraight: {
fieldType: FieldType.bool,
label: "Large Straight",
score: 40,
},
kadi: {
fieldType: FieldType.superkadi,
label: "Super Kadis",
score: 50,
maxSuperkadis: 5,
},
chance: {
fieldType: FieldType.number,
label: "Chance",
},
},
},
},
},
{
id: DEFAULT_RULESET,
label: "Standard-Kadi-Regelwerk (de)",
blocks: {
top: {
label: "Oben",
hasBonus: true,
bonusScore: 35,
bonusFor: 63,
cells: {
aces: {
fieldType: FieldType.multiplier,
label: "Einser",
multiplier: 1,
maxMultiples: defaultDiceCount,
},
twos: {
fieldType: FieldType.multiplier,
label: "Zweier",
multiplier: 2,
maxMultiples: defaultDiceCount,
},
threes: {
fieldType: FieldType.multiplier,
label: "Dreier",
multiplier: 3,
maxMultiples: defaultDiceCount,
},
fours: {
fieldType: FieldType.multiplier,
label: "Vierer",
multiplier: 4,
maxMultiples: defaultDiceCount,
},
fives: {
fieldType: FieldType.multiplier,
label: "Fünfer",
multiplier: 5,
maxMultiples: defaultDiceCount,
},
sixes: {
fieldType: FieldType.multiplier,
label: "Sechser",
multiplier: 6,
maxMultiples: defaultDiceCount,
},
},
},
bottom: {
label: "Unten",
hasBonus: false,
cells: {
threeKind: {
fieldType: FieldType.number,
label: "Dreierpasch",
},
fourKind: {
fieldType: FieldType.number,
label: "Viererpasch",
},
fullSouse: {
fieldType: FieldType.bool,
label: "Full House",
score: 25,
},
smlStraight: {
fieldType: FieldType.bool,
label: "Kleine Straße",
score: 30,
},
lgStraight: {
fieldType: FieldType.bool,
label: "Große Straße",
score: 40,
},
kadi: {
fieldType: FieldType.superkadi,
label: "Ultrakadi",
score: 50,
maxSuperkadis: 5,
},
change: {
fieldType: FieldType.number,
label: "Chance",
},
},
},
},
},
]; ];
export function getGameSchemaById(schemaId: string): Ruleset { export function getGameSchemaById(schemaId: string): Ruleset {
for (const schema of gameSchemas) { for (const schema of gameSchemas) {
if (schema.id === schemaId) { if (schema.id === schemaId) {
return schema; return schema;
}
} }
} throw new RangeError("No such GameSchema with id '" + schemaId + "'!");
throw new RangeError("No such GameSchema with id '" + schemaId + "'!");
} }
export interface SchemaListing { export interface SchemaListing {
id: string; id: string;
label: string; label: string;
} }
export function getSchemaListings(): SchemaListing[] { export function getSchemaListings(): SchemaListing[] {
return gameSchemas.map((s) => ({ id: s.id, label: s.label })); return gameSchemas.map((s) => ({id: s.id, label: s.label}));
} }