Finished implementing the stats updating, just need to implement the new model methods. Added a notes file.
This commit is contained in:
7
notes.txt
Normal file
7
notes.txt
Normal 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.
|
||||
@@ -40,5 +40,9 @@
|
||||
"ts-loader": "^7.0.5",
|
||||
"tslint": "^6.1.1",
|
||||
"typescript": "^3.9.3"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4,
|
||||
"jsxBracketSameLine": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,32 @@ import { RequestHandler } from "express";
|
||||
import Player, { IPlayer } from "../models/player";
|
||||
|
||||
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 {
|
||||
ruleset: string;
|
||||
players: { id: string; nick: string }[];
|
||||
results: GameResults[];
|
||||
results: PlayerGameResult[];
|
||||
}
|
||||
|
||||
interface GameResults {
|
||||
playerId: string;
|
||||
blocks: Record<BlockName, Block>;
|
||||
interface ScoredResult extends ScoreTotalFields, PlayerGameResult {}
|
||||
|
||||
interface ScoreTotalFields {
|
||||
topBonus: boolean;
|
||||
topSubtotal: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
type PlayerGameResult = { playerId: string; blocks: Record<BlockName, Block> };
|
||||
type BlockName = "top" | "bottom";
|
||||
|
||||
interface Block {
|
||||
cells: Record<CellName, StandardCell>;
|
||||
}
|
||||
|
||||
type Block = { cells: Record<CellName, StandardCell> };
|
||||
type CellName =
|
||||
| "aces"
|
||||
| "twos"
|
||||
@@ -35,13 +43,15 @@ type CellName =
|
||||
| "lg_straight"
|
||||
| "yahtzee"
|
||||
| "chance";
|
||||
|
||||
interface StandardCell {
|
||||
value: CellValue;
|
||||
type StandardCell = { value: CellValue };
|
||||
export type CellValue = number | boolean | "cellFlagStrike";
|
||||
enum ResultType {
|
||||
winner,
|
||||
drawn,
|
||||
runnerUp,
|
||||
loser,
|
||||
}
|
||||
|
||||
type CellValue = number | boolean | "cellFlagStrike";
|
||||
|
||||
export const listGames: RequestHandler = async (req, res) => {
|
||||
const user = req.user as IDbUser;
|
||||
const dbUser = await DbUser.findById(user.id, {
|
||||
@@ -51,7 +61,8 @@ export const listGames: RequestHandler = async (req, res) => {
|
||||
});
|
||||
if (dbUser) {
|
||||
res.json({ games: dbUser.savedGames });
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
};
|
||||
@@ -59,62 +70,162 @@ export const listGames: RequestHandler = async (req, res) => {
|
||||
export const saveGame: RequestHandler = async (req, res) => {
|
||||
const user = req.user as IDbUser;
|
||||
const submission = req.body as GameSubmission;
|
||||
await addNewGuestsFromSubmissionAndFillInIds(submission, user);
|
||||
const newGuests: IPlayer[] = await addNewGuests(submission, user);
|
||||
if (newGuests.length > 0) {
|
||||
fillOutSubmissionWithNewIds(submission, newGuests);
|
||||
}
|
||||
const newGame = await user.addGame(submission);
|
||||
if (submission.ruleset === DEFAULT_RULESET) {
|
||||
processStandardStatistics(submission.results, user);
|
||||
}
|
||||
console.log(JSON.stringify(req.body));
|
||||
res.send({ message: "Game submitted successfully!", newGame: newGame });
|
||||
};
|
||||
|
||||
async function addNewGuestsFromSubmissionAndFillInIds(
|
||||
submission: GameSubmission,
|
||||
user: IDbUser
|
||||
): Promise<void> {
|
||||
for (const playerInParticipantList of submission.players) {
|
||||
if (playerInParticipantList.id === playerInParticipantList.nick) {
|
||||
const newGuest = await user.addGuest(playerInParticipantList.nick);
|
||||
const gameResultsFromNewGuest = submission.results.find(
|
||||
(result) => result.playerId === playerInParticipantList.nick
|
||||
);
|
||||
gameResultsFromNewGuest!.playerId = newGuest.id;
|
||||
playerInParticipantList.id = newGuest.id;
|
||||
async function addNewGuests(submission: GameSubmission, user: IDbUser): Promise<IPlayer[]> {
|
||||
const newGuestIds: IPlayer[] = [];
|
||||
for (const playerDetails of submission.players) {
|
||||
const isNewPlayer = playerDetails.id === playerDetails.nick;
|
||||
if (isNewPlayer) {
|
||||
const newGuest: IPlayer = await user.addGuest(playerDetails.nick);
|
||||
newGuestIds.push(newGuest);
|
||||
}
|
||||
}
|
||||
return newGuestIds;
|
||||
}
|
||||
|
||||
function processStandardStatistics(results: GameResults[], account: IDbUser) {
|
||||
let runnerUp: IPlayer;
|
||||
const drawnPlayers: IPlayer[] = [];
|
||||
const scoredResults: { pid: string; score: number }[] = [];
|
||||
function fillOutSubmissionWithNewIds(submission: GameSubmission, newGuestList: IPlayer[]): GameSubmission {
|
||||
for (const newGuest of newGuestList) {
|
||||
const gameResultsFromNewGuest = submission.results.find((result) => result.playerId === newGuest.nick);
|
||||
if (gameResultsFromNewGuest) {
|
||||
gameResultsFromNewGuest.playerId = newGuest.id;
|
||||
}
|
||||
const playerEntryForNewGuest = submission.players.find((player) => player.id === newGuest.nick);
|
||||
if (playerEntryForNewGuest) {
|
||||
playerEntryForNewGuest.id = newGuest.id;
|
||||
}
|
||||
}
|
||||
return submission;
|
||||
}
|
||||
|
||||
function processStandardStatistics(results: PlayerGameResult[], account: IDbUser) {
|
||||
let scoredResults: ScoredResult[] = [];
|
||||
for (const result of results) {
|
||||
Player.updatePlayerStats(result.playerId, result);
|
||||
scoredResults.push({pid: result.playerId, score: calculateStandardScore(result)});
|
||||
const scoredResult = {
|
||||
...result,
|
||||
...getStandardScoreFields(result),
|
||||
};
|
||||
scoredResults.push(scoredResult);
|
||||
updatePlayerStats(result.playerId, scoredResult);
|
||||
}
|
||||
const winner = Math.max(...scoredResults.map(result => result.score));
|
||||
//winner.incrementWin();
|
||||
//runnerUp.incrementRunnerUp();
|
||||
//drawnPlayers.forEach(player => player.incrementDraw();
|
||||
const { wasDraw } = incrementPlayerPlacings(scoredResults);
|
||||
if (wasDraw) {
|
||||
DbUser.incrementTimesNoWinner(account.id);
|
||||
}
|
||||
DbUser.incrementGamesPlayed(account.id);
|
||||
}
|
||||
|
||||
function calculateStandardScore(result: GameResults): number {
|
||||
const top = result.blocks.top.cells;
|
||||
const bottom = result.blocks.bottom.cells;
|
||||
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(top.aces) +
|
||||
cellScore(top.twos) * 2 +
|
||||
cellScore(top.threes) * 3 +
|
||||
cellScore(top.fours) * 4 +
|
||||
cellScore(top.fives) * 5 +
|
||||
cellScore(top.sixes) * 6 +
|
||||
cellScore(bottom.three_kind) +
|
||||
cellScore(bottom.four_kind) +
|
||||
cellScore(bottom.full_house) * 25 +
|
||||
cellScore(bottom.sml_straight) * 30 +
|
||||
cellScore(bottom.lg_straight) * 40 +
|
||||
cellScore(bottom.yahtzee) * 50 +
|
||||
cellScore(bottom.chance)
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,42 +237,3 @@ function cellScore(cell: StandardCell) {
|
||||
}
|
||||
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" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -71,6 +71,8 @@ export interface IDbUserModel extends mongoose.Model<IDbUserDoc> {
|
||||
userWithEmailExists(email: string): Promise<boolean>;
|
||||
userWithUsernameExists(username: string): Promise<boolean>;
|
||||
getSerializedAuthUser(id: string): Promise<IDbUser>;
|
||||
incrementTimesNoWinner(id: string): Promise<void>;
|
||||
incrementGamesPlayed(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
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[]> {
|
||||
const user: IDbUserDoc = await tryQuery(async () => {
|
||||
return DbUser.findById(this.id, {"guests.nick": 1, "guests._id": 1}).exec();
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import mongoose from "mongoose";
|
||||
import {IPlayerStats, IPlayerStatsDoc, PlayerStatsSchema} from "./stats";
|
||||
import {globalSchemaOptions} from "./utils";
|
||||
import {CellValue} from "../controllers/statsController";
|
||||
|
||||
interface CellDetails {
|
||||
id: string;
|
||||
value: CellValue;
|
||||
}
|
||||
|
||||
export interface IPlayer {
|
||||
id: string;
|
||||
@@ -15,7 +21,13 @@ export interface IPlayerDoc extends mongoose.Document {
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -23,5 +35,34 @@ export const PlayerSchema = new mongoose.Schema({
|
||||
stats: { type: PlayerStatsSchema, required: true, default: () => ({}) },
|
||||
}, {...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);
|
||||
export default Player;
|
||||
@@ -17,7 +17,6 @@ export interface IBaseStats {
|
||||
four: IMultiplierFieldStats;
|
||||
five: IMultiplierFieldStats;
|
||||
six: IMultiplierFieldStats;
|
||||
upperBonus: IBonusFieldStats;
|
||||
upperTotal: ITotalFieldStats;
|
||||
threeKind: INumberFieldStats;
|
||||
fourKind: INumberFieldStats;
|
||||
@@ -30,9 +29,6 @@ export interface IBaseStats {
|
||||
lowerTotal: ITotalFieldStats;
|
||||
gamesPlayed: number;
|
||||
}
|
||||
interface IBonusFieldStats {
|
||||
total: number;
|
||||
}
|
||||
interface ITotalFieldStats {
|
||||
average: number;
|
||||
best: number;
|
||||
@@ -69,7 +65,6 @@ interface IBaseStatsDoc extends mongoose.Document {
|
||||
four: IMultiplierFieldStatsDoc;
|
||||
five: IMultiplierFieldStatsDoc;
|
||||
six: IMultiplierFieldStatsDoc;
|
||||
upperBonus: IBonusFieldStatsDoc;
|
||||
upperTotal: ITotalFieldStatsDoc;
|
||||
threeKind: INumberFieldStatsDoc;
|
||||
fourKind: INumberFieldStatsDoc;
|
||||
@@ -83,7 +78,6 @@ interface IBaseStatsDoc extends mongoose.Document {
|
||||
gamesPlayed: number;
|
||||
}
|
||||
|
||||
type IBonusFieldStatsDoc = mongoose.Document & IBonusFieldStats;
|
||||
type ITotalFieldStatsDoc = mongoose.Document & ITotalFieldStats;
|
||||
type IBoolFieldStatsDoc = mongoose.Document & IBoolFieldStats;
|
||||
type INumberFieldStatsDoc = mongoose.Document & INumberFieldStats;
|
||||
@@ -106,10 +100,6 @@ class Int extends mongoose.SchemaType {
|
||||
}
|
||||
(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( {
|
||||
average: {type: Number, 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: () => ({}) },
|
||||
five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
||||
six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
||||
upperBonus: { type: BonusFieldStatsSchema, required: true, default: () => ({}) },
|
||||
upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
|
||||
threeKind: { 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: () => ({}) },
|
||||
grandTotal: { 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 },
|
||||
wins: { 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: () => ({}) },
|
||||
five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
||||
six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
|
||||
upperBonus: { type: BonusFieldStatsSchema, required: true, default: () => ({}) },
|
||||
upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
|
||||
threeKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
|
||||
fourKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
|
||||
|
||||
Reference in New Issue
Block a user