Big update progress on encapsulating models
This commit is contained in:
31
src/classes/AccountStatsUpdater.ts
Normal file
31
src/classes/AccountStatsUpdater.ts
Normal 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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/classes/PlayerStatsUpdater.ts
Normal file
24
src/classes/PlayerStatsUpdater.ts
Normal 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;
|
||||
@@ -1,12 +1,12 @@
|
||||
import ScoreCell, {
|
||||
import ScoreCellCalculator, {
|
||||
createCellFromDef,
|
||||
ScoreCellValue,
|
||||
CellState,
|
||||
ScoreCellJSONRepresentation
|
||||
} from "./ScoreCell";
|
||||
} from "./ScoreCellCalculator";
|
||||
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) {
|
||||
return new ScoreBlockWithBonus(blockId, blockDef);
|
||||
}
|
||||
@@ -19,16 +19,16 @@ export interface ScoreBlockJSONRepresentation {
|
||||
cells: Record<string, ScoreCellJSONRepresentation>;
|
||||
}
|
||||
|
||||
abstract class ScoreBlock {
|
||||
protected cells: ScoreCell[];
|
||||
abstract class ScoreBlockCalculator {
|
||||
protected cells: ScoreCellCalculator[];
|
||||
protected id: string;
|
||||
|
||||
protected constructor(blockId: string, blockDef: BlockDef) {
|
||||
this.cells = ScoreBlock.generateCells(blockDef.cells);
|
||||
this.cells = ScoreBlockCalculator.generateCells(blockDef.cells);
|
||||
this.id = blockId;
|
||||
}
|
||||
|
||||
private static generateCells(cellDefs: Record<string, CellDef>): ScoreCell[] {
|
||||
private static generateCells(cellDefs: Record<string, CellDef>): ScoreCellCalculator[] {
|
||||
const cells = [];
|
||||
for (const cellId in cellDefs) {
|
||||
cells.push(createCellFromDef(cellId, cellDefs[cellId]));
|
||||
@@ -56,7 +56,7 @@ abstract class ScoreBlock {
|
||||
return this.getCellById(cellId).isStruck();
|
||||
}
|
||||
|
||||
private getCellById(cellId: string): ScoreCell {
|
||||
private getCellById(cellId: string): ScoreCellCalculator {
|
||||
const foundScoreCell = this.cells.find(cell => cell.getId() === cellId);
|
||||
if (foundScoreCell !== undefined) {
|
||||
return foundScoreCell;
|
||||
@@ -77,7 +77,7 @@ abstract class ScoreBlock {
|
||||
}
|
||||
}
|
||||
|
||||
class ScoreBlockWithBonus extends ScoreBlock {
|
||||
class ScoreBlockWithBonus extends ScoreBlockCalculator {
|
||||
protected readonly bonus: 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) {
|
||||
super(blockId, blockDef);
|
||||
}
|
||||
@@ -111,4 +111,4 @@ class ScoreBlockNoBonus extends ScoreBlock {
|
||||
}
|
||||
}
|
||||
|
||||
export default ScoreBlock;
|
||||
export default ScoreBlockCalculator;
|
||||
@@ -1,5 +1,5 @@
|
||||
import {BlockDef, Ruleset} from "../rulesets";
|
||||
import ScoreBlock, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlock";
|
||||
import ScoreBlockCalculator, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlockCalculator";
|
||||
|
||||
export type CellLocation = { blockId: string, cellId: string };
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface ScoreCardJSONRepresentation {
|
||||
}
|
||||
|
||||
class ScoreCalculator {
|
||||
private readonly blocks: ScoreBlock[];
|
||||
private readonly blocks: ScoreBlockCalculator[];
|
||||
|
||||
constructor(gameSchema: Ruleset) {
|
||||
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 = [];
|
||||
for (const blockId in blockDefs) {
|
||||
blocks.push(createBlockFromDef(blockId, blockDefs[blockId]));
|
||||
@@ -56,7 +56,7 @@ class ScoreCalculator {
|
||||
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);
|
||||
if (foundScoreBlock !== undefined) {
|
||||
return foundScoreBlock;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "../rulesets";
|
||||
import { CellFlag, FieldType } from "../enums";
|
||||
|
||||
export const createCellFromDef = (cellId: string, cellDef: CellDef): ScoreCell => {
|
||||
export const createCellFromDef = (cellId: string, cellDef: CellDef): ScoreCellCalculator => {
|
||||
switch (cellDef.fieldType) {
|
||||
case FieldType.number:
|
||||
return new NumberScoreCell(cellId, cellDef);
|
||||
@@ -33,7 +33,7 @@ export interface ScoreCellJSONRepresentation {
|
||||
value: number | boolean | CellFlag.strike;
|
||||
}
|
||||
|
||||
abstract class ScoreCell {
|
||||
abstract class ScoreCellCalculator {
|
||||
protected readonly id: string;
|
||||
protected static readonly fieldType: FieldType;
|
||||
protected struck: boolean;
|
||||
@@ -69,7 +69,7 @@ abstract class ScoreCell {
|
||||
}
|
||||
}
|
||||
|
||||
class NumberScoreCell extends ScoreCell {
|
||||
class NumberScoreCell extends ScoreCellCalculator {
|
||||
protected static readonly fieldType = FieldType.number;
|
||||
|
||||
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;
|
||||
private readonly score: number;
|
||||
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;
|
||||
private readonly score: 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 readonly multiplier: 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
119
src/models/stats.ts → src/classes/StatsUpdater.ts
Executable file → Normal file
@@ -1,66 +1,18 @@
|
||||
import {Ruleset} from "../rulesets";
|
||||
import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator";
|
||||
import {UpdateError} from "../errors";
|
||||
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 {
|
||||
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 {
|
||||
class StatsUpdater {
|
||||
private data?: BaseStats;
|
||||
private validationRuleset?: Ruleset;
|
||||
private calculator?: ScoreCalculator;
|
||||
@@ -138,49 +90,4 @@ class BaseStatsUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
export class PlayerStatsUpdater {
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
export default StatsUpdater;
|
||||
@@ -1,6 +1,6 @@
|
||||
import passport from "passport";
|
||||
import DbUser from "../models/dbUser_old";
|
||||
import {RequestHandler} from "express";
|
||||
import StoredUsers, {LoginDetails} from "../models/StoredUser";
|
||||
|
||||
export const showLoginPage: RequestHandler = (req, res) => {
|
||||
res.render("login.ejs");
|
||||
@@ -20,7 +20,8 @@ export const showRegistrationPage: RequestHandler = (req, res) => {
|
||||
|
||||
export const registerNewUser: RequestHandler = async (req, res) => {
|
||||
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) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
|
||||
29
src/errors.ts
Normal file
29
src/errors.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
15
src/models/MongoStoredObject.ts
Normal file
15
src/models/MongoStoredObject.ts
Normal 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;
|
||||
84
src/models/MongoStoredObjectCollection.ts
Normal file
84
src/models/MongoStoredObjectCollection.ts
Normal 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
100
src/models/Stats.ts
Executable 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,
|
||||
};
|
||||
9
src/models/StoredObject.ts
Normal file
9
src/models/StoredObject.ts
Normal 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;
|
||||
9
src/models/StoredObjectCollection.ts
Normal file
9
src/models/StoredObjectCollection.ts
Normal 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;
|
||||
@@ -1,16 +1,13 @@
|
||||
import {
|
||||
OutcomeType,
|
||||
PlayerGameResults,
|
||||
PlayerStats,
|
||||
PlayerStatsUpdater
|
||||
} from "./stats";
|
||||
import {
|
||||
getMongoObjectCollection,
|
||||
MongoStoredObject, MongoStoredObjectCollection, StoredObject, StoredObjectCollection, tryQuery,
|
||||
} from "./utils";
|
||||
import Stats, {OutcomeType, PlayerGameResults, PlayerStats} from "./Stats";
|
||||
import {getMongoObjectCollection} from "./utils";
|
||||
import {CellValue} from "../controllers/statsController";
|
||||
import mongo from "mongodb";
|
||||
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 {
|
||||
id: string;
|
||||
@@ -24,6 +21,7 @@ export interface StoredPlayerData {
|
||||
}
|
||||
|
||||
interface StoredPlayerCollection extends StoredObjectCollection<StoredPlayer> {
|
||||
newPlayer(nick: string): Promise<StoredPlayer>;
|
||||
}
|
||||
|
||||
class MongoStoredPlayerCollection
|
||||
@@ -36,10 +34,9 @@ class MongoStoredPlayerCollection
|
||||
this.updater = new PlayerStatsUpdater();
|
||||
}
|
||||
|
||||
newPlayer(nick: string): Promise<StoredPlayer> {
|
||||
return tryQuery(async () => {
|
||||
return this.create({nick});
|
||||
});
|
||||
async newPlayer(nick: string): Promise<StoredPlayer> {
|
||||
const newPlayer = {nick, stats: Stats.makeBlankDataFields()};
|
||||
return this.newObject(newPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {SupportedLang} from "../enums";
|
||||
import Player, {MongoStoredPlayer, StoredPlayer, StoredPlayerData} from "./StoredPlayer";
|
||||
import {AccountStats} from "./stats";
|
||||
import {StoredPlayer} from "./StoredPlayer";
|
||||
import {AccountStats} from "./Stats";
|
||||
import {SavedGameData, StoredSavedGame} from "./savedGame";
|
||||
import {
|
||||
GenericModelError, getMongoObjectCollection,
|
||||
GenericPersistenceError, getMongoObjectCollection,
|
||||
MongoStoredObject,
|
||||
MongoStoredObjectCollection,
|
||||
StoredObject,
|
||||
@@ -57,7 +57,7 @@ export interface StoredUser extends StoredObject {
|
||||
}
|
||||
|
||||
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 {
|
||||
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> {
|
||||
const usernameTaken = await this.userWithUsernameExists(loginDetails.username);
|
||||
const emailTaken = await this.userWithEmailExists(loginDetails.email);
|
||||
@@ -183,13 +172,23 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection<StoredUserDa
|
||||
throw new CredentialsTakenError(usernameTaken, emailTaken);
|
||||
}
|
||||
else {
|
||||
const hashedPassword = await bcrypt.hash(loginDetails.password, 10);
|
||||
return tryQuery(() =>
|
||||
this.addNewUser({...loginDetails, password: hashedPassword})
|
||||
);
|
||||
return this.addNewUser({...loginDetails})
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
const object = await this.findObjectByAttribute("email", email);
|
||||
return object !== null;
|
||||
@@ -206,9 +205,13 @@ class MongoStoredUserCollection extends MongoStoredObjectCollection<StoredUserDa
|
||||
return dbResult;
|
||||
}
|
||||
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"));
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import mongoose, {Types} from "mongoose";
|
||||
import Player, {IPlayer} from "./StoredPlayer";
|
||||
import {GameSubmission} from "../controllers/statsController";
|
||||
import {tryQuery, globalSchemaOptions, StoredObjectCollection, StoredObject} from "./utils";
|
||||
import DbUser from "./dbUser_old";
|
||||
import {Ruleset} from "../rulesets";
|
||||
import {StoredObjectCollection, StoredObject, StoredObjectId} from "./utils";
|
||||
import {PlayerGameResults} from "./Stats";
|
||||
|
||||
export interface SavedGameData {
|
||||
id: string;
|
||||
rulesetUsed: RulesetData;
|
||||
players: mongoose.Types.ObjectId[];
|
||||
results: [];
|
||||
players: StoredObjectId[];
|
||||
results: PlayerGameResults[];
|
||||
}
|
||||
|
||||
export interface StoredSavedGame extends StoredObject {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import mongo, {MongoClient, Db} from "mongodb";
|
||||
import {MongoClient, Db} from "mongodb";
|
||||
import Settings from "../server-config.json";
|
||||
import {GenericPersistenceError, MongoError} from "../errors";
|
||||
|
||||
let SessionDbClient: Db;
|
||||
export async function initMongoSessionClient() {
|
||||
@@ -18,121 +19,13 @@ export function getMongoObjectCollection(collectionName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export type StoredObjectId = string;
|
||||
|
||||
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>;
|
||||
type CallbackWrapper = <T>(query: () => T) => Promise<T>;
|
||||
|
||||
export const tryQuery: CallbackWrapper = async (cb) => {
|
||||
try {
|
||||
return cb();
|
||||
}
|
||||
catch (err) {
|
||||
throw new GenericModelError(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;
|
||||
},
|
||||
throw new GenericPersistenceError(err);
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import passport from "passport";
|
||||
import {Strategy as LocalStrategy, VerifyFunction} from "passport-local";
|
||||
import bcrypt from "bcrypt";
|
||||
import DbUser, {IDbUser} from "./models/dbUser_old";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
import StoredUsers, {StoredUser} from "./models/StoredUser";
|
||||
|
||||
export const requireAuthenticated = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.isAuthenticated()) {
|
||||
@@ -23,12 +23,12 @@ export const requireNotAuthenticated = (req: Request, res: Response, next: NextF
|
||||
};
|
||||
|
||||
const authenticateUser: VerifyFunction = async (email, password, done) => {
|
||||
const user = await DbUser.findByEmail(email);
|
||||
const user = await StoredUsers.findByEmail(email);
|
||||
if (!user) {
|
||||
return done(null, false, { message: "A user with that email does not exist."} );
|
||||
}
|
||||
try {
|
||||
if (await bcrypt.compare(password, user.password)) {
|
||||
if (await bcrypt.compare(password, (await user.getLoginDetails()).password)) {
|
||||
return done(null, user);
|
||||
} else {
|
||||
return done(null, false, {message: "Password incorrect"});
|
||||
@@ -41,11 +41,11 @@ const authenticateUser: VerifyFunction = async (email, password, done) => {
|
||||
|
||||
export const initialisePassport = () => {
|
||||
passport.use(new LocalStrategy({ usernameField: "email" }, authenticateUser));
|
||||
passport.serializeUser((user: IDbUser, done) => {
|
||||
done(null, user.id)
|
||||
passport.serializeUser((user: StoredUser, done) => {
|
||||
done(null, user.id())
|
||||
});
|
||||
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);
|
||||
});
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import express from "express";
|
||||
import {requireAuthenticated} from "../passport-config";
|
||||
import routers from "./routers";
|
||||
import {IDbUser} from "../models/dbUser_old";
|
||||
import {ModelParameterError} from "../models/utils";
|
||||
import {LoginDetails} from "../models/StoredUser";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -10,13 +10,13 @@ router.use("/account", routers.signup);
|
||||
router.use("/api", routers.api);
|
||||
router.get("/game", requireAuthenticated, (req, res) => {
|
||||
res.render("gameIndex.ejs", {
|
||||
username: (req.user as IDbUser).username,
|
||||
username: (req.user as LoginDetails).username,
|
||||
rootUrl: req.app.locals.rootUrl
|
||||
});
|
||||
});
|
||||
router.get("/**", requireAuthenticated, (req, res) => {
|
||||
res.render("frontendIndex.ejs", {
|
||||
username: (req.user as IDbUser).username,
|
||||
username: (req.user as LoginDetails).username,
|
||||
rootUrl: req.app.locals.rootUrl
|
||||
});
|
||||
});
|
||||
|
||||
104
src/rulesets.ts
104
src/rulesets.ts
@@ -1,4 +1,6 @@
|
||||
import {FieldType} from "./enums";
|
||||
import RulesetsPage from "../../frontend/src/Components/RulesetsPage";
|
||||
import {RulesetStats} from "./models/Stats";
|
||||
|
||||
export const defaultCellValues = {
|
||||
[FieldType.number]: 0,
|
||||
@@ -66,12 +68,12 @@ interface DefaultCellDef {
|
||||
|
||||
|
||||
// ----- Predefined sets
|
||||
export const DEFAULT_RULESET_NAME = "DEFAULT_RULESET";
|
||||
const defaultDiceCount = 5;
|
||||
const DEFAULT_RULESET = "DEFAULT_RULESET";
|
||||
|
||||
const gameSchemas: Ruleset[] = [
|
||||
{
|
||||
id: DEFAULT_RULESET,
|
||||
id: DEFAULT_RULESET_NAME,
|
||||
label: "Standard Kadi Rules (en)",
|
||||
blocks: {
|
||||
top: {
|
||||
@@ -155,7 +157,7 @@ const gameSchemas: Ruleset[] = [
|
||||
score: 40,
|
||||
},
|
||||
|
||||
kadi: {
|
||||
superkadi: {
|
||||
fieldType: FieldType.superkadi,
|
||||
label: "Super Kadis",
|
||||
score: 50,
|
||||
@@ -169,101 +171,7 @@ const gameSchemas: Ruleset[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user