diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..db67a2c --- /dev/null +++ b/notes.txt @@ -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. \ No newline at end of file diff --git a/package.json b/package.json index 1e93b31..20f1613 100755 --- a/package.json +++ b/package.json @@ -40,5 +40,9 @@ "ts-loader": "^7.0.5", "tslint": "^6.1.1", "typescript": "^3.9.3" + }, + "prettier": { + "tabWidth": 4, + "jsxBracketSameLine": true } } diff --git a/src/controllers/statsController.ts b/src/controllers/statsController.ts index 97dfa93..dedc9a6 100755 --- a/src/controllers/statsController.ts +++ b/src/controllers/statsController.ts @@ -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; +interface ScoredResult extends ScoreTotalFields, PlayerGameResult {} + +interface ScoreTotalFields { + topBonus: boolean; + topSubtotal: number; + top: number; + bottom: number; + total: number; } +type PlayerGameResult = { playerId: string; blocks: Record }; type BlockName = "top" | "bottom"; - -interface Block { - cells: Record; -} - +type Block = { cells: Record }; 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 { - 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 { + 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) { + 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) { + 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" }, - ], - }, - ], - }, - ], -}; diff --git a/src/models/dbUser.ts b/src/models/dbUser.ts index 03561a3..4a65665 100755 --- a/src/models/dbUser.ts +++ b/src/models/dbUser.ts @@ -71,6 +71,8 @@ export interface IDbUserModel extends mongoose.Model { userWithEmailExists(email: string): Promise; userWithUsernameExists(username: string): Promise; getSerializedAuthUser(id: string): Promise; + incrementTimesNoWinner(id: string): Promise; + incrementGamesPlayed(id: string): Promise; } 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 { + ... +}; + +DbUserSchema.statics.incrementGamesPlayed = async function (id: string): Promise { + ... +}; + DbUserSchema.methods.getGuests = async function (this: IDbUser): Promise { const user: IDbUserDoc = await tryQuery(async () => { return DbUser.findById(this.id, {"guests.nick": 1, "guests._id": 1}).exec(); diff --git a/src/models/player.ts b/src/models/player.ts index 474fc37..becde27 100755 --- a/src/models/player.ts +++ b/src/models/player.ts @@ -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 { - // 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; } 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 { + ... +}; + +PlayerSchema.statics.incrementWinFor = async function (playerId: string): Promise { + ... +}; + +PlayerSchema.statics.incrementDrawFor = async function (playerId: string): Promise { + ... +}; + +PlayerSchema.statics.incrementRunnerUpFor = async function (playerId: string): Promise { + ... +}; + +PlayerSchema.statics.incrementLossFor = async function (playerId: string): Promise { + ... +}; + +PlayerSchema.statics.updateCellStats = async function (playerId: string, cellDetails: CellDetails): void { + ... +}; + +PlayerSchema.statics.incrementBonus = async function (playerId: string): Promise { + ... +}; + + const Player = mongoose.model("Player", PlayerSchema); export default Player; \ No newline at end of file diff --git a/src/models/stats.ts b/src/models/stats.ts index 8443e17..954c5b1 100755 --- a/src/models/stats.ts +++ b/src/models/stats.ts @@ -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: () => ({}) },