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

View File

@@ -3,165 +3,237 @@ 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[];
ruleset: string;
players: { id: string; nick: string }[];
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"
| "threes"
| "fours"
| "fives"
| "sixes"
| "three_kind"
| "four_kind"
| "full_house"
| "sml_straight"
| "lg_straight"
| "yahtzee"
| "chance";
interface StandardCell {
value: CellValue;
| "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 {
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, {
"savedGames._id": 1,
"savedGames.results": 1,
"savedGames.createdAt": 1,
});
if (dbUser) {
res.json({ games: dbUser.savedGames });
} else {
res.sendStatus(404);
}
const user = req.user as IDbUser;
const dbUser = await DbUser.findById(user.id, {
"savedGames._id": 1,
"savedGames.results": 1,
"savedGames.createdAt": 1,
});
if (dbUser) {
res.json({ games: dbUser.savedGames });
}
else {
res.sendStatus(404);
}
};
export const saveGame: RequestHandler = async (req, res) => {
const user = req.user as IDbUser;
const submission = req.body as GameSubmission;
await addNewGuestsFromSubmissionAndFillInIds(submission, user);
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 });
const user = req.user as IDbUser;
const submission = req.body as GameSubmission;
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);
}
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 }[] = [];
for (const result of results) {
Player.updatePlayerStats(result.playerId, result);
scoredResults.push({pid: result.playerId, score: calculateStandardScore(result)});
}
const winner = Math.max(...scoredResults.map(result => result.score));
//winner.incrementWin();
//runnerUp.incrementRunnerUp();
//drawnPlayers.forEach(player => player.incrementDraw();
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 calculateStandardScore(result: GameResults): number {
const top = result.blocks.top.cells;
const bottom = result.blocks.bottom.cells;
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)
);
function processStandardStatistics(results: PlayerGameResult[], account: IDbUser) {
let scoredResults: ScoredResult[] = [];
for (const result of results) {
const scoredResult = {
...result,
...getStandardScoreFields(result),
};
scoredResults.push(scoredResult);
updatePlayerStats(result.playerId, scoredResult);
}
const { wasDraw } = incrementPlayerPlacings(scoredResults);
if (wasDraw) {
DbUser.incrementTimesNoWinner(account.id);
}
DbUser.incrementGamesPlayed(account.id);
}
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) {
if (cell.value === "cellFlagStrike" || cell.value === false) {
return 0;
} else if (cell.value === true) {
return 1;
}
return cell.value;
if (cell.value === "cellFlagStrike" || cell.value === false) {
return 0;
} else if (cell.value === true) {
return 1;
}
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>;
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();

View File

@@ -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;

View File

@@ -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: () => ({}) },