From e4eae5bffb856ebf5a906e66d8b2cca5172c1a96 Mon Sep 17 00:00:00 2001 From: Mike Rosack Date: Mon, 4 Dec 2023 09:41:28 -0600 Subject: [PATCH] fix global stats date issues --- api/controllers/gameController/revert.ts | 9 +- functions/sns/dataMigrations.ts | 9 +- functions/sqs/globalStatsUpdate.ts | 10 +- lib/dynamoose/gameRepository.ts | 7 +- lib/dynamoose/miscDataRepository.ts | 12 +-- lib/dynamoose/userRepository.ts | 7 +- lib/services/gameTurnService.int-spec.ts | 12 +-- lib/services/gameTurnService.ts | 83 +--------------- lib/util/statsUtil.ts | 116 +++++++++++++++++++++++ lib/util/userUtil.ts | 33 +------ 10 files changed, 151 insertions(+), 147 deletions(-) create mode 100644 lib/util/statsUtil.ts diff --git a/api/controllers/gameController/revert.ts b/api/controllers/gameController/revert.ts index fbcde61..7666ea9 100644 --- a/api/controllers/gameController/revert.ts +++ b/api/controllers/gameController/revert.ts @@ -8,15 +8,12 @@ import { IUserRepository, USER_REPOSITORY_SYMBOL } from '../../../lib/dynamoose/ import { inject, provideSingleton } from '../../../lib/ioc'; import { pydtLogger } from '../../../lib/logging'; import { Game, GameTurn } from '../../../lib/models'; -import { - GAME_TURN_SERVICE_SYMBOL, - GameTurnService, - IGameTurnService -} from '../../../lib/services/gameTurnService'; +import { GAME_TURN_SERVICE_SYMBOL, IGameTurnService } from '../../../lib/services/gameTurnService'; import { ISnsProvider, SNS_PROVIDER_SYMBOL } from '../../../lib/snsProvider'; import { GameUtil } from '../../../lib/util/gameUtil'; import { ErrorResponse, HttpRequest, HttpResponseError } from '../../framework'; import { ISqsProvider, SQS_PROVIDER_SYMBOL } from '../../../lib/sqsProvider'; +import { StatsUtil } from '../../../lib/util/statsUtil'; @Route('game') @Tags('game') @@ -61,7 +58,7 @@ export class GameController_Revert { } while (!lastTurn); const user = await this.userRepository.get(lastTurn.playerSteamId); - GameTurnService.updateTurnStatistics(game, lastTurn, user, true); + StatsUtil.updateTurnStatistics(game, lastTurn, user, true); // Update previous turn data delete lastTurn.skipped; diff --git a/functions/sns/dataMigrations.ts b/functions/sns/dataMigrations.ts index 479b917..ff40a3a 100644 --- a/functions/sns/dataMigrations.ts +++ b/functions/sns/dataMigrations.ts @@ -8,13 +8,12 @@ import { IUserRepository, USER_REPOSITORY_SYMBOL } from '../../lib/dynamoose/use import { inject } from '../../lib/ioc'; import { loggingHandler } from '../../lib/logging'; import { Game, TurnData, User } from '../../lib/models'; -import { GameTurnService } from '../../lib/services/gameTurnService'; import { IMiscDataRepository, MISC_DATA_REPOSITORY_SYMBOL } from '../../lib/dynamoose/miscDataRepository'; -import { UserUtil } from '../../lib/util/userUtil'; import { GameUtil } from '../../lib/util/gameUtil'; +import { StatsUtil } from '../../lib/util/statsUtil'; export const handler = loggingHandler(async (event, context, iocContainer) => { const rus = iocContainer.resolve(DataMigrations); @@ -105,7 +104,7 @@ export class DataMigrations { user.statsByGameType = []; for (const turn of allTurns) { - GameTurnService.updateTurnStatistics( + StatsUtil.updateTurnStatistics( allGames[turn.gameId] || ({ players: [] @@ -146,7 +145,7 @@ export class DataMigrations { const turns = await this.gameTurnRepository.getAllTurnsForGame(game.gameId); for (const turn of turns) { - GameTurnService.updateTurnStatistics(game, turn, { + StatsUtil.updateTurnStatistics(game, turn, { steamId: turn.playerSteamId } as User); } @@ -157,7 +156,7 @@ export class DataMigrations { for (const curStats of [ globalStats.data, - UserUtil.getUserGameStats(globalStats.data, game.gameType) + StatsUtil.getGameStats(globalStats.data, game.gameType) ]) { if ( !curStats.firstTurnEndDate || diff --git a/functions/sqs/globalStatsUpdate.ts b/functions/sqs/globalStatsUpdate.ts index 7c5a766..ca72d22 100644 --- a/functions/sqs/globalStatsUpdate.ts +++ b/functions/sqs/globalStatsUpdate.ts @@ -5,9 +5,8 @@ import { IMiscDataRepository, MISC_DATA_REPOSITORY_SYMBOL } from '../../lib/dynamoose/miscDataRepository'; -import { GameTurnService } from '../../lib/services/gameTurnService'; import { GlobalStatsUpdateMessage } from '../../lib/sqsProvider'; -import { UserUtil } from '../../lib/util/userUtil'; +import { StatsUtil } from '../../lib/util/statsUtil'; export const handler = loggingHandler(async (event, context, iocContainer) => { const uugc = iocContainer.resolve(GlobalStatsUpdate); @@ -25,16 +24,17 @@ export class GlobalStatsUpdate { const globalStats = await this.miscDataRepository.getGlobalStats(true); for (const message of messages) { + console.log(message); message.turn.endDate = message.turn.endDate ? new Date(message.turn.endDate) : undefined; message.turn.startDate = message.turn.startDate ? new Date(message.turn.startDate) : undefined; - GameTurnService.updateTurnData(message.turn, globalStats.data, message.undo); + StatsUtil.updateTurnData(message.turn, globalStats.data, message.undo); - GameTurnService.updateTurnData( + StatsUtil.updateTurnData( message.turn, - UserUtil.getUserGameStats(globalStats.data, message.gameType), + StatsUtil.getGameStats(globalStats.data, message.gameType), message.undo ); } diff --git a/lib/dynamoose/gameRepository.ts b/lib/dynamoose/gameRepository.ts index edf3196..07309a6 100644 --- a/lib/dynamoose/gameRepository.ts +++ b/lib/dynamoose/gameRepository.ts @@ -6,6 +6,7 @@ import { Game, GamePlayer, User } from '../models'; import { BaseDynamooseRepository, IRepository } from './common'; import { CIV6_GAME } from '../metadata/civGames/civ6'; import { legacyBoolean, legacyStringSet } from '../util/dynamooseLegacy'; +import { StatsUtil } from '../util/statsUtil'; export const GAME_REPOSITORY_SYMBOL = Symbol('IGameRepository'); @@ -79,11 +80,7 @@ export class GameRepository // Legacy complex array, see above type: String, get: (value: string) => - (value ? JSON.parse(value) : []).map(x => ({ - ...x, - firstTurnEndDate: x.firstTurnEndDate ? new Date(x.firstTurnEndDate) : undefined, - lastTurnEndDate: x.lastTurnEndDate ? new Date(x.lastTurnEndDate) : undefined - })), + (value ? JSON.parse(value) : []).map(x => StatsUtil.fixTurnDataDates(x)), pydtSet: (value: GamePlayer[]) => { return JSON.stringify( (value || []).map(x => ({ diff --git a/lib/dynamoose/miscDataRepository.ts b/lib/dynamoose/miscDataRepository.ts index b51b2ea..3b3eda9 100644 --- a/lib/dynamoose/miscDataRepository.ts +++ b/lib/dynamoose/miscDataRepository.ts @@ -1,6 +1,7 @@ import { Config } from '../config'; import { provideSingleton } from '../ioc'; import { GlobalStats, MiscData } from '../models/miscData'; +import { StatsUtil } from '../util/statsUtil'; import { BaseDynamooseRepository, IRepository } from './common'; export const MISC_DATA_REPOSITORY_SYMBOL = Symbol('IMiscDataRepository'); @@ -37,12 +38,11 @@ export class MiscDataRepository } }) as GlobalStats; - result.data.firstTurnEndDate = result.data.firstTurnEndDate - ? new Date(result.data.firstTurnEndDate) - : undefined; - result.data.lastTurnEndDate = result.data.lastTurnEndDate - ? new Date(result.data.lastTurnEndDate) - : undefined; + StatsUtil.fixTurnDataDates(result.data); + + for (const gameType of result.data.statsByGameType || []) { + StatsUtil.fixTurnDataDates(gameType); + } return result; } diff --git a/lib/dynamoose/userRepository.ts b/lib/dynamoose/userRepository.ts index a263608..2a286bb 100644 --- a/lib/dynamoose/userRepository.ts +++ b/lib/dynamoose/userRepository.ts @@ -4,6 +4,7 @@ import { User, DeprecatedUser } from '../models/user'; import { BaseDynamooseRepository, IRepository } from './common'; import { Game, GameTypeTurnData } from '../models'; import { legacyBoolean, legacyStringSet } from '../util/dynamooseLegacy'; +import { StatsUtil } from '../util/statsUtil'; export const USER_REPOSITORY_SYMBOL = Symbol('IUserRepository'); @@ -117,11 +118,7 @@ export class UserRepository // Legacy complex array, see above type: String, get: (value: string) => - (value ? JSON.parse(value) : []).map(x => ({ - ...x, - firstTurnEndDate: x.firstTurnEndDate ? new Date(x.firstTurnEndDate) : undefined, - lastTurnEndDate: x.lastTurnEndDate ? new Date(x.lastTurnEndDate) : undefined - })), + (value ? JSON.parse(value) : []).map(x => StatsUtil.fixTurnDataDates(x)), pydtSet: (value: GameTypeTurnData[]) => { return JSON.stringify( (value || []).map(x => ({ diff --git a/lib/services/gameTurnService.int-spec.ts b/lib/services/gameTurnService.int-spec.ts index ba10362..90ccac2 100644 --- a/lib/services/gameTurnService.int-spec.ts +++ b/lib/services/gameTurnService.int-spec.ts @@ -2,7 +2,6 @@ import 'reflect-metadata'; import { aws as dynamooseAws, Table } from 'dynamoose'; import { describe, it } from 'mocha'; import { v4 as uuid } from 'uuid'; -import { GameTurnService } from './gameTurnService'; import { GameRepository } from '../dynamoose/gameRepository'; import { CIV6_GAME } from '../metadata/civGames/civ6'; import { Game, GameTurn, User } from '../models'; @@ -11,6 +10,7 @@ import { UserRepository } from '../dynamoose/userRepository'; import { expect } from 'chai'; import { cloneDeep } from 'lodash'; import { ONE_DAY, ONE_HOUR } from 'pydt-shared-models'; +import { StatsUtil } from '../util/statsUtil'; dynamooseAws.ddb.set( new dynamooseAws.ddb.DynamoDB({ @@ -78,7 +78,7 @@ describe('GameTurnService_updateTurnStatistics', () => { displayName: 'player 1' } as User; - GameTurnService.updateTurnStatistics(game, turn1, user); + StatsUtil.updateTurnStatistics(game, turn1, user); const gameRepository = new GameRepository(); const userRepository = new UserRepository(); @@ -114,7 +114,7 @@ describe('GameTurnService_updateTurnStatistics', () => { expect(data.lastTurnEndDate).to.deep.equal(new Date('2023-11-02T09:00:00.000Z')); } - GameTurnService.updateTurnStatistics(game, turn2, user); + StatsUtil.updateTurnStatistics(game, turn2, user); await gameRepository.saveVersioned(game); await userRepository.saveVersioned(user); @@ -148,7 +148,7 @@ describe('GameTurnService_updateTurnStatistics', () => { expect(data.lastTurnEndDate).to.deep.equal(new Date('2023-11-02T23:00:00.000Z')); } - GameTurnService.updateTurnStatistics(game, turn3, user); + StatsUtil.updateTurnStatistics(game, turn3, user); await gameRepository.saveVersioned(game); await userRepository.saveVersioned(user); @@ -182,7 +182,7 @@ describe('GameTurnService_updateTurnStatistics', () => { expect(data.lastTurnEndDate).to.deep.equal(new Date('2023-11-03T23:00:00.000Z')); } - GameTurnService.updateTurnStatistics(game, turn3, user, true); + StatsUtil.updateTurnStatistics(game, turn3, user, true); await gameRepository.saveVersioned(game); await userRepository.saveVersioned(user); @@ -216,7 +216,7 @@ describe('GameTurnService_updateTurnStatistics', () => { expect(data.lastTurnEndDate).to.deep.equal(new Date('2023-11-03T23:00:00.000Z')); } - GameTurnService.updateTurnStatistics(game, turn2, user, true); + StatsUtil.updateTurnStatistics(game, turn2, user, true); await gameRepository.saveVersioned(game); await userRepository.saveVersioned(user); diff --git a/lib/services/gameTurnService.ts b/lib/services/gameTurnService.ts index fb42d74..6b3c303 100644 --- a/lib/services/gameTurnService.ts +++ b/lib/services/gameTurnService.ts @@ -8,7 +8,7 @@ import { GAME_TURN_REPOSITORY_SYMBOL, IGameTurnRepository } from '../dynamoose/g import { IUserRepository, USER_REPOSITORY_SYMBOL } from '../dynamoose/userRepository'; import { inject, provideSingleton } from '../ioc'; import { pydtLogger } from '../logging'; -import { Game, GamePlayer, GameTurn, TurnData, User } from '../models'; +import { Game, GamePlayer, GameTurn, User } from '../models'; import { IS3Provider, S3_PROVIDER_SYMBOL } from '../s3Provider'; import { ActorType, SaveHandler } from '../saveHandlers/saveHandler'; import { SaveHandlerFactory } from '../saveHandlers/saveHandlerFactory'; @@ -20,7 +20,7 @@ import { IPrivateUserDataRepository } from '../dynamoose/privateUserDataRepository'; import { ISqsProvider, SQS_PROVIDER_SYMBOL } from '../sqsProvider'; -import { HOUR_OF_DAY_KEY, ONE_HOUR, TURN_BUCKETS } from 'pydt-shared-models'; +import { StatsUtil } from '../util/statsUtil'; export const GAME_TURN_SERVICE_SYMBOL = Symbol('IGameTurnService'); @@ -66,7 +66,7 @@ export class GameTurnService implements IGameTurnService { private async closeGameTurn(game: Game, gameTurn: GameTurn, user: User) { game.lastTurnEndDate = gameTurn.endDate = new Date(); - GameTurnService.updateTurnStatistics(game, gameTurn, user); + StatsUtil.updateTurnStatistics(game, gameTurn, user); await this.gameTurnRepository.saveVersioned(gameTurn); } @@ -149,83 +149,6 @@ export class GameTurnService implements IGameTurnService { await this.updateSaveFileForGameState(game, users, this.parseSaveFile(data.Body, game)); } - public static updateTurnStatistics(game: Game, gameTurn: GameTurn, user: User, undo?: boolean) { - const player = - game.players.find(p => { - return p.steamId === user.steamId; - }) || ({} as GamePlayer); - - const gameStats = UserUtil.getUserGameStats(user, game.gameType); - - GameTurnService.updateTurnData(gameTurn, player, !!undo); - GameTurnService.updateTurnData(gameTurn, user, !!undo); - GameTurnService.updateTurnData(gameTurn, gameStats, !!undo); - GameTurnService.updateTurnData(gameTurn, game, !!undo); - } - - public static updateTurnData(gameTurn: GameTurn, turnData: Partial, undo: boolean) { - const undoInc = undo ? -1 : 1; - - if (gameTurn.endDate) { - if (!turnData.firstTurnEndDate || gameTurn.endDate < turnData.firstTurnEndDate) { - turnData.firstTurnEndDate = gameTurn.endDate; - } - - if (!turnData.lastTurnEndDate || gameTurn.endDate > turnData.lastTurnEndDate) { - turnData.lastTurnEndDate = gameTurn.endDate; - } - - turnData.turnLengthBuckets = turnData.turnLengthBuckets || {}; - turnData.yearBuckets = turnData.yearBuckets || {}; - - if (gameTurn.skipped) { - turnData.turnsSkipped = (turnData.turnsSkipped || 0) + undoInc; - } else { - turnData.turnsPlayed = (turnData.turnsPlayed || 0) + undoInc; - - const timeTaken = gameTurn.endDate.getTime() - gameTurn.startDate.getTime(); - turnData.timeTaken = (turnData.timeTaken || 0) + timeTaken * undoInc; - - if (timeTaken < ONE_HOUR) { - turnData.fastTurns = (turnData.fastTurns || 0) + undoInc; - } - - if (timeTaken > ONE_HOUR * 6) { - turnData.slowTurns = (turnData.slowTurns || 0) + undoInc; - } - - const MAX_QUEUE_LENGTH = 100; - - // Not sure undoing these queues will make sense in all scenarios... - if (!undo) { - turnData.hourOfDayQueue = - (turnData.hourOfDayQueue || '') + HOUR_OF_DAY_KEY[gameTurn.endDate.getUTCHours()]; - - if (turnData.hourOfDayQueue.length > MAX_QUEUE_LENGTH) { - turnData.hourOfDayQueue = turnData.hourOfDayQueue.substring(1); - } - - turnData.dayOfWeekQueue = (turnData.dayOfWeekQueue || '') + gameTurn.endDate.getUTCDay(); - - if (turnData.dayOfWeekQueue.length > MAX_QUEUE_LENGTH) { - turnData.dayOfWeekQueue = turnData.dayOfWeekQueue.substring(1); - } - } - - turnData.yearBuckets[gameTurn.endDate.getUTCFullYear()] = - (turnData.yearBuckets[gameTurn.endDate.getUTCFullYear()] || 0) + undoInc; - - for (const bucket of TURN_BUCKETS) { - if (timeTaken < bucket) { - turnData.turnLengthBuckets[bucket] = - (turnData.turnLengthBuckets[bucket] || 0) + undoInc; - break; - } - } - } - } - } - public async updateSaveFileForGameState(game, users, handler: SaveHandler, setPlayerType = true) { for (let i = 0; i < handler.civData.length; i++) { if (game.players[i]) { diff --git a/lib/util/statsUtil.ts b/lib/util/statsUtil.ts new file mode 100644 index 0000000..9cc1836 --- /dev/null +++ b/lib/util/statsUtil.ts @@ -0,0 +1,116 @@ +import { HOUR_OF_DAY_KEY, ONE_HOUR, TURN_BUCKETS } from 'pydt-shared-models'; +import { Game, GamePlayer, GameTurn, GameTypeTurnData, TurnData, User } from '../models'; + +export class StatsUtil { + public static getGameStats(user: { statsByGameType: GameTypeTurnData[] }, gameType: string) { + user.statsByGameType = user.statsByGameType || []; + let gameStats = user.statsByGameType.find(x => x.gameType === gameType); + + if (!gameStats) { + gameStats = { + gameType: gameType, + activeGames: 0, + totalGames: 0, + fastTurns: 0, + slowTurns: 0, + timeTaken: 0, + turnsPlayed: 0, + turnsSkipped: 0, + dayOfWeekQueue: '', + hourOfDayQueue: '', + turnLengthBuckets: {}, + yearBuckets: {} + }; + + user.statsByGameType.push(gameStats); + } + + return gameStats; + } + + public static updateTurnStatistics(game: Game, gameTurn: GameTurn, user: User, undo?: boolean) { + const player = + game.players.find(p => { + return p.steamId === user.steamId; + }) || ({} as GamePlayer); + + const gameStats = StatsUtil.getGameStats(user, game.gameType); + + StatsUtil.updateTurnData(gameTurn, player, !!undo); + StatsUtil.updateTurnData(gameTurn, user, !!undo); + StatsUtil.updateTurnData(gameTurn, gameStats, !!undo); + StatsUtil.updateTurnData(gameTurn, game, !!undo); + } + + public static fixTurnDataDates(turnData: Partial) { + turnData.firstTurnEndDate = turnData.firstTurnEndDate + ? new Date(turnData.firstTurnEndDate) + : undefined; + turnData.lastTurnEndDate = turnData.lastTurnEndDate + ? new Date(turnData.lastTurnEndDate) + : undefined; + } + + public static updateTurnData(gameTurn: GameTurn, turnData: Partial, undo: boolean) { + const undoInc = undo ? -1 : 1; + + if (gameTurn.endDate) { + if (!turnData.firstTurnEndDate || gameTurn.endDate < turnData.firstTurnEndDate) { + turnData.firstTurnEndDate = gameTurn.endDate; + } + + if (!turnData.lastTurnEndDate || gameTurn.endDate > turnData.lastTurnEndDate) { + turnData.lastTurnEndDate = gameTurn.endDate; + } + + turnData.turnLengthBuckets = turnData.turnLengthBuckets || {}; + turnData.yearBuckets = turnData.yearBuckets || {}; + + if (gameTurn.skipped) { + turnData.turnsSkipped = (turnData.turnsSkipped || 0) + undoInc; + } else { + turnData.turnsPlayed = (turnData.turnsPlayed || 0) + undoInc; + + const timeTaken = gameTurn.endDate.getTime() - gameTurn.startDate.getTime(); + turnData.timeTaken = (turnData.timeTaken || 0) + timeTaken * undoInc; + + if (timeTaken < ONE_HOUR) { + turnData.fastTurns = (turnData.fastTurns || 0) + undoInc; + } + + if (timeTaken > ONE_HOUR * 6) { + turnData.slowTurns = (turnData.slowTurns || 0) + undoInc; + } + + const MAX_QUEUE_LENGTH = 100; + + // Not sure undoing these queues will make sense in all scenarios... + if (!undo) { + turnData.hourOfDayQueue = + (turnData.hourOfDayQueue || '') + HOUR_OF_DAY_KEY[gameTurn.endDate.getUTCHours()]; + + if (turnData.hourOfDayQueue.length > MAX_QUEUE_LENGTH) { + turnData.hourOfDayQueue = turnData.hourOfDayQueue.substring(1); + } + + turnData.dayOfWeekQueue = (turnData.dayOfWeekQueue || '') + gameTurn.endDate.getUTCDay(); + + if (turnData.dayOfWeekQueue.length > MAX_QUEUE_LENGTH) { + turnData.dayOfWeekQueue = turnData.dayOfWeekQueue.substring(1); + } + } + + turnData.yearBuckets[gameTurn.endDate.getUTCFullYear()] = + (turnData.yearBuckets[gameTurn.endDate.getUTCFullYear()] || 0) + undoInc; + + for (const bucket of TURN_BUCKETS) { + if (timeTaken < bucket) { + turnData.turnLengthBuckets[bucket] = + (turnData.turnLengthBuckets[bucket] || 0) + undoInc; + break; + } + } + } + } + } +} diff --git a/lib/util/userUtil.ts b/lib/util/userUtil.ts index 648397e..2357d94 100644 --- a/lib/util/userUtil.ts +++ b/lib/util/userUtil.ts @@ -1,4 +1,5 @@ -import { User, Game, GameTypeTurnData } from '../models'; +import { User, Game } from '../models'; +import { StatsUtil } from './statsUtil'; export class UserUtil { public static createS3GameCacheKey(steamId: string): string { @@ -9,7 +10,7 @@ export class UserUtil { user.activeGameIds = user.activeGameIds || []; user.activeGameIds = [...new Set([...user.activeGameIds, game.gameId])]; - const gameStats = this.getUserGameStats(user, game.gameType); + const gameStats = StatsUtil.getGameStats(user, game.gameType); gameStats.activeGames++; gameStats.totalGames++; } @@ -23,37 +24,11 @@ export class UserUtil { user.inactiveGameIds = [...new Set([...user.inactiveGameIds, game.gameId])]; } - const gameStats = this.getUserGameStats(user, game.gameType); + const gameStats = StatsUtil.getGameStats(user, game.gameType); gameStats.activeGames = user.activeGameIds.length; if (!addToInactiveGames) { gameStats.totalGames--; } } - - public static getUserGameStats(user: { statsByGameType: GameTypeTurnData[] }, gameType: string) { - user.statsByGameType = user.statsByGameType || []; - let gameStats = user.statsByGameType.find(x => x.gameType === gameType); - - if (!gameStats) { - gameStats = { - gameType: gameType, - activeGames: 0, - totalGames: 0, - fastTurns: 0, - slowTurns: 0, - timeTaken: 0, - turnsPlayed: 0, - turnsSkipped: 0, - dayOfWeekQueue: '', - hourOfDayQueue: '', - turnLengthBuckets: {}, - yearBuckets: {} - }; - - user.statsByGameType.push(gameStats); - } - - return gameStats; - } }