Finished implementing the stats updating, just need to implement the new model methods. Added a notes file.

This commit is contained in:
Daniel Ledda
2020-06-24 23:30:30 +02:00
parent df2c6c95bd
commit c13ac5f12b
6 changed files with 273 additions and 150 deletions

7
notes.txt Normal file
View File

@@ -0,0 +1,7 @@
- Make a whole new class for each model called "Player", "DbUser", etc. and wrap mongoose completely.
- Create a corresponding namespace "PlayerCollection", "DbUserCollection", etc. for the database model itself and its
corresponding methods.
- Decide on whether to always use the namespace to call methods and pass an id, with the returned objects "Player",
"DbUser", etc. all being immutable object copies with limited attributes and no methods, or actually create extra
classes that have methods that can be called (right now thinking the first idea is better).
-> This way will be completely decoupled from Mongoose and MongoDB and will have full control.

View File

@@ -40,5 +40,9 @@
"ts-loader": "^7.0.5", "ts-loader": "^7.0.5",
"tslint": "^6.1.1", "tslint": "^6.1.1",
"typescript": "^3.9.3" "typescript": "^3.9.3"
},
"prettier": {
"tabWidth": 4,
"jsxBracketSameLine": true
} }
} }

View File

@@ -3,165 +3,237 @@ import { RequestHandler } from "express";
import Player, { IPlayer } from "../models/player"; import Player, { IPlayer } from "../models/player";
const DEFAULT_RULESET = "DEFAULT_RULESET"; const DEFAULT_RULESET = "DEFAULT_RULESET";
const UPPER_BONUS_THRESHOLD = 63;
const UPPER_BONUS = 35;
const FULL_HOUSE_SCORE = 25;
const SML_STRAIGHT_SCORE = 30;
const LG_STRAIGHT_SCORE = 40;
const YAHTZEE_SCORE = 50;
export interface GameSubmission { export interface GameSubmission {
ruleset: string; ruleset: string;
players: { id: string; nick: string }[]; players: { id: string; nick: string }[];
results: GameResults[]; results: PlayerGameResult[];
} }
interface GameResults { interface ScoredResult extends ScoreTotalFields, PlayerGameResult {}
playerId: string;
blocks: Record<BlockName, Block>; interface ScoreTotalFields {
topBonus: boolean;
topSubtotal: number;
top: number;
bottom: number;
total: number;
} }
type PlayerGameResult = { playerId: string; blocks: Record<BlockName, Block> };
type BlockName = "top" | "bottom"; type BlockName = "top" | "bottom";
type Block = { cells: Record<CellName, StandardCell> };
interface Block {
cells: Record<CellName, StandardCell>;
}
type CellName = type CellName =
| "aces" | "aces"
| "twos" | "twos"
| "threes" | "threes"
| "fours" | "fours"
| "fives" | "fives"
| "sixes" | "sixes"
| "three_kind" | "three_kind"
| "four_kind" | "four_kind"
| "full_house" | "full_house"
| "sml_straight" | "sml_straight"
| "lg_straight" | "lg_straight"
| "yahtzee" | "yahtzee"
| "chance"; | "chance";
type StandardCell = { value: CellValue };
interface StandardCell { export type CellValue = number | boolean | "cellFlagStrike";
value: CellValue; enum ResultType {
winner,
drawn,
runnerUp,
loser,
} }
type CellValue = number | boolean | "cellFlagStrike";
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 IDbUser;
const dbUser = await DbUser.findById(user.id, { const dbUser = await DbUser.findById(user.id, {
"savedGames._id": 1, "savedGames._id": 1,
"savedGames.results": 1, "savedGames.results": 1,
"savedGames.createdAt": 1, "savedGames.createdAt": 1,
}); });
if (dbUser) { if (dbUser) {
res.json({ games: dbUser.savedGames }); res.json({ games: dbUser.savedGames });
} else { }
res.sendStatus(404); else {
} res.sendStatus(404);
}
}; };
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 IDbUser;
const submission = req.body as GameSubmission; const submission = req.body as GameSubmission;
await addNewGuestsFromSubmissionAndFillInIds(submission, user); const newGuests: IPlayer[] = await addNewGuests(submission, user);
const newGame = await user.addGame(submission); if (newGuests.length > 0) {
if (submission.ruleset === DEFAULT_RULESET) { fillOutSubmissionWithNewIds(submission, newGuests);
processStandardStatistics(submission.results, user); }
} const newGame = await user.addGame(submission);
console.log(JSON.stringify(req.body)); if (submission.ruleset === DEFAULT_RULESET) {
res.send({ message: "Game submitted successfully!", newGame: newGame }); processStandardStatistics(submission.results, user);
}
res.send({ message: "Game submitted successfully!", newGame: newGame });
}; };
async function addNewGuestsFromSubmissionAndFillInIds( async function addNewGuests(submission: GameSubmission, user: IDbUser): Promise<IPlayer[]> {
submission: GameSubmission, const newGuestIds: IPlayer[] = [];
user: IDbUser for (const playerDetails of submission.players) {
): Promise<void> { const isNewPlayer = playerDetails.id === playerDetails.nick;
for (const playerInParticipantList of submission.players) { if (isNewPlayer) {
if (playerInParticipantList.id === playerInParticipantList.nick) { const newGuest: IPlayer = await user.addGuest(playerDetails.nick);
const newGuest = await user.addGuest(playerInParticipantList.nick); newGuestIds.push(newGuest);
const gameResultsFromNewGuest = submission.results.find( }
(result) => result.playerId === playerInParticipantList.nick
);
gameResultsFromNewGuest!.playerId = newGuest.id;
playerInParticipantList.id = newGuest.id;
} }
} return newGuestIds;
} }
function processStandardStatistics(results: GameResults[], account: IDbUser) { function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: IPlayer[]): GameSubmission {
let runnerUp: IPlayer; for (const newGuest of newGuestList) {
const drawnPlayers: IPlayer[] = []; const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.nick);
const scoredResults: { pid: string; score: number }[] = []; if (gameResultsFromNewGuest) {
for (const result of results) { gameResultsFromNewGuest.playerId = newGuest.id;
Player.updatePlayerStats(result.playerId, result); }
scoredResults.push({pid: result.playerId, score: calculateStandardScore(result)}); const playerEntryForNewGuest = submission.players.find((player) => player.id === newGuest.nick);
} if (playerEntryForNewGuest) {
const winner = Math.max(...scoredResults.map(result => result.score)); playerEntryForNewGuest.id = newGuest.id;
//winner.incrementWin(); }
//runnerUp.incrementRunnerUp(); }
//drawnPlayers.forEach(player => player.incrementDraw(); return submission;
} }
function calculateStandardScore(result: GameResults): number { function processStandardStatistics(results: PlayerGameResult[], account: IDbUser) {
const top = result.blocks.top.cells; let scoredResults: ScoredResult[] = [];
const bottom = result.blocks.bottom.cells; for (const result of results) {
return ( const scoredResult = {
cellScore(top.aces) + ...result,
cellScore(top.twos) * 2 + ...getStandardScoreFields(result),
cellScore(top.threes) * 3 + };
cellScore(top.fours) * 4 + scoredResults.push(scoredResult);
cellScore(top.fives) * 5 + updatePlayerStats(result.playerId, scoredResult);
cellScore(top.sixes) * 6 + }
cellScore(bottom.three_kind) + const { wasDraw } = incrementPlayerPlacings(scoredResults);
cellScore(bottom.four_kind) + if (wasDraw) {
cellScore(bottom.full_house) * 25 + DbUser.incrementTimesNoWinner(account.id);
cellScore(bottom.sml_straight) * 30 + }
cellScore(bottom.lg_straight) * 40 + DbUser.incrementGamesPlayed(account.id);
cellScore(bottom.yahtzee) * 50 + }
cellScore(bottom.chance)
); async function updatePlayerStats(playerId: string, result: ScoredResult) {
const player: IPlayer = await Player.findById(playerId) as IPlayer;
for (const blockId in result.blocks) {
const cells = result.blocks[blockId as BlockName].cells;
for (const cellId in cells) {
Player.updateCellStats(player.id, {id: cellId, value: cells[cellId as CellName].value});
}
if (result.topBonus) {
Player.incrementBonus(player.id);
}
}
Player.incrementGamesPlayed(player.id);
}
function incrementPlayerPlacings(scoredResults: ScoredResult[]) {
scoredResults = sortDescendingByScore(scoredResults);
const placingFacts: { wasDraw: boolean } = { wasDraw: false };
let runnerUpsStart: number;
if (scoredResults[0].total !== scoredResults[1].total) {
Player.incrementWinFor(scoredResults[0].playerId);
runnerUpsStart = 1;
}
else {
runnerUpsStart = icrmtPlayerDrawsTilScoreChange(scoredResults);
placingFacts.wasDraw = true;
}
const losersStart = icrmtPlayerRunnerUpsTilScoreChange(
scoredResults.slice(runnerUpsStart)
);
icrmtPlayerLosses(scoredResults.slice(losersStart));
return placingFacts;
}
function icrmtPlayerDrawsTilScoreChange(scoredResults: ScoredResult[]): number {
for (let i = 0; i < scoredResults.length; i++) {
if (scoredResults[i].total === scoredResults[0].total) {
Player.incrementDrawFor(scoredResults[i].playerId);
}
else {
return i;
}
}
return scoredResults.length;
}
function icrmtPlayerRunnerUpsTilScoreChange(scoredResults: ScoredResult[]): number {
for (let i = 0; i < scoredResults.length; i++) {
if (scoredResults[i].total === scoredResults[0].total) {
Player.incrementRunnerUpFor(scoredResults[i].playerId);
}
else {
return i;
}
}
return scoredResults.length;
}
function icrmtPlayerLosses(scoredResults: ScoredResult[]): void {
for (const scoredResult of scoredResults) {
Player.incrementLossFor(scoredResult.playerId);
}
}
function sortDescendingByScore(scoredResults: ScoredResult[]) {
return scoredResults.sort((a, b) => b.total - a.total);
}
function getStandardScoreFields(result: PlayerGameResult): ScoreTotalFields {
const scoreFields: ScoreTotalFields = { topBonus: false, topSubtotal: 0, top: 0, bottom: 0, total: 0 };
scoreFields.topSubtotal = topSubtotal(result.blocks.top.cells);
scoreFields.top = scoreFields.topSubtotal;
if (scoreFields.topSubtotal >= UPPER_BONUS_THRESHOLD) {
scoreFields.topBonus = true;
scoreFields.top += UPPER_BONUS;
}
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) { function cellScore(cell: StandardCell) {
if (cell.value === "cellFlagStrike" || cell.value === false) { if (cell.value === "cellFlagStrike" || cell.value === false) {
return 0; return 0;
} else if (cell.value === true) { } else if (cell.value === true) {
return 1; return 1;
} }
return cell.value; return cell.value;
} }
const example = {
players: [
{
id: "5ecbf33a9d246114c0c9d9bb",
nick: "Ledda",
},
],
results: [
{
playerId: "5ecbf33a9d246114c0c9d9bb",
blocks: [
{
id: "top",
cells: [
{ id: "aces", value: 1 },
{ id: "twos", value: 1 },
{ id: "threes", value: 1 },
{ id: "fours", value: 1 },
{ id: "fives", value: 1 },
{ id: "sixes", value: 1 },
],
},
{
id: "bottom",
cells: [
{ id: "three_kind", value: "cellFlagStrike" },
{ id: "four_kind", value: "cellFlagStrike" },
{ id: "full_house", value: true },
{ id: "sml_straight", value: true },
{ id: "lg_straight", value: true },
{ id: "yahtzee", value: 1 },
{ id: "chance", value: "cellFlagStrike" },
],
},
],
},
],
};

View File

@@ -71,6 +71,8 @@ export interface IDbUserModel extends mongoose.Model<IDbUserDoc> {
userWithEmailExists(email: string): Promise<boolean>; userWithEmailExists(email: string): Promise<boolean>;
userWithUsernameExists(username: string): Promise<boolean>; userWithUsernameExists(username: string): Promise<boolean>;
getSerializedAuthUser(id: string): Promise<IDbUser>; getSerializedAuthUser(id: string): Promise<IDbUser>;
incrementTimesNoWinner(id: string): Promise<void>;
incrementGamesPlayed(id: string): Promise<void>;
} }
export const DbUserSchema = new mongoose.Schema({ export const DbUserSchema = new mongoose.Schema({
@@ -132,6 +134,14 @@ DbUserSchema.statics.getSerializedAuthUser = async function (id: string): Promis
}); });
}; };
DbUserSchema.statics.incrementTimesNoWinner = async function (id: string): Promise<void> {
...
};
DbUserSchema.statics.incrementGamesPlayed = async function (id: string): Promise<void> {
...
};
DbUserSchema.methods.getGuests = async function (this: IDbUser): Promise<IPlayer[]> { DbUserSchema.methods.getGuests = async function (this: IDbUser): Promise<IPlayer[]> {
const user: IDbUserDoc = await tryQuery(async () => { const user: IDbUserDoc = await tryQuery(async () => {
return DbUser.findById(this.id, {"guests.nick": 1, "guests._id": 1}).exec(); return DbUser.findById(this.id, {"guests.nick": 1, "guests._id": 1}).exec();

View File

@@ -1,6 +1,12 @@
import mongoose from "mongoose"; import mongoose from "mongoose";
import {IPlayerStats, IPlayerStatsDoc, PlayerStatsSchema} from "./stats"; import {IPlayerStats, IPlayerStatsDoc, PlayerStatsSchema} from "./stats";
import {globalSchemaOptions} from "./utils"; import {globalSchemaOptions} from "./utils";
import {CellValue} from "../controllers/statsController";
interface CellDetails {
id: string;
value: CellValue;
}
export interface IPlayer { export interface IPlayer {
id: string; id: string;
@@ -15,7 +21,13 @@ export interface IPlayerDoc extends mongoose.Document {
} }
export interface IPlayerModel extends mongoose.Model<IPlayerDoc> { export interface IPlayerModel extends mongoose.Model<IPlayerDoc> {
// virtual static methods incrementGamesPlayed(playerId: string): void;
incrementWinFor(playerId: string): void;
incrementDrawFor(playerId: string): void;
incrementRunnerUpFor(playerId: string): void;
incrementLossFor(playerId: string): void;
updateCellStats(playerId: string, cellDetails: CellDetails): void;
incrementBonus(playerId: string): Promise<void>;
} }
export const PlayerSchema = new mongoose.Schema({ export const PlayerSchema = new mongoose.Schema({
@@ -23,5 +35,34 @@ export const PlayerSchema = new mongoose.Schema({
stats: { type: PlayerStatsSchema, required: true, default: () => ({}) }, stats: { type: PlayerStatsSchema, required: true, default: () => ({}) },
}, {...globalSchemaOptions}); }, {...globalSchemaOptions});
PlayerSchema.statics.incrementGamesPlayed = async function (playerId: string): Promise<void> {
...
};
PlayerSchema.statics.incrementWinFor = async function (playerId: string): Promise<void> {
...
};
PlayerSchema.statics.incrementDrawFor = async function (playerId: string): Promise<void> {
...
};
PlayerSchema.statics.incrementRunnerUpFor = async function (playerId: string): Promise<void> {
...
};
PlayerSchema.statics.incrementLossFor = async function (playerId: string): Promise<void> {
...
};
PlayerSchema.statics.updateCellStats = async function (playerId: string, cellDetails: CellDetails): void {
...
};
PlayerSchema.statics.incrementBonus = async function (playerId: string): Promise<void> {
...
};
const Player = mongoose.model<IPlayerDoc, IPlayerModel>("Player", PlayerSchema); const Player = mongoose.model<IPlayerDoc, IPlayerModel>("Player", PlayerSchema);
export default Player; export default Player;

View File

@@ -17,7 +17,6 @@ export interface IBaseStats {
four: IMultiplierFieldStats; four: IMultiplierFieldStats;
five: IMultiplierFieldStats; five: IMultiplierFieldStats;
six: IMultiplierFieldStats; six: IMultiplierFieldStats;
upperBonus: IBonusFieldStats;
upperTotal: ITotalFieldStats; upperTotal: ITotalFieldStats;
threeKind: INumberFieldStats; threeKind: INumberFieldStats;
fourKind: INumberFieldStats; fourKind: INumberFieldStats;
@@ -30,9 +29,6 @@ export interface IBaseStats {
lowerTotal: ITotalFieldStats; lowerTotal: ITotalFieldStats;
gamesPlayed: number; gamesPlayed: number;
} }
interface IBonusFieldStats {
total: number;
}
interface ITotalFieldStats { interface ITotalFieldStats {
average: number; average: number;
best: number; best: number;
@@ -69,7 +65,6 @@ interface IBaseStatsDoc extends mongoose.Document {
four: IMultiplierFieldStatsDoc; four: IMultiplierFieldStatsDoc;
five: IMultiplierFieldStatsDoc; five: IMultiplierFieldStatsDoc;
six: IMultiplierFieldStatsDoc; six: IMultiplierFieldStatsDoc;
upperBonus: IBonusFieldStatsDoc;
upperTotal: ITotalFieldStatsDoc; upperTotal: ITotalFieldStatsDoc;
threeKind: INumberFieldStatsDoc; threeKind: INumberFieldStatsDoc;
fourKind: INumberFieldStatsDoc; fourKind: INumberFieldStatsDoc;
@@ -83,7 +78,6 @@ interface IBaseStatsDoc extends mongoose.Document {
gamesPlayed: number; gamesPlayed: number;
} }
type IBonusFieldStatsDoc = mongoose.Document & IBonusFieldStats;
type ITotalFieldStatsDoc = mongoose.Document & ITotalFieldStats; type ITotalFieldStatsDoc = mongoose.Document & ITotalFieldStats;
type IBoolFieldStatsDoc = mongoose.Document & IBoolFieldStats; type IBoolFieldStatsDoc = mongoose.Document & IBoolFieldStats;
type INumberFieldStatsDoc = mongoose.Document & INumberFieldStats; type INumberFieldStatsDoc = mongoose.Document & INumberFieldStats;
@@ -106,10 +100,6 @@ class Int extends mongoose.SchemaType {
} }
(mongoose.Schema.Types as any).Int = Int; (mongoose.Schema.Types as any).Int = Int;
const BonusFieldStatsSchema = new mongoose.Schema( {
average: {type: Number, required: true, default: 0, min: 0},
total: {type: Int, required: true, default: 0, min: 0}
}, { _id: false });
const TotalFieldStatsSchema = new mongoose.Schema( { const TotalFieldStatsSchema = new mongoose.Schema( {
average: {type: Number, required: true, default: 0, min: 0}, average: {type: Number, required: true, default: 0, min: 0},
best: {type: Int, required: true, default: 0, min: 0}, best: {type: Int, required: true, default: 0, min: 0},
@@ -145,7 +135,6 @@ export const PlayerStatsSchema = new mongoose.Schema( {
four: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) }, four: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) }, five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) }, six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
upperBonus: { type: BonusFieldStatsSchema, required: true, default: () => ({}) },
upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) }, upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
threeKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) }, threeKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
fourKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) }, fourKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
@@ -156,6 +145,7 @@ export const PlayerStatsSchema = new mongoose.Schema( {
chance: { type: NumberFieldStatsSchema, required: true, default: () => ({}) }, chance: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
grandTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) }, grandTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
lowerTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) }, lowerTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
timesHadBonus: {type: Int, required: true, default: 0, min: 0},
gamesPlayed: { type: Int, required: true, default: 0, min: 0 }, gamesPlayed: { type: Int, required: true, default: 0, min: 0 },
wins: { type: Int, required: true, default: 0, min: 0 }, wins: { type: Int, required: true, default: 0, min: 0 },
runnerUps: { type: Int, required: true, default: 0, min: 0 }, runnerUps: { type: Int, required: true, default: 0, min: 0 },
@@ -169,7 +159,6 @@ export const AccountStatsSchema = new mongoose.Schema( {
four: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) }, four: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) }, five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) }, six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
upperBonus: { type: BonusFieldStatsSchema, required: true, default: () => ({}) },
upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) }, upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
threeKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) }, threeKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
fourKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) }, fourKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },