Skip to content

Commit

Permalink
add first iteration for the new hero stats computation (missing warba…
Browse files Browse the repository at this point in the history
…nds)
  • Loading branch information
sebastientromp committed Feb 10, 2023
1 parent 4601cd8 commit 88d0c70
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ $ curl https://static-api.firestoneapp.com/bgs-quests?questCardId=BG24_Quest_313

Used this project as template: https://github.com/alukach/aws-sam-typescript-boilerplate

# Random musings
# Random notes for Quests

what do we want to get?

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firestone-hs/bgs-global-stats",
"version": "1.0.29",
"version": "1.0.33",
"description": "",
"scripts": {
"lint": "eslint --color --fix --ext .ts .",
Expand Down
2 changes: 2 additions & 0 deletions src/bgs-global-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ export interface MmrPercentile {
readonly mmr: number;
readonly percentile: 100 | 50 | 25 | 10 | 1;
}

export * from './stats-v2/bgs-hero-stat';
32 changes: 31 additions & 1 deletion src/build-battlegrounds-hero-stats-new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { buildMmrPercentiles, buildPlacementDistribution, filterRowsForTimePerio
import { InternalBgsRow } from './internal-model';
import { handleQuests } from './quests';
import { handleQuestsV2 } from './quests-v2/quests-v2';
import { handleStatsV2 } from './stats-v2/stats-v2';
import { formatDate, normalizeHeroCardId } from './utils/util-functions';

export const s3 = new S3();
Expand Down Expand Up @@ -49,6 +50,12 @@ export const handleNewStats = async (event, context: Context) => {
for (const timePeriod of allTimePeriods) {
await handleQuestsV2(timePeriod, rows, lastPatch);
}
} else if (event.statsV2) {
const rows: readonly InternalBgsRow[] = await readRowsFromS3();
logger.log('read rows', rows?.length);
for (const timePeriod of allTimePeriods) {
await handleStatsV2(timePeriod, rows, lastPatch, allCards);
}
} else if (event.permutation) {
const rows: readonly InternalBgsRow[] = await readRowsFromS3();
logger.log('read rows', rows?.length);
Expand All @@ -63,6 +70,7 @@ export const handleNewStats = async (event, context: Context) => {
await dispatchNewLambdas(rows, context);
await dispatchQuestsLambda(rows, context);
await dispatchQuestsV2Lambda(rows, context);
await dispatchStatsV2Lambda(rows, context);
}

cleanup();
Expand Down Expand Up @@ -113,6 +121,28 @@ const dispatchQuestsV2Lambda = async (rows: readonly InternalBgsRow[], context:
logger.log('\tinvocation result', result);
};

const dispatchStatsV2Lambda = async (rows: readonly InternalBgsRow[], context: Context) => {
const newEvent = {
statsV2: true,
};
const params = {
FunctionName: context.functionName,
InvocationType: 'Event',
LogType: 'Tail',
Payload: JSON.stringify(newEvent),
};
logger.log('\tinvoking lambda', params);
const result = await lambda
.invoke({
FunctionName: context.functionName,
InvocationType: 'Event',
LogType: 'Tail',
Payload: JSON.stringify(newEvent),
})
.promise();
logger.log('\tinvocation result', result);
};

const dispatchNewLambdas = async (rows: readonly InternalBgsRow[], context: Context) => {
const allTribes = extractAllTribes(rows);
logger.log('all tribes', allTribes);
Expand Down Expand Up @@ -349,7 +379,7 @@ const buildWarbandStats = (
return result;
};

const buildCombatWinrate = (
export const buildCombatWinrate = (
rows: readonly InternalBgsRow[],
): readonly { turn: number; dataPoints: number; totalWinrate: number }[] => {
const ref = rows[0];
Expand Down
12 changes: 12 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ export const buildPlacementDistribution = (
return placementDistribution;
};

export const buildPlacementDistributionWithPercentages = (
rows: readonly InternalBgsRow[],
): readonly { rank: number; percentage: number }[] => {
const placementDistribution = buildPlacementDistribution(rows);
const totalMatches = placementDistribution.map(p => p.totalMatches).reduce((a, b) => a + b, 0);
const result: readonly { rank: number; percentage: number }[] = placementDistribution.map(p => ({
rank: p.rank,
percentage: (100 * p.totalMatches) / totalMatches,
}));
return result;
};

export interface PatchInfo {
readonly number: number;
readonly version: string;
Expand Down
32 changes: 32 additions & 0 deletions src/stats-v2/bgs-hero-stat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Race } from '@firestone-hs/reference-data';
import { MmrPercentile } from '../bgs-global-stats';
import { WithMmrAndTimePeriod } from '../quests-v2/charged-stat';

export interface BgsHeroStatsV2 {
readonly lastUpdateDate: Date;
readonly mmrPercentiles: readonly MmrPercentile[];
readonly dataPoints: number;
readonly heroStats: readonly WithMmrAndTimePeriod<BgsGlobalHeroStat>[];
}

export interface BgsGlobalHeroStat {
readonly heroCardId: string;
readonly dataPoints: number;
readonly averagePosition: number;
readonly placementDistribution: readonly { rank: number; percentage: number }[];
readonly combatWinrate: readonly { turn: number; winrate: number }[];
// // Same
// readonly warbandStats: readonly { turn: number; dataPoints: number; totalStats: number }[];
readonly tribeStats: readonly BgsHeroTribeStat[];
}

export interface BgsHeroTribeStat {
readonly tribe: Race;
readonly dataPoints: number;
readonly averagePosition: number;
readonly impactAveragePosition: number;
readonly placementDistribution: readonly { rank: number; percentage: number }[];
readonly impactPlacementDistribution: readonly { rank: number; impact: number }[];
readonly combatWinrate: readonly { turn: number; winrate: number }[];
readonly impactCombatWinrate: readonly { turn: number; impact: number }[];
}
104 changes: 104 additions & 0 deletions src/stats-v2/stats-buikder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { groupByFunction } from '@firestone-hs/aws-lambda-utils';
import { AllCardsService, CardIds, Race } from '@firestone-hs/reference-data';
import { buildCombatWinrate } from '../build-battlegrounds-hero-stats-new';
import { buildPlacementDistributionWithPercentages } from '../common';
import { InternalBgsRow } from '../internal-model';
import { normalizeHeroCardId } from '../utils/util-functions';
import { BgsGlobalHeroStat, BgsHeroTribeStat } from './bgs-hero-stat';

export const buildStats = (
rows: readonly InternalBgsRow[],
allCards: AllCardsService,
): readonly BgsGlobalHeroStat[] => {
const groupedByHero: {
[questCardId: string]: readonly InternalBgsRow[];
} = groupByFunction((row: InternalBgsRow) => normalizeHeroCardId(row.heroCardId, allCards))(rows);
return Object.values(groupedByHero).flatMap(data => buildStatsForSingleHero(data));
};

// All rows here belong to a single hero
const buildStatsForSingleHero = (rows: readonly InternalBgsRow[]): BgsGlobalHeroStat => {
const ref = rows[0];
const averagePosition = average(rows.map(r => r.rank));
const placementDistribution = buildPlacementDistributionWithPercentages(rows);
const rawCombatWinrates = buildCombatWinrate(rows);
const combatWinrate: readonly { turn: number; winrate: number }[] = rawCombatWinrates.map(info => ({
turn: info.turn,
winrate: info.totalWinrate / info.dataPoints,
}));
const result: BgsGlobalHeroStat = {
heroCardId: ref.heroCardId,
dataPoints: rows.length,
averagePosition: averagePosition,
placementDistribution: placementDistribution,
combatWinrate: combatWinrate,
tribeStats: buildTribeStats(rows, averagePosition, placementDistribution, combatWinrate),
};
if (ref.heroCardId === CardIds.PatchesThePirateBattlegrounds) {
console.debug('Patches sanity', result);
console.debug(
'Patches sanity',
result.tribeStats.map(t => t.impactAveragePosition * t.dataPoints).reduce((a, b) => a + b, 0),
);
console.debug(
'Patches sanity',
rows.filter(r => !r.tribes.includes('' + Race.PIRATE)).length,
rows.filter(r => !r.tribes.includes('' + Race.PIRATE)),
);
}
return result;
};

const buildTribeStats = (
rows: readonly InternalBgsRow[],
refAveragePosition: number,
refPlacementDistribution: readonly { rank: number; percentage: number }[],
refCombatWinrate: readonly { turn: number; winrate: number }[],
): readonly BgsHeroTribeStat[] => {
const uniqueTribes: readonly Race[] = [...new Set(rows.flatMap(r => r.tribes.split(',')).map(r => parseInt(r)))];
return uniqueTribes.map(tribe => {
const rowsForTribe = rows.filter(r => r.tribes.split(',').includes('' + tribe));
const rowsWithoutTribe = rows.filter(r => !r.tribes.split(',').includes('' + tribe));
const averagePosition = average(rowsForTribe.map(r => r.rank));
const placementDistribution = buildPlacementDistributionWithPercentages(rowsForTribe);
const rawCombatWinrates = buildCombatWinrate(rowsForTribe);
const combatWinrate = rawCombatWinrates.map(info => ({
turn: info.turn,
winrate: info.totalWinrate / info.dataPoints,
}));
return {
tribe: tribe,
dataPoints: rowsForTribe.length,
dataPointsOnMissingTribe: rowsWithoutTribe.length,
averagePosition: averagePosition,
impactAveragePosition: averagePosition - refAveragePosition,
placementDistribution: placementDistribution,
impactPlacementDistribution: refPlacementDistribution.map(p => {
const newPlacementInfo = placementDistribution.find(p2 => p2.rank === p.rank);
// Cna happen when there isn't a lot of data points, typically for high MMR
if (!newPlacementInfo) {
console.log('missing placement info', placementDistribution, p);
}
return {
rank: p.rank,
impact: (newPlacementInfo?.percentage ?? 0) - p.percentage,
};
}),
combatWinrate: combatWinrate,
impactCombatWinrate: refCombatWinrate.map(c => {
const newCombatWinrate = combatWinrate.find(c2 => c2.turn === c.turn);
if (!newCombatWinrate) {
console.debug('missing winrate info', combatWinrate);
}
return {
turn: c.turn,
impact: (newCombatWinrate?.winrate ?? 0) - c.winrate,
};
}),
};
});
};

const average = (data: readonly number[]): number => {
return data.reduce((a, b) => a + b, 0) / data.length;
};
41 changes: 41 additions & 0 deletions src/stats-v2/stats-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { logger } from '@firestone-hs/aws-lambda-utils';
import { AllCardsService } from '@firestone-hs/reference-data';
import { gzipSync } from 'zlib';
import { s3 } from '../build-battlegrounds-hero-stats-new';
import { PatchInfo } from '../common';
import { InternalBgsRow } from '../internal-model';
import { buildSplitStats } from '../quests-v2/data-filter';
import { BgsHeroStatsV2 } from './bgs-hero-stat';
import { buildStats } from './stats-buikder';

export const STATS_BUCKET = `static.zerotoheroes.com`;

export const handleStatsV2 = async (
timePeriod: 'all-time' | 'past-three' | 'past-seven' | 'last-patch',
rows: readonly InternalBgsRow[],
lastPatch: PatchInfo,
allCards: AllCardsService,
) => {
const rowsWithStats = rows.filter(row => !!row.rank).filter(r => !!r.tribes?.length);
console.log('total relevant rows', rowsWithStats?.length);

const statResult = await buildSplitStats(rowsWithStats, timePeriod, lastPatch, (data: InternalBgsRow[]) =>
buildStats(data, allCards),
);
const stats = statResult.stats;
const statsV2: BgsHeroStatsV2 = {
lastUpdateDate: new Date(),
mmrPercentiles: statResult.mmrPercentiles,
heroStats: stats,
dataPoints: stats.map(s => s.dataPoints).reduce((a, b) => a + b, 0),
};
logger.log('\tbuilt stats', statsV2.dataPoints, statsV2.heroStats?.length);
const timeSuffix = timePeriod;
await s3.writeFile(
gzipSync(JSON.stringify(statsV2)),
STATS_BUCKET,
`api/bgs/stats-v2/bgs-${timeSuffix}.gz.json`,
'application/json',
'gzip',
);
};

0 comments on commit 88d0c70

Please sign in to comment.