I think it's done?
This commit is contained in:
@@ -1,14 +1,20 @@
|
|||||||
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
|
|
||||||
import mongo from "mongodb";
|
|
||||||
import {CredentialsTakenError, GenericPersistenceError} from "../errors";
|
import {CredentialsTakenError, GenericPersistenceError} from "../errors";
|
||||||
import StoredPlayers from "../ObjectCollections/PlayerCollection";
|
import mongo from "mongodb";
|
||||||
import {SupportedLang} from "../enums";
|
|
||||||
import {AccountStatsMongoData, defaultAccountStatsMongoData} from "../Objects/DefaultStatsMongoData";
|
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
import {SupportedLang} from "../enums";
|
||||||
|
import {AccountStatsMongoData, defaultAccountStatsMongoData, OutcomeType, PlayerGameResults} from "../Objects/DefaultStatsMongoData";
|
||||||
import {getMongoObjectCollection} from "../database";
|
import {getMongoObjectCollection} from "../database";
|
||||||
import KadiUser, {LoginDetails} from "../Objects/KadiUser";
|
import KadiUser, {LoginDetails} from "../Objects/KadiUser";
|
||||||
import {ActiveRecordId} from "../Objects/ActiveRecord";
|
import {ActiveRecordId, OrId} from "../Objects/ActiveRecord";
|
||||||
import {SavedGameData} from "../Objects/savedGame";
|
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
|
||||||
|
import SavedGameCollection from "./SavedGameCollection";
|
||||||
|
import SavedGame from "../Objects/SavedGame";
|
||||||
|
import PlayerCollection from "../ObjectCollections/PlayerCollection";
|
||||||
|
import Player from "../Objects/Player";
|
||||||
|
import {GameSubmission} from "../controllers/statsController";
|
||||||
|
import Ruleset from "../Objects/Ruleset";
|
||||||
|
import RulesetCollection from "./RulesetCollection";
|
||||||
|
import AccountStats from "../Objects/AccountStats";
|
||||||
|
|
||||||
export interface KadiUserMongoData {
|
export interface KadiUserMongoData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -20,7 +26,7 @@ export interface KadiUserMongoData {
|
|||||||
player: ActiveRecordId;
|
player: ActiveRecordId;
|
||||||
guests: ActiveRecordId[];
|
guests: ActiveRecordId[];
|
||||||
accountStats: AccountStatsMongoData;
|
accountStats: AccountStatsMongoData;
|
||||||
savedGames: SavedGameData[];
|
savedGames: ActiveRecordId[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> {
|
class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> {
|
||||||
@@ -28,7 +34,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
|
|||||||
super(collectionClient);
|
super(collectionClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
private storedUserFrom(data: KadiUserMongoData): KadiUser {
|
private kadiUserFrom(data: KadiUserMongoData): KadiUser {
|
||||||
return new KadiUser(
|
return new KadiUser(
|
||||||
data.id,
|
data.id,
|
||||||
data.username,
|
data.username,
|
||||||
@@ -37,20 +43,15 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
|
|||||||
data.lang);
|
data.lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
async read(id: string): Promise<KadiUser | null> {
|
async read(id: string): Promise<KadiUser> {
|
||||||
const foundUser = await this.mongoRead(id);
|
const foundUser = await this.mongoRead(id);
|
||||||
if (foundUser) {
|
return this.kadiUserFrom(foundUser);
|
||||||
return this.storedUserFrom(foundUser);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByEmail(emailQuery: string): Promise<KadiUser | null> {
|
async findByEmail(emailQuery: string): Promise<KadiUser | null> {
|
||||||
const foundUser = await this.mongoFindByAttribute("email", emailQuery);
|
const foundUser = await this.mongoFindByAttribute("email", emailQuery);
|
||||||
if (foundUser) {
|
if (foundUser) {
|
||||||
return this.storedUserFrom(foundUser);
|
return this.kadiUserFrom(foundUser);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return null;
|
return null;
|
||||||
@@ -69,7 +70,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async addNewUser(loginDetails: LoginDetails): Promise<KadiUser> {
|
private async addNewUser(loginDetails: LoginDetails): Promise<KadiUser> {
|
||||||
const newPlayer = await StoredPlayers.create(loginDetails.username);
|
const newPlayer = await PlayerCollection.create(loginDetails.username);
|
||||||
const securePassword = await this.makePasswordSecure(loginDetails.password);
|
const securePassword = await this.makePasswordSecure(loginDetails.password);
|
||||||
const newUser = await this.mongoCreate({
|
const newUser = await this.mongoCreate({
|
||||||
username: loginDetails.username,
|
username: loginDetails.username,
|
||||||
@@ -82,7 +83,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
|
|||||||
guests: [],
|
guests: [],
|
||||||
savedGames: [],
|
savedGames: [],
|
||||||
});
|
});
|
||||||
return this.storedUserFrom(newUser);
|
return this.kadiUserFrom(newUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async userWithEmailExists(email: string): Promise<boolean> {
|
async userWithEmailExists(email: string): Promise<boolean> {
|
||||||
@@ -98,7 +99,7 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
|
|||||||
async getSerializedAuthUser(id: string): Promise<KadiUser> {
|
async getSerializedAuthUser(id: string): Promise<KadiUser> {
|
||||||
const foundUser = await this.mongoRead(id);
|
const foundUser = await this.mongoRead(id);
|
||||||
if (foundUser) {
|
if (foundUser) {
|
||||||
return this.storedUserFrom(foundUser);
|
return this.kadiUserFrom(foundUser);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new GenericPersistenceError("User not found!");
|
throw new GenericPersistenceError("User not found!");
|
||||||
@@ -108,6 +109,61 @@ class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData>
|
|||||||
async makePasswordSecure(password: string): Promise<string> {
|
async makePasswordSecure(password: string): Promise<string> {
|
||||||
return bcrypt.hash(password, 10);
|
return bcrypt.hash(password, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addGuestForUser(userOrId: OrId<KadiUser>, newGuestNick: string): Promise<Player> {
|
||||||
|
const newGuest = await PlayerCollection.create(newGuestNick);
|
||||||
|
await this.mongoDbClientCollection.findOneAndUpdate(
|
||||||
|
{_id: this.idFromRecordOrId(userOrId)},
|
||||||
|
{$push: {guests: newGuest.getId()}});
|
||||||
|
return newGuest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGuestFromUser(userOrId: OrId<KadiUser>, guestOrGuestId: OrId<Player>): Promise<Player> {
|
||||||
|
const deletedGuest = await PlayerCollection.delete(this.idFromRecordOrId(guestOrGuestId));
|
||||||
|
await this.mongoDbClientCollection.findOneAndUpdate(
|
||||||
|
{_id: this.idFromRecordOrId(userOrId)},
|
||||||
|
{$pull: {guests: this.idFromRecordOrId(guestOrGuestId)}});
|
||||||
|
return deletedGuest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllGuestsForUser(userOrId: OrId<KadiUser>): Promise<Promise<Player>[]> {
|
||||||
|
const guestIdList = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.guests;
|
||||||
|
return guestIdList.map(async (guestId) => {
|
||||||
|
return await PlayerCollection.read(guestId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMainPlayerForUser(userOrId: OrId<KadiUser>): Promise<Player> {
|
||||||
|
const userData = await this.mongoRead(this.idFromRecordOrId(userOrId));
|
||||||
|
return PlayerCollection.read(userData?.player);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSavedGamesForUser(userOrId: OrId<KadiUser>): Promise<Promise<SavedGame>[]> {
|
||||||
|
const savedGameIds = (await this.mongoRead(this.idFromRecordOrId(userOrId)))?.savedGames;
|
||||||
|
return savedGameIds.map(async (savedGameId) => {
|
||||||
|
return await SavedGameCollection.read(savedGameId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addGameForUser(userOrId: OrId<KadiUser>, gameSubmission: GameSubmission): Promise<SavedGame> {
|
||||||
|
const newGame = await SavedGameCollection.create(gameSubmission);
|
||||||
|
await this.mongoDbClientCollection.findOneAndUpdate(
|
||||||
|
{_id: this.idFromRecordOrId(userOrId)},
|
||||||
|
{$push: {savedGames: newGame.getId()}});
|
||||||
|
return newGame;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAccountStats(userOrId: OrId<KadiUser>, gameResults: (PlayerGameResults & {outcome: OutcomeType})[], rulesetUsedOrId: OrId<Ruleset>): Promise<void> {
|
||||||
|
const userId = this.idFromRecordOrId(userOrId);
|
||||||
|
const rulesetUsed = rulesetUsedOrId instanceof Ruleset ? rulesetUsedOrId : await RulesetCollection.read(rulesetUsedOrId);
|
||||||
|
const accountStatsMongoData = await this.mongoRead(userId);
|
||||||
|
const accountStatsObject = new AccountStats(accountStatsMongoData.accountStats);
|
||||||
|
accountStatsObject.updateStats(gameResults, rulesetUsed);
|
||||||
|
this.mongoUpdate({
|
||||||
|
id: this.idFromRecordOrId(userId),
|
||||||
|
accountStats: accountStatsObject.getData()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const KadiUserCollectionSingleton = new KadiUserCollection(getMongoObjectCollection("users"));
|
const KadiUserCollectionSingleton = new KadiUserCollection(getMongoObjectCollection("users"));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import mongo from "mongodb";
|
import mongo from "mongodb";
|
||||||
import {tryQuery} from "./database";
|
import {tryQuery} from "../database";
|
||||||
import {MongoError} from "../errors";
|
import {InvalidIdError, MongoError} from "../errors";
|
||||||
import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord";
|
import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord";
|
||||||
|
|
||||||
|
|
||||||
@@ -19,10 +19,15 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async mongoRead(id: string): Promise<IRawData | null> {
|
protected async mongoRead(id: string): Promise<IRawData> {
|
||||||
return tryQuery(async () =>
|
return tryQuery(async () => {
|
||||||
await this.mongoDbClientCollection.findOne({_id: id})
|
const result = await this.mongoDbClientCollection.findOne({_id: id});
|
||||||
);
|
if (result) {
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw new InvalidIdError(`Object in collection "${typeof this}" with id ${JSON.stringify(id)} not found!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async mongoFindByAttribute(attribute: string, value: any): Promise<IRawData | null> {
|
protected async mongoFindByAttribute(attribute: string, value: any): Promise<IRawData | null> {
|
||||||
@@ -31,7 +36,7 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async mongoDelete(objectId: ActiveRecordId, returnObject?: boolean): Promise<IRawData | null | void> {
|
protected async mongoDelete(objectId: ActiveRecordId, returnObject?: boolean): Promise<IRawData | void> {
|
||||||
let deletedObject;
|
let deletedObject;
|
||||||
if (returnObject ?? true) {
|
if (returnObject ?? true) {
|
||||||
deletedObject = await this.mongoRead(objectId);
|
deletedObject = await this.mongoRead(objectId);
|
||||||
@@ -40,15 +45,19 @@ abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}
|
|||||||
if (deleteWriteOpResult.result.ok === 1) {
|
if (deleteWriteOpResult.result.ok === 1) {
|
||||||
return deletedObject;
|
return deletedObject;
|
||||||
} else {
|
} else {
|
||||||
throw new MongoError(`Error deleting the object with id: ${JSON.stringify(objectId)}`);
|
throw new MongoError(`Error deleting the object in collection "${typeof this}" with id: ${JSON.stringify(objectId)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async mongoSave(object: IRawData) {
|
protected async mongoUpdate(object: Partial<IRawData> & {id: ActiveRecordId}) {
|
||||||
await tryQuery(() =>
|
await tryQuery(() =>
|
||||||
this.mongoDbClientCollection.findOneAndUpdate({_id: object.id}, {$set: {...object, id: undefined}})
|
this.mongoDbClientCollection.findOneAndUpdate({_id: object.id}, {$set: {...object, id: undefined}})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected idFromRecordOrId<T extends ActiveRecord>(recordOrRecordId: T | ActiveRecordId): ActiveRecordId {
|
||||||
|
return typeof recordOrRecordId === "string" ? recordOrRecordId : recordOrRecordId.getId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,62 @@
|
|||||||
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
|
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
|
||||||
import mongo from "mongodb";
|
import mongo from "mongodb";
|
||||||
import {defaultPlayerStatsMongoData, PlayerStatsMongoData} from "../Objects/DefaultStatsMongoData";
|
import {
|
||||||
import {getMongoObjectCollection} from "./database";
|
defaultPlayerStatsMongoData,
|
||||||
|
OutcomeType,
|
||||||
|
PlayerGameResults,
|
||||||
|
PlayerStatsMongoData
|
||||||
|
} from "../Objects/DefaultStatsMongoData";
|
||||||
|
import {getMongoObjectCollection} from "../database";
|
||||||
import Player from "../Objects/Player";
|
import Player from "../Objects/Player";
|
||||||
import PlayerStats from "../Objects/PlayerStats";
|
import PlayerStats from "../Objects/PlayerStats";
|
||||||
|
import {ActiveRecordId, OrId} from "../Objects/ActiveRecord";
|
||||||
|
import Ruleset from "../Objects/Ruleset";
|
||||||
|
import RulesetCollection from "./RulesetCollection";
|
||||||
|
|
||||||
export interface MongoStoredPlayerData {
|
export interface PlayerMongoData {
|
||||||
id: string;
|
id: string;
|
||||||
nick: string;
|
nick: string;
|
||||||
stats?: PlayerStatsMongoData;
|
stats?: PlayerStatsMongoData;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayerCollection extends MongoStoredObjectCollection<MongoStoredPlayerData> {
|
class PlayerCollection extends MongoStoredObjectCollection<PlayerMongoData> {
|
||||||
constructor(collectionClient: mongo.Collection) {
|
constructor(collectionClient: mongo.Collection) {
|
||||||
super(collectionClient);
|
super(collectionClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
private storedPlayerFrom(data: MongoStoredPlayerData): Player {
|
private playerFrom(data: PlayerMongoData): Player {
|
||||||
return new Player(data.id, data.nick, data.stats ? new PlayerStats(data.stats) : undefined);
|
return new Player(data.id, data.nick, data.stats ? new PlayerStats(data.stats) : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(nick: string): Promise<Player> {
|
async create(nick: string): Promise<Player> {
|
||||||
const newPlayer = {nick, stats: defaultPlayerStatsMongoData()};
|
const newPlayer = {nick, stats: defaultPlayerStatsMongoData()};
|
||||||
return this.storedPlayerFrom(await this.mongoCreate(newPlayer));
|
return this.playerFrom(await this.mongoCreate(newPlayer));
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(id: ActiveRecordId): Promise<Player> {
|
||||||
|
return this.playerFrom(await this.mongoRead(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: ActiveRecordId): Promise<Player> {
|
||||||
|
return this.playerFrom(await this.mongoDelete(id, true) as PlayerMongoData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(player: Player): Promise<Player> {
|
||||||
|
await this.mongoUpdate({id: player.getId(), nick: player.getNick(), stats: player.getStats()?.getData()});
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStatsForPlayer(playerOrId: OrId<Player>, gameResults: PlayerGameResults & {outcome: OutcomeType}, rulesetUsedOrId: OrId<Ruleset>): Promise<void> {
|
||||||
|
playerOrId = playerOrId instanceof Player ? playerOrId : await this.read(playerOrId);
|
||||||
|
rulesetUsedOrId = rulesetUsedOrId instanceof Ruleset ? rulesetUsedOrId : await RulesetCollection.read(rulesetUsedOrId);
|
||||||
|
playerOrId.updateStats(gameResults, rulesetUsedOrId);
|
||||||
|
this.mongoUpdate({
|
||||||
|
id: this.idFromRecordOrId(playerOrId),
|
||||||
|
stats: playerOrId.getStats()?.getData()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const StoredPlayers = new PlayerCollection(getMongoObjectCollection("players"));
|
|
||||||
export default StoredPlayers;
|
const PlayerCollectionSingleton = new PlayerCollection(getMongoObjectCollection("players"));
|
||||||
|
export default PlayerCollectionSingleton;
|
||||||
30
src/ObjectCollections/RulesetCollection.ts
Normal file
30
src/ObjectCollections/RulesetCollection.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
|
||||||
|
import mongo from "mongodb";
|
||||||
|
import {DEFAULT_RULESET, DEFAULT_RULESET_NAME, RulesetSchema} from "../rulesets";
|
||||||
|
import {getMongoObjectCollection} from "../database";
|
||||||
|
import Ruleset from "../Objects/Ruleset";
|
||||||
|
|
||||||
|
type RulesetMongoData = RulesetSchema;
|
||||||
|
|
||||||
|
class RulesetCollection extends MongoStoredObjectCollection<RulesetMongoData> {
|
||||||
|
constructor(collectionClient: mongo.Collection) {
|
||||||
|
super(collectionClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async rulesetFrom(data: RulesetMongoData): Promise<Ruleset> {
|
||||||
|
return new Ruleset(data.id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(id: string): Promise<Ruleset> {
|
||||||
|
if (id === DEFAULT_RULESET_NAME) {
|
||||||
|
return new Ruleset(DEFAULT_RULESET_NAME, DEFAULT_RULESET);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const foundRuleset = await this.mongoRead(id);
|
||||||
|
return this.rulesetFrom(foundRuleset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RulesetCollectionSingleton = new RulesetCollection(getMongoObjectCollection("users"));
|
||||||
|
export default RulesetCollectionSingleton;
|
||||||
54
src/ObjectCollections/SavedGameCollection.ts
Normal file
54
src/ObjectCollections/SavedGameCollection.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
|
||||||
|
import mongo from "mongodb";
|
||||||
|
import {ActiveRecordId} from "../Objects/ActiveRecord";
|
||||||
|
import PlayerCollection from "./PlayerCollection";
|
||||||
|
import SavedGame from "../Objects/SavedGame";
|
||||||
|
import {getMongoObjectCollection} from "../database";
|
||||||
|
import {PlayerGameResults} from "../Objects/DefaultStatsMongoData";
|
||||||
|
import RulesetCollection from "./RulesetCollection";
|
||||||
|
import {GameSubmission} from "../controllers/statsController";
|
||||||
|
|
||||||
|
export interface SavedGameMongoData {
|
||||||
|
id: string;
|
||||||
|
rulesetUsed: ActiveRecordId;
|
||||||
|
players: ActiveRecordId[];
|
||||||
|
results: PlayerGameResults[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SavedGameCollection extends MongoStoredObjectCollection<SavedGameMongoData> {
|
||||||
|
constructor(collectionClient: mongo.Collection) {
|
||||||
|
super(collectionClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async savedGameFrom(data: SavedGameMongoData): Promise<SavedGame> {
|
||||||
|
const playerList: {name: string, id: ActiveRecordId}[] = [];
|
||||||
|
for (const playerId of data.players) {
|
||||||
|
const player = await PlayerCollection.read(playerId);
|
||||||
|
playerList.push({name: player.getNick(), id: playerId})
|
||||||
|
}
|
||||||
|
const rulesetUsed = await RulesetCollection.read(data.rulesetUsed);
|
||||||
|
return new SavedGame(
|
||||||
|
data.id,
|
||||||
|
{name: rulesetUsed.getName(), id: data.rulesetUsed},
|
||||||
|
playerList,
|
||||||
|
data.results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(id: string): Promise<SavedGame> {
|
||||||
|
const foundGame = await this.mongoRead(id);
|
||||||
|
return this.savedGameFrom(foundGame);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(gameSubmission: GameSubmission): Promise<SavedGame> {
|
||||||
|
const pids = gameSubmission.players.map(playerIdAndNick => playerIdAndNick.id);
|
||||||
|
return this.savedGameFrom(
|
||||||
|
await this.mongoCreate({
|
||||||
|
rulesetUsed: gameSubmission.rulesetId,
|
||||||
|
players: pids,
|
||||||
|
results: gameSubmission.results})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SavedGameCollectionSingleton = new SavedGameCollection(getMongoObjectCollection("users"));
|
||||||
|
export default SavedGameCollectionSingleton;
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import {Ruleset} from "../rulesets";
|
import {RulesetSchema} from "../rulesets";
|
||||||
import {AccountStatsMongoData, OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
|
import {AccountStatsMongoData, OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
|
||||||
import {UpdateError} from "../errors";
|
import {UpdateError} from "../errors";
|
||||||
import StatsUpdater from "./StatsUpdater";
|
import StatsUpdater from "./StatsUpdater";
|
||||||
|
import Ruleset from "./Ruleset";
|
||||||
|
|
||||||
class AccountStats {
|
class AccountStats {
|
||||||
private data?: AccountStatsMongoData;
|
private data: AccountStatsMongoData;
|
||||||
private readonly updater: StatsUpdater;
|
private readonly updater: StatsUpdater;
|
||||||
constructor(data?: AccountStatsMongoData) {
|
constructor(data: AccountStatsMongoData) {
|
||||||
if (data) {
|
this.data = data;
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
this.updater = new StatsUpdater();
|
this.updater = new StatsUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,16 +17,22 @@ class AccountStats {
|
|||||||
this.updater.use(data);
|
this.updater.use(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
|
updateStats(playerGameResults: (PlayerGameResults & {outcome: OutcomeType})[], ruleset: Ruleset): void {
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.updater.updateStats(playerGameResults, ruleset);
|
for (const playerGameResult of playerGameResults) {
|
||||||
this.data.gamesPlayed += 1;
|
this.updater.updateStats(playerGameResult, ruleset);
|
||||||
|
this.data.gamesPlayed += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data
|
throw new UpdateError(`Cannot update without data! Call the use() method to hydrate the updater with data
|
||||||
to analyse.`);
|
to analyse.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getData(): AccountStatsMongoData {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AccountStats;
|
export default AccountStats;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export type ActiveRecordId = string;
|
export type ActiveRecordId = string;
|
||||||
|
export type OrId<T extends ActiveRecord> = T | ActiveRecordId;
|
||||||
|
|
||||||
interface ActiveRecord {
|
interface ActiveRecord {
|
||||||
getId(): ActiveRecordId;
|
getId(): ActiveRecordId;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import {CellDef, DEFAULT_RULESET, DEFAULT_RULESET_NAME, Ruleset} from "../rulesets";
|
import {CellDef, DEFAULT_RULESET, DEFAULT_RULESET_NAME, RulesetSchema} from "../rulesets";
|
||||||
import {FieldType} from "../enums";
|
import {FieldType} from "../enums";
|
||||||
|
|
||||||
|
|
||||||
export type OutcomeType = "win" | "loss" | "runnerUp" | "draw";
|
export enum OutcomeType {
|
||||||
export interface PlayerStatsMongoData extends BaseStatsMongoData {}
|
win,
|
||||||
export interface AccountStatsMongoData extends BaseStatsMongoData {
|
loss,
|
||||||
timesNoWinner: number;
|
runnerUp,
|
||||||
|
draw,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlayerStatsMongoData extends BaseStatsMongoData {}
|
||||||
|
export interface AccountStatsMongoData extends BaseStatsMongoData {}
|
||||||
export interface BaseStatsMongoData {
|
export interface BaseStatsMongoData {
|
||||||
statsByRuleset: Record<string, RulesetStatsMongoData>
|
statsByRuleset: Record<string, RulesetStatsMongoData>
|
||||||
gamesPlayed: number;
|
gamesPlayed: number;
|
||||||
@@ -130,7 +134,7 @@ function defaultBlockStatsMongoData(cellSchemas: Record<string, CellDef>, hasBon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultRulesetStatsMongoData(ruleset: Ruleset): RulesetStatsMongoData {
|
function defaultRulesetStatsMongoData(ruleset: RulesetSchema): RulesetStatsMongoData {
|
||||||
const blockStatsRecord: Record<string, BlockStatsMongoData> = {};
|
const blockStatsRecord: Record<string, BlockStatsMongoData> = {};
|
||||||
for (const blockLabel in ruleset.blocks) {
|
for (const blockLabel in ruleset.blocks) {
|
||||||
blockStatsRecord[blockLabel] = defaultBlockStatsMongoData(ruleset.blocks[blockLabel].cells, ruleset.blocks[blockLabel].hasBonus);
|
blockStatsRecord[blockLabel] = defaultBlockStatsMongoData(ruleset.blocks[blockLabel].cells, ruleset.blocks[blockLabel].hasBonus);
|
||||||
@@ -155,7 +159,7 @@ function defaultBaseStatsMongoData(): BaseStatsMongoData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function defaultAccountStatsMongoData(): AccountStatsMongoData {
|
export function defaultAccountStatsMongoData(): AccountStatsMongoData {
|
||||||
return {...defaultBaseStatsMongoData(), timesNoWinner: 0};
|
return defaultBaseStatsMongoData();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defaultPlayerStatsMongoData(): PlayerStatsMongoData {
|
export function defaultPlayerStatsMongoData(): PlayerStatsMongoData {
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import {CellValue} from "../controllers/statsController";
|
import {CellValue} from "../controllers/statsController";
|
||||||
import {Ruleset} from "../rulesets";
|
import {RulesetSchema} from "../rulesets";
|
||||||
import {ActiveRecordId} from "./ActiveRecord";
|
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
|
||||||
import {UpdateError} from "../errors";
|
import {UpdateError} from "../errors";
|
||||||
import {OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
|
import {OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
|
||||||
import PlayerStats from "./PlayerStats";
|
import PlayerStats from "./PlayerStats";
|
||||||
|
import Ruleset from "./Ruleset";
|
||||||
|
|
||||||
export interface CellDetails {
|
export interface CellDetails {
|
||||||
id: string;
|
id: string;
|
||||||
value: CellValue;
|
value: CellValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Player {
|
export class Player implements ActiveRecord {
|
||||||
constructor(
|
constructor(
|
||||||
private id: ActiveRecordId,
|
private id: ActiveRecordId,
|
||||||
private nick: string,
|
private nick: string,
|
||||||
@@ -25,11 +26,15 @@ export class Player {
|
|||||||
return this.nick;
|
return this.nick;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setNick(newNick: string): Promise<void> {
|
setNick(newNick: string): void {
|
||||||
this.nick = newNick;
|
this.nick = newNick;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset) {
|
getStats(): PlayerStats | undefined {
|
||||||
|
return this.stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
|
||||||
if (this.stats) {
|
if (this.stats) {
|
||||||
this.stats.updateStats(playerGameResults, ruleset);
|
this.stats.updateStats(playerGameResults, ruleset);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import {Ruleset} from "../rulesets";
|
import {RulesetSchema} from "../rulesets";
|
||||||
import {OutcomeType, PlayerGameResults, PlayerStatsMongoData} from "./DefaultStatsMongoData";
|
import {OutcomeType, PlayerGameResults, PlayerStatsMongoData} from "./DefaultStatsMongoData";
|
||||||
import StatsUpdater from "./StatsUpdater";
|
import StatsUpdater from "./StatsUpdater";
|
||||||
|
import Ruleset from "./Ruleset";
|
||||||
|
|
||||||
class PlayerStats {
|
class PlayerStats {
|
||||||
private data?: PlayerStatsMongoData;
|
private data: PlayerStatsMongoData;
|
||||||
private readonly updater: StatsUpdater;
|
private readonly updater: StatsUpdater;
|
||||||
constructor(data?: PlayerStatsMongoData) {
|
constructor(data: PlayerStatsMongoData) {
|
||||||
if (data) {
|
this.data = data;
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
this.updater = new StatsUpdater();
|
this.updater = new StatsUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,6 +19,10 @@ class PlayerStats {
|
|||||||
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
|
updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): void {
|
||||||
this.updater.updateStats(playerGameResults, ruleset);
|
this.updater.updateStats(playerGameResults, ruleset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getData(): PlayerStatsMongoData {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PlayerStats;
|
export default PlayerStats;
|
||||||
31
src/Objects/Ruleset.ts
Normal file
31
src/Objects/Ruleset.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {BlockDef, CellDef, RulesetSchema} from "../rulesets";
|
||||||
|
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
|
||||||
|
|
||||||
|
export class Ruleset implements ActiveRecord {
|
||||||
|
constructor(
|
||||||
|
private id: ActiveRecordId,
|
||||||
|
private schema: RulesetSchema,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getId(): ActiveRecordId {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.schema.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
rename(newName: string) {
|
||||||
|
this.schema.label = newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlocks(): Record<string, BlockDef> {
|
||||||
|
return Object.assign({}, this.schema.blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCellsInBlock(blockId: string): Record<string, CellDef> {
|
||||||
|
return Object.assign({}, this.schema.blocks[blockId].cells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Ruleset;
|
||||||
29
src/Objects/SavedGame.ts
Executable file
29
src/Objects/SavedGame.ts
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
|
||||||
|
import {PlayerGameResults} from "./DefaultStatsMongoData";
|
||||||
|
|
||||||
|
class SavedGame implements ActiveRecord {
|
||||||
|
constructor(
|
||||||
|
private id: string,
|
||||||
|
private rulesetUsed: {name: string, id: ActiveRecordId},
|
||||||
|
private players: {name: string, id: ActiveRecordId}[],
|
||||||
|
private results: PlayerGameResults[],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayers() {
|
||||||
|
return this.players;
|
||||||
|
}
|
||||||
|
|
||||||
|
getResults() {
|
||||||
|
return this.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRulesetUsed() {
|
||||||
|
return this.rulesetUsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SavedGame;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import {BlockDef, Ruleset} from "../rulesets";
|
import {BlockDef, RulesetSchema} from "../rulesets";
|
||||||
import ScoreBlockCalculator, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlockCalculator";
|
import ScoreBlockCalculator, {createBlockFromDef, ScoreBlockJSONRepresentation} from "./ScoreBlockCalculator";
|
||||||
|
import Ruleset from "./Ruleset";
|
||||||
|
|
||||||
export type CellLocation = { blockId: string, cellId: string };
|
export type CellLocation = { blockId: string, cellId: string };
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ class ScoreCalculator {
|
|||||||
private readonly blocks: ScoreBlockCalculator[];
|
private readonly blocks: ScoreBlockCalculator[];
|
||||||
|
|
||||||
constructor(gameSchema: Ruleset) {
|
constructor(gameSchema: Ruleset) {
|
||||||
this.blocks = ScoreCalculator.generateBlocks(gameSchema.blocks);
|
this.blocks = ScoreCalculator.generateBlocks(gameSchema.getBlocks());
|
||||||
}
|
}
|
||||||
|
|
||||||
hydrateWithJSON(jsonRep: ScoreCardJSONRepresentation): void {
|
hydrateWithJSON(jsonRep: ScoreCardJSONRepresentation): void {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Ruleset} from "../rulesets";
|
import {RulesetSchema} from "../rulesets";
|
||||||
import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator";
|
import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator";
|
||||||
import {UpdateError} from "../errors";
|
import {UpdateError} from "../errors";
|
||||||
import {FieldType} from "../enums";
|
import {FieldType} from "../enums";
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
PlayerGameResults, RulesetStatsMongoData,
|
PlayerGameResults, RulesetStatsMongoData,
|
||||||
TotalFieldStatsMongoData
|
TotalFieldStatsMongoData
|
||||||
} from "./DefaultStatsMongoData";
|
} from "./DefaultStatsMongoData";
|
||||||
|
import Ruleset from "./Ruleset";
|
||||||
|
|
||||||
class StatsUpdater {
|
class StatsUpdater {
|
||||||
private data?: BaseStatsMongoData;
|
private data?: BaseStatsMongoData;
|
||||||
@@ -29,8 +30,8 @@ class StatsUpdater {
|
|||||||
this.validationRuleset = ruleset;
|
this.validationRuleset = ruleset;
|
||||||
this.calculator = new ScoreCalculator(ruleset);
|
this.calculator = new ScoreCalculator(ruleset);
|
||||||
this.calculator.hydrateWithJSON(playerGameResults as ScoreCardJSONRepresentation);
|
this.calculator.hydrateWithJSON(playerGameResults as ScoreCardJSONRepresentation);
|
||||||
this.currentStatsObject = this.data.statsByRuleset[ruleset.id];
|
this.currentStatsObject = this.data.statsByRuleset[ruleset.getId()];
|
||||||
for (const blockId in ruleset.blocks) {
|
for (const blockId in ruleset.getBlocks()) {
|
||||||
this.updateBlockStats(blockId);
|
this.updateBlockStats(blockId);
|
||||||
}
|
}
|
||||||
this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal());
|
this.updateTotalFieldStats(this.currentStatsObject.grandTotal, this.calculator.getTotal());
|
||||||
@@ -58,15 +59,15 @@ class StatsUpdater {
|
|||||||
if (this.calculator!.blockWithIdHasBonus(blockId)) {
|
if (this.calculator!.blockWithIdHasBonus(blockId)) {
|
||||||
blockStats.timesHadBonus! += 1;
|
blockStats.timesHadBonus! += 1;
|
||||||
}
|
}
|
||||||
for (const cellId in this.validationRuleset!.blocks[blockId].cells) {
|
for (const cellId in this.validationRuleset!.getBlocks()[blockId].cells) {
|
||||||
this.updateCellStatsByIds({cellId, blockId});
|
this.updateCellStatsByIds({cellId, blockId});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateCellStatsByIds(ids: {cellId: string, blockId: string}) {
|
private updateCellStatsByIds(ids: {cellId: string, blockId: string}) {
|
||||||
const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.id});
|
const cellStats = this.getCellStatsByIds({...ids, rulesetId: this.validationRuleset!.getId()});
|
||||||
const cellFieldType = this.validationRuleset?.blocks[ids.blockId].cells[ids.cellId].fieldType;
|
const cellFieldType = this.validationRuleset?.getBlocks()[ids.blockId].cells[ids.cellId].fieldType;
|
||||||
const cellScore = this.calculator!.getCellScoreByLocation({...ids});
|
const cellScore = this.calculator!.getCellScoreByLocation({...ids});
|
||||||
if (cellScore > 0 && cellFieldType === FieldType.bool) {
|
if (cellScore > 0 && cellFieldType === FieldType.bool) {
|
||||||
(cellStats as BoolFieldStatsMongoData).total += 1;
|
(cellStats as BoolFieldStatsMongoData).total += 1;
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import {GameSubmission} from "../controllers/statsController";
|
|
||||||
import {StoredObjectCollection, StoredObject, StoredObjectId} from "./utils";
|
|
||||||
import {PlayerGameResults} from "./Stats";
|
|
||||||
|
|
||||||
export interface SavedGameData {
|
|
||||||
id: string;
|
|
||||||
rulesetUsed: RulesetData;
|
|
||||||
players: StoredObjectId[];
|
|
||||||
results: PlayerGameResults[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoredSavedGame extends StoredObject {
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoredSavedGameCollection extends StoredObjectCollection<StoredSavedGame> {
|
|
||||||
createFromGameSubmission(submission: GameSubmission): Promise<StoredSavedGame>;
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ import {RequestHandler} from "express";
|
|||||||
import KadiUser from "../Objects/KadiUser";
|
import KadiUser from "../Objects/KadiUser";
|
||||||
import Player from "../Objects/Player";
|
import Player from "../Objects/Player";
|
||||||
import KadiUserCollection from "../ObjectCollections/KadiUserCollection";
|
import KadiUserCollection from "../ObjectCollections/KadiUserCollection";
|
||||||
|
import PlayerCollection from "../ObjectCollections/PlayerCollection";
|
||||||
|
|
||||||
export const currentUserDetails: RequestHandler = async (req, res) => {
|
export const currentUserDetails: RequestHandler = async (req, res) => {
|
||||||
if (req.isAuthenticated()) {
|
if (req.isAuthenticated()) {
|
||||||
@@ -26,7 +27,7 @@ export const changeLang: RequestHandler = async (req, res) => {
|
|||||||
export const addGuest: RequestHandler = async (req, res) => {
|
export const addGuest: RequestHandler = async (req, res) => {
|
||||||
const user = (req.user as KadiUser);
|
const user = (req.user as KadiUser);
|
||||||
if (req.body.guestName) {
|
if (req.body.guestName) {
|
||||||
const newGuest: Player = await KadiUserCollection.addGuestForAccount(req.body.guestName);
|
const newGuest: Player = await KadiUserCollection.addGuestForUser(user, req.body.guestName);
|
||||||
res.send({
|
res.send({
|
||||||
username: user.getUsername(),
|
username: user.getUsername(),
|
||||||
userId: user.getId(),
|
userId: user.getId(),
|
||||||
@@ -46,7 +47,9 @@ export const updateGuest: RequestHandler = async (req, res) => {
|
|||||||
const {id: guestId} = req.params;
|
const {id: guestId} = req.params;
|
||||||
if (req.body.newName) {
|
if (req.body.newName) {
|
||||||
const {newName} = req.body;
|
const {newName} = req.body;
|
||||||
const updatedGuest = await KadiUserCollection.updateGuestForAccount({id: guestId, newNick: newName});
|
const guest = await PlayerCollection.read(guestId);
|
||||||
|
guest.setNick(newName);
|
||||||
|
const updatedGuest = await PlayerCollection.save(guest);
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
userId: user.getId(),
|
userId: user.getId(),
|
||||||
username: user.getUsername(),
|
username: user.getUsername(),
|
||||||
@@ -64,7 +67,7 @@ export const updateGuest: RequestHandler = async (req, res) => {
|
|||||||
export const getGuest: RequestHandler = async (req, res) => {
|
export const getGuest: RequestHandler = async (req, res) => {
|
||||||
const user = (req.user as KadiUser);
|
const user = (req.user as KadiUser);
|
||||||
const {id: guestId} = req.params;
|
const {id: guestId} = req.params;
|
||||||
const guest = await KadiUserCollection.getGuestForAccount(guestId, user.getId());
|
const guest = await PlayerCollection.read(guestId);
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
userId: user.getId(),
|
userId: user.getId(),
|
||||||
username: user.getUsername(),
|
username: user.getUsername(),
|
||||||
@@ -75,7 +78,7 @@ export const getGuest: RequestHandler = async (req, res) => {
|
|||||||
export const deleteGuest: RequestHandler = async (req, res) => {
|
export const deleteGuest: RequestHandler = async (req, res) => {
|
||||||
const user = (req.user as KadiUser);
|
const user = (req.user as KadiUser);
|
||||||
const {id: guestId} = req.params;
|
const {id: guestId} = req.params;
|
||||||
const deletedGuest = await KadiUserCollection.deleteGuestForAccount(guestId, user.getId());
|
const deletedGuest = await KadiUserCollection.deleteGuestFromUser(user, guestId);
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
userId: user.getId(),
|
userId: user.getId(),
|
||||||
username: user.getUsername(),
|
username: user.getUsername(),
|
||||||
@@ -85,7 +88,7 @@ export const deleteGuest: RequestHandler = async (req, res) => {
|
|||||||
|
|
||||||
export const getGuests: RequestHandler = async (req, res) => {
|
export const getGuests: RequestHandler = async (req, res) => {
|
||||||
const user = (req.user as KadiUser);
|
const user = (req.user as KadiUser);
|
||||||
const guests = await KadiUserCollection.getGuestsForAccount(user.getId());
|
const guests = await KadiUserCollection.getAllGuestsForUser(user);
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
userId: user.getId(),
|
userId: user.getId(),
|
||||||
username: user.getUsername(),
|
username: user.getUsername(),
|
||||||
@@ -95,7 +98,7 @@ export const getGuests: RequestHandler = async (req, res) => {
|
|||||||
|
|
||||||
export const getAllPlayersAssociatedWithAccount: RequestHandler = async (req, res) => {
|
export const getAllPlayersAssociatedWithAccount: RequestHandler = async (req, res) => {
|
||||||
const user = (req.user as KadiUser);
|
const user = (req.user as KadiUser);
|
||||||
const guests = await KadiUserCollection.getAllGuestsForAccount(user.getId());
|
const guests = await KadiUserCollection.getAllGuestsForUser(user);
|
||||||
const mainPlayer = await KadiUserCollection.getMainPlayerForAccount(user.getId());
|
const mainPlayer = await KadiUserCollection.getMainPlayerForUser(user);
|
||||||
res.status(200).send({guests, mainPlayer});
|
res.status(200).send({guests, mainPlayer});
|
||||||
};
|
};
|
||||||
@@ -1,50 +1,24 @@
|
|||||||
import DbUser, { IDbUser } from "../models/dbUser_old";
|
import KadiUserCollection from "../ObjectCollections/KadiUserCollection";
|
||||||
import { RequestHandler } from "express";
|
import PlayerCollection from "../ObjectCollections/PlayerCollection";
|
||||||
import Player, { IPlayer } from "../models/StoredPlayer";
|
import Player from "../Objects/Player";
|
||||||
|
import {RequestHandler} from "express";
|
||||||
const DEFAULT_RULESET = "DEFAULT_RULESET";
|
import ScoreCalculator from "../Objects/ScoreCalculator";
|
||||||
const UPPER_BONUS_THRESHOLD = 63;
|
import KadiUser from "../Objects/KadiUser";
|
||||||
const UPPER_BONUS = 35;
|
import {CellFlag} from "../enums";
|
||||||
const FULL_HOUSE_SCORE = 25;
|
import RulesetCollection from "../ObjectCollections/RulesetCollection";
|
||||||
const SML_STRAIGHT_SCORE = 30;
|
import Ruleset from "../Objects/Ruleset";
|
||||||
const LG_STRAIGHT_SCORE = 40;
|
import {OutcomeType} from "../Objects/DefaultStatsMongoData";
|
||||||
const YAHTZEE_SCORE = 50;
|
|
||||||
|
|
||||||
export interface GameSubmission {
|
export interface GameSubmission {
|
||||||
ruleset: string;
|
rulesetId: string;
|
||||||
players: { id: string; nick: string }[];
|
players: { id: string; nick: string }[];
|
||||||
results: PlayerGameResult[];
|
results: PlayerGameResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScoredResult extends ScoreTotalFields, PlayerGameResult {}
|
type PlayerGameResult = { playerId: string; blocks: Record<string, Block> };
|
||||||
|
type Block = { cells: Record<string, Cell> };
|
||||||
interface ScoreTotalFields {
|
type Cell = { value: CellValue };
|
||||||
topBonus: boolean;
|
export type CellValue = number | boolean | CellFlag.strike;
|
||||||
topSubtotal: number;
|
|
||||||
top: number;
|
|
||||||
bottom: number;
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlayerGameResult = { playerId: string; blocks: Record<BlockName, Block> };
|
|
||||||
type BlockName = "top" | "bottom";
|
|
||||||
type Block = { cells: Record<CellName, StandardCell> };
|
|
||||||
type CellName =
|
|
||||||
| "aces"
|
|
||||||
| "twos"
|
|
||||||
| "threes"
|
|
||||||
| "fours"
|
|
||||||
| "fives"
|
|
||||||
| "sixes"
|
|
||||||
| "three_kind"
|
|
||||||
| "four_kind"
|
|
||||||
| "full_house"
|
|
||||||
| "sml_straight"
|
|
||||||
| "lg_straight"
|
|
||||||
| "yahtzee"
|
|
||||||
| "chance";
|
|
||||||
type StandardCell = { value: CellValue };
|
|
||||||
export type CellValue = number | boolean | "cellFlagStrike";
|
|
||||||
enum ResultType {
|
enum ResultType {
|
||||||
winner,
|
winner,
|
||||||
drawn,
|
drawn,
|
||||||
@@ -52,15 +26,14 @@ enum ResultType {
|
|||||||
loser,
|
loser,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScoredResults = {score: number, results: PlayerGameResult};
|
||||||
|
type ScoredResultsWithOutcome = {score: number, results: PlayerGameResult & {outcome: OutcomeType}};
|
||||||
|
|
||||||
export const listGames: RequestHandler = async (req, res) => {
|
export const listGames: RequestHandler = async (req, res) => {
|
||||||
const user = req.user as IDbUser;
|
const user = req.user as KadiUser;
|
||||||
const dbUser = await DbUser.findById(user.id, {
|
const gamesList = await KadiUserCollection.getSavedGamesForUser(user);
|
||||||
"savedGames._id": 1,
|
if (gamesList) {
|
||||||
"savedGames.results": 1,
|
res.json({ games: gamesList });
|
||||||
"savedGames.createdAt": 1,
|
|
||||||
});
|
|
||||||
if (dbUser) {
|
|
||||||
res.json({ games: dbUser.savedGames });
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res.sendStatus(404);
|
res.sendStatus(404);
|
||||||
@@ -68,172 +41,118 @@ export const listGames: RequestHandler = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const saveGame: RequestHandler = async (req, res) => {
|
export const saveGame: RequestHandler = async (req, res) => {
|
||||||
const user = req.user as IDbUser;
|
const user = req.user as KadiUser;
|
||||||
const submission = req.body as GameSubmission;
|
const submission = req.body as GameSubmission;
|
||||||
const newGuests: IPlayer[] = await addNewGuests(submission, user);
|
const newGuests: Player[] = await addNewGuests(submission, user);
|
||||||
if (newGuests.length > 0) {
|
if (newGuests.length > 0) {
|
||||||
fillOutSubmissionWithNewIds(submission, newGuests);
|
fillOutSubmissionWithNewIds(submission, newGuests);
|
||||||
}
|
}
|
||||||
const newGame = await user.addGame(submission);
|
const newGame = await KadiUserCollection.addGameForUser(user, submission);
|
||||||
if (submission.ruleset === DEFAULT_RULESET) {
|
processStats(await RulesetCollection.read(submission.rulesetId), submission.results, user);
|
||||||
processStandardStatistics(submission.results, user);
|
|
||||||
}
|
|
||||||
res.send({ message: "Game submitted successfully!", newGame: newGame });
|
res.send({ message: "Game submitted successfully!", newGame: newGame });
|
||||||
};
|
};
|
||||||
|
|
||||||
async function addNewGuests(submission: GameSubmission, user: IDbUser): Promise<IPlayer[]> {
|
async function addNewGuests(submission: GameSubmission, user: KadiUser): Promise<Player[]> {
|
||||||
const newGuestIds: IPlayer[] = [];
|
const newGuestIds: Player[] = [];
|
||||||
for (const playerDetails of submission.players) {
|
for (const playerDetails of submission.players) {
|
||||||
const isNewPlayer = playerDetails.id === playerDetails.nick;
|
const isNewPlayer = playerDetails.id === playerDetails.nick;
|
||||||
if (isNewPlayer) {
|
if (isNewPlayer) {
|
||||||
const newGuest: IPlayer = await user.addGuest(playerDetails.nick);
|
const newGuest: Player = await KadiUserCollection.addGuestForUser(user, playerDetails.nick);
|
||||||
newGuestIds.push(newGuest);
|
newGuestIds.push(newGuest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newGuestIds;
|
return newGuestIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: IPlayer[]): GameSubmission {
|
function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: Player[]): GameSubmission {
|
||||||
for (const newGuest of newGuestList) {
|
for (const newGuest of newGuestList) {
|
||||||
const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.nick);
|
const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.getNick());
|
||||||
if (gameResultsFromNewGuest) {
|
if (gameResultsFromNewGuest) {
|
||||||
gameResultsFromNewGuest.playerId = newGuest.id;
|
gameResultsFromNewGuest.playerId = newGuest.getId();
|
||||||
}
|
}
|
||||||
const playerEntryForNewGuest = submission.players.find((player) => player.id === newGuest.nick);
|
const playerEntryForNewGuest = submission.players.find((player) => player.id === newGuest.getNick());
|
||||||
if (playerEntryForNewGuest) {
|
if (playerEntryForNewGuest) {
|
||||||
playerEntryForNewGuest.id = newGuest.id;
|
playerEntryForNewGuest.id = newGuest.getId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return submission;
|
return submission;
|
||||||
}
|
}
|
||||||
|
|
||||||
function processStandardStatistics(results: PlayerGameResult[], account: IDbUser) {
|
async function processStats(ruleset: Ruleset, results: PlayerGameResult[], account: KadiUser) {
|
||||||
let scoredResults: ScoredResult[] = [];
|
const calc = new ScoreCalculator(ruleset);
|
||||||
|
let playerScoreList: ScoredResults[] = [];
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const scoredResult = {
|
calc.hydrateWithJSON(result);
|
||||||
...result,
|
playerScoreList.push({
|
||||||
...getStandardScoreFields(result),
|
score: calc.getTotal(),
|
||||||
};
|
results: result
|
||||||
scoredResults.push(scoredResult);
|
});
|
||||||
updatePlayerStats(result.playerId, scoredResult);
|
|
||||||
}
|
}
|
||||||
const { wasDraw } = incrementPlayerPlacings(scoredResults);
|
const playerScoreListWithOutcomes = updateScoreListWithOutcomes(playerScoreList, ruleset);
|
||||||
if (wasDraw) {
|
updateStatsForIndividualPlayers(playerScoreListWithOutcomes, ruleset);
|
||||||
DbUser.incrementTimesNoWinner(account.id);
|
const gameResults = playerScoreListWithOutcomes.map(scoredResults => scoredResults.results);
|
||||||
}
|
await KadiUserCollection.updateAccountStats(account.getId(), gameResults, ruleset);
|
||||||
DbUser.incrementGamesPlayed(account.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updatePlayerStats(playerId: string, result: ScoredResult) {
|
function updateScoreListWithOutcomes(playerScoreList: ScoredResults[], rulesetUsed: Ruleset): ScoredResultsWithOutcome[] {
|
||||||
const player: IPlayer = await Player.findById(playerId) as IPlayer;
|
playerScoreList = sortDescendingByScore(playerScoreList);
|
||||||
for (const blockId in result.blocks) {
|
const playerScoreListWithOutcomes: ScoredResultsWithOutcome[] = playerScoreList.map(scoredResults => {
|
||||||
const cells = result.blocks[blockId as BlockName].cells;
|
const newResults = {...scoredResults.results, outcome: OutcomeType.loss};
|
||||||
for (const cellId in cells) {
|
return {...scoredResults, results: newResults};
|
||||||
Player.updateCellStats(player.id, {id: cellId, value: cells[cellId as CellName].value});
|
});
|
||||||
}
|
|
||||||
if (result.topBonus) {
|
|
||||||
Player.incrementBonus(player.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Player.incrementGamesPlayed(player.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementPlayerPlacings(scoredResults: ScoredResult[]) {
|
|
||||||
scoredResults = sortDescendingByScore(scoredResults);
|
|
||||||
const placingFacts: { wasDraw: boolean } = { wasDraw: false };
|
|
||||||
let runnerUpsStart: number;
|
let runnerUpsStart: number;
|
||||||
if (scoredResults[0].total !== scoredResults[1].total) {
|
if (playerScoreListWithOutcomes[0].score !== playerScoreListWithOutcomes[1].score) {
|
||||||
Player.incrementWinFor(scoredResults[0].playerId);
|
playerScoreListWithOutcomes[0].results.outcome = OutcomeType.win;
|
||||||
|
|
||||||
runnerUpsStart = 1;
|
runnerUpsStart = 1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
runnerUpsStart = icrmtPlayerDrawsTilScoreChange(scoredResults);
|
runnerUpsStart = updateScoreListWithDraws(playerScoreListWithOutcomes, rulesetUsed);
|
||||||
placingFacts.wasDraw = true;
|
|
||||||
}
|
}
|
||||||
const losersStart = icrmtPlayerRunnerUpsTilScoreChange(
|
const losersStart = updateScoreListWithRunnerUps(playerScoreListWithOutcomes.slice(runnerUpsStart), rulesetUsed);
|
||||||
scoredResults.slice(runnerUpsStart)
|
updateScoreListWithLosses(playerScoreListWithOutcomes.slice(losersStart), rulesetUsed);
|
||||||
);
|
return playerScoreListWithOutcomes;
|
||||||
icrmtPlayerLosses(scoredResults.slice(losersStart));
|
|
||||||
return placingFacts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function icrmtPlayerDrawsTilScoreChange(scoredResults: ScoredResult[]): number {
|
function updateScoreListWithDraws(playerScoreList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): number {
|
||||||
for (let i = 0; i < scoredResults.length; i++) {
|
for (let i = 0; i < playerScoreList.length; i++) {
|
||||||
if (scoredResults[i].total === scoredResults[0].total) {
|
if (playerScoreList[i].score === playerScoreList[0].score) {
|
||||||
Player.incrementDrawFor(scoredResults[i].playerId);
|
playerScoreList[i].results.outcome = OutcomeType.draw;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return scoredResults.length;
|
return playerScoreList.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function icrmtPlayerRunnerUpsTilScoreChange(scoredResults: ScoredResult[]): number {
|
function updateScoreListWithRunnerUps(playerScoreList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): number {
|
||||||
for (let i = 0; i < scoredResults.length; i++) {
|
for (let i = 0; i < playerScoreList.length; i++) {
|
||||||
if (scoredResults[i].total === scoredResults[0].total) {
|
if (playerScoreList[i].score === playerScoreList[0].score) {
|
||||||
Player.incrementRunnerUpFor(scoredResults[i].playerId);
|
playerScoreList[i].results.outcome = OutcomeType.runnerUp;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return scoredResults.length;
|
return playerScoreList.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function icrmtPlayerLosses(scoredResults: ScoredResult[]): void {
|
function updateScoreListWithLosses(scoreResultsList: ScoredResultsWithOutcome[], rulesetUsed: Ruleset) {
|
||||||
for (const scoredResult of scoredResults) {
|
for (const lostPlayerResults of scoreResultsList) {
|
||||||
Player.incrementLossFor(scoredResult.playerId);
|
lostPlayerResults.results.outcome = OutcomeType.loss;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortDescendingByScore(scoredResults: ScoredResult[]) {
|
function sortDescendingByScore(playerScoreList: ScoredResults[]) {
|
||||||
return scoredResults.sort((a, b) => b.total - a.total);
|
return playerScoreList.sort((a, b) => b.score - a.score);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateStatsForIndividualPlayers(playerScoreListWithOutcomes: ScoredResultsWithOutcome[], rulesetUsed: Ruleset): void {
|
||||||
function getStandardScoreFields(result: PlayerGameResult): ScoreTotalFields {
|
for (const scoredResults of playerScoreListWithOutcomes) {
|
||||||
const scoreFields: ScoreTotalFields = { topBonus: false, topSubtotal: 0, top: 0, bottom: 0, total: 0 };
|
PlayerCollection.updateStatsForPlayer(
|
||||||
scoreFields.topSubtotal = topSubtotal(result.blocks.top.cells);
|
scoredResults.results.playerId,
|
||||||
scoreFields.top = scoreFields.topSubtotal;
|
{...scoredResults.results, outcome: scoredResults.results.outcome},
|
||||||
if (scoreFields.topSubtotal >= UPPER_BONUS_THRESHOLD) {
|
rulesetUsed);
|
||||||
scoreFields.topBonus = true;
|
|
||||||
scoreFields.top += UPPER_BONUS;
|
|
||||||
}
|
}
|
||||||
scoreFields.bottom = bottomTotal(result.blocks.bottom.cells);
|
}
|
||||||
scoreFields.total = scoreFields.top + scoreFields.bottom;
|
|
||||||
return scoreFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
function topSubtotal(topResult: Record<CellName, StandardCell>) {
|
|
||||||
return (
|
|
||||||
cellScore(topResult.aces) +
|
|
||||||
cellScore(topResult.twos) * 2 +
|
|
||||||
cellScore(topResult.threes) * 3 +
|
|
||||||
cellScore(topResult.fours) * 4 +
|
|
||||||
cellScore(topResult.fives) * 5 +
|
|
||||||
cellScore(topResult.sixes) * 6
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function bottomTotal(bottomResult: Record<CellName, StandardCell>) {
|
|
||||||
return (
|
|
||||||
cellScore(bottomResult.three_kind) +
|
|
||||||
cellScore(bottomResult.four_kind) +
|
|
||||||
cellScore(bottomResult.full_house) * FULL_HOUSE_SCORE +
|
|
||||||
cellScore(bottomResult.sml_straight) * SML_STRAIGHT_SCORE +
|
|
||||||
cellScore(bottomResult.lg_straight) * LG_STRAIGHT_SCORE +
|
|
||||||
cellScore(bottomResult.yahtzee) * YAHTZEE_SCORE +
|
|
||||||
cellScore(bottomResult.chance)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cellScore(cell: StandardCell) {
|
|
||||||
if (cell.value === "cellFlagStrike" || cell.value === false) {
|
|
||||||
return 0;
|
|
||||||
} else if (cell.value === true) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return cell.value;
|
|
||||||
}
|
|
||||||
@@ -37,4 +37,11 @@ export class CredentialsTakenError extends KadiError {
|
|||||||
this.emailExists = emailExists;
|
this.emailExists = emailExists;
|
||||||
this.name = "CredentialsTakenError";
|
this.name = "CredentialsTakenError";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidIdError extends GenericPersistenceError {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "InvalidIdError";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import {requireAuthenticated} from "../passport-config";
|
|
||||||
import * as statsController from "../controllers/statsController";
|
import * as statsController from "../controllers/statsController";
|
||||||
import * as dbUserController from "../controllers/kadiUserController"
|
import * as KadiUserController from "../controllers/kadiUserController"
|
||||||
|
import {requireAuthenticated} from "./routerMiddleware";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Basic User Settings
|
// Basic User Settings
|
||||||
router.get("/user", dbUserController.currentUserDetails);
|
router.get("/user", KadiUserController.currentUserDetails);
|
||||||
router.put("/lang", requireAuthenticated, dbUserController.changeLang);
|
router.put("/lang", requireAuthenticated, KadiUserController.changeLang);
|
||||||
|
|
||||||
// Guests
|
// Guests
|
||||||
router.get("/players", requireAuthenticated, dbUserController.getAllPlayersAssociatedWithAccount);
|
router.get("/players", requireAuthenticated, KadiUserController.getAllPlayersAssociatedWithAccount);
|
||||||
router.get("/guests", requireAuthenticated, dbUserController.getGuests);
|
router.get("/guests", requireAuthenticated, KadiUserController.getGuests);
|
||||||
router.get("/guest/:id", requireAuthenticated, dbUserController.getGuest);
|
router.get("/guest/:id", requireAuthenticated, KadiUserController.getGuest);
|
||||||
router.put("/guest/:id", requireAuthenticated, dbUserController.updateGuest);
|
router.put("/guest/:id", requireAuthenticated, KadiUserController.updateGuest);
|
||||||
router.post("/guests", requireAuthenticated, dbUserController.addGuest);
|
router.post("/guests", requireAuthenticated, KadiUserController.addGuest);
|
||||||
router.delete("/guest/:id", requireAuthenticated, dbUserController.deleteGuest);
|
router.delete("/guest/:id", requireAuthenticated, KadiUserController.deleteGuest);
|
||||||
|
|
||||||
// Games
|
// Games
|
||||||
router.get("/games", requireAuthenticated, statsController.listGames);
|
router.get("/games", requireAuthenticated, statsController.listGames);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {FieldType} from "./enums";
|
import {FieldType} from "./enums";
|
||||||
import RulesetsPage from "../../frontend/src/Components/RulesetsPage";
|
|
||||||
import {RulesetStats} from "./models/Stats";
|
|
||||||
|
|
||||||
export const defaultCellValues = {
|
export const defaultCellValues = {
|
||||||
[FieldType.number]: 0,
|
[FieldType.number]: 0,
|
||||||
@@ -12,7 +10,7 @@ export const defaultCellValues = {
|
|||||||
[FieldType.multiplier]: 0,
|
[FieldType.multiplier]: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Ruleset {
|
export interface RulesetSchema {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
blocks: Record<string, BlockDef>;
|
blocks: Record<string, BlockDef>;
|
||||||
@@ -71,7 +69,7 @@ interface DefaultCellDef {
|
|||||||
export const DEFAULT_RULESET_NAME = "DEFAULT_RULESET";
|
export const DEFAULT_RULESET_NAME = "DEFAULT_RULESET";
|
||||||
const defaultDiceCount = 5;
|
const defaultDiceCount = 5;
|
||||||
|
|
||||||
export const DEFAULT_RULESET: Ruleset = {
|
export const DEFAULT_RULESET: RulesetSchema = {
|
||||||
id: DEFAULT_RULESET_NAME,
|
id: DEFAULT_RULESET_NAME,
|
||||||
label: "Standard Kadi Rules (en)",
|
label: "Standard Kadi Rules (en)",
|
||||||
blocks: {
|
blocks: {
|
||||||
|
|||||||
Reference in New Issue
Block a user