Preprocess standards data

This commit is contained in:
Dominic Ferrando
2025-09-15 12:30:38 -04:00
parent 8c523b4e12
commit 8a251f95c4
2 changed files with 40 additions and 70 deletions
+40 -51
View File
@@ -1,4 +1,4 @@
import { Activity, Attribute, attributes, ActivityPerformance, findNearestPoints, getActivityAttribute, getAttributeActivities, Player, StandardsMap, Levels, Standard, Metrics, activities } from "./util";
import { Activity, Attribute, attributes, ActivityPerformance, getActivityAttribute, getAttributeActivities, Player, StandardsMap, Levels, Standard, Metrics } from "./util";
import rawStandards from "./standards.json" assert { type: "json" }
// SOURCES
@@ -11,12 +11,6 @@ import rawStandards from "./standards.json" assert { type: "json" }
// Broad Jump:
//
const standards = rawStandards as StandardsMap;
type TableData = Record<Activity, {
}>
type LevelCalculatorConfig = {
compressTo?: number;
expandIters?: number;
@@ -24,24 +18,36 @@ type LevelCalculatorConfig = {
export class LevelCalculator {
cfg: Required<LevelCalculatorConfig>;
standardsMap: StandardsMap;
public constructor(cfg: LevelCalculatorConfig = {}) {
// initialize config
this.cfg = {
compressTo: cfg.compressTo ?? 100,
expandIters: cfg.expandIters ?? 5
}
// initialize standards map
this.standardsMap = rawStandards as StandardsMap;
for (const activity of Object.keys(this.standardsMap) as Activity[]) {
for (const standard of this.standardsMap[activity]) {
const expandedLevels = this.expandLevels(standard.levels, this.cfg.expandIters);
const compressedLevels = this.compressLevels(expandedLevels, this.cfg.compressTo);
standard.levels = compressedLevels;
}
}
}
// -------------------------------------------------------------------------------------------------
// Level utilities (helpers)
// -------------------------------------------------------------------------------------------------
findLevel(levels: Levels, performance: number): number {
private findLevel(standard: Standard, performance: number): number {
let resultLevel = 1;
let resultLevelDiff = Infinity;
for (const level in levels) {
const diff = Math.abs(performance - levels[level as unknown as number]);
for (const level in standard.levels) {
const diff = Math.abs(performance - standard.levels[level as unknown as number]);
if (diff < resultLevelDiff) {
resultLevel = +level;
resultLevelDiff = diff;
@@ -50,7 +56,7 @@ export class LevelCalculator {
return resultLevel;
}
findNearestStandards(standards: Standard[], target: number, getValue: (x: Metrics) => number) {
private findNearestStandards(standards: Standard[], target: number, getValue: (x: Metrics) => number) {
const lowerStandard = [...standards]
.sort((a, b) => getValue(a.metrics) - getValue(b.metrics))
.reverse()
@@ -61,7 +67,7 @@ export class LevelCalculator {
return { lowerStandard, upperStandard };
}
compressLevels(
private compressLevels(
levels: Levels,
targetLevelsAmount: number,
): Levels {
@@ -90,7 +96,7 @@ export class LevelCalculator {
return compressedLevels;
}
expandLevels(
private expandLevels(
levels: Levels,
i: number
): Levels {
@@ -117,7 +123,7 @@ export class LevelCalculator {
// Standards interpolation (heavy math bits)
// -------------------------------------------------------------------------------------------------
interpolateLevels(
private interpolateLevels(
lower: Levels,
upper: Levels,
ratio: number,
@@ -133,25 +139,7 @@ export class LevelCalculator {
return interpolatedLevels;
}
interpolateByWeight(
targetAge: number,
targetWeight: number,
standards: Standard[],
): Levels {
const ageFilteredStandards = standards.filter((s) => s.metrics.age === targetAge);
if (ageFilteredStandards[0].metrics.weight === -1) return ageFilteredStandards[0].levels;
const { upperStandard, lowerStandard } = this.findNearestStandards(ageFilteredStandards, targetWeight, m => m.weight);
const weightLower = lowerStandard.metrics.weight;
const weightUpper = upperStandard.metrics.weight;
let weightRatio = weightUpper === weightLower ? 1 : (targetWeight - weightLower) / (weightUpper - weightLower);
weightRatio = Math.max(0, Math.min(1, weightRatio));
return this.interpolateLevels(lowerStandard.levels, upperStandard.levels, weightRatio);
}
getInterpolatedActivityStandard(
private getInterpolatedActivityStandard(
activity: Activity,
metrics: Metrics,
): Standard {
@@ -161,7 +149,7 @@ export class LevelCalculator {
};
// Filter by gender
const standardsByGender = standards[activity]
const standardsByGender = this.standardsMap[activity]
.filter(a => a.metrics.gender === metrics.gender);
if (standardsByGender.length === 0) return interpolatedStandard;
@@ -171,18 +159,27 @@ export class LevelCalculator {
const ageUpper = upperStandard.metrics.age;
// Interpolate by weight
const lowerLevels = this.interpolateByWeight(ageLower, metrics.weight, standardsByGender);
const upperLevels = this.interpolateByWeight(ageUpper, metrics.weight, standardsByGender);
const interpolateByWeight = (targetAge: number): Levels => {
const ageFilteredStandards = standardsByGender.filter((s) => s.metrics.age === targetAge);
if (ageFilteredStandards[0].metrics.weight === -1) return ageFilteredStandards[0].levels;
const { upperStandard, lowerStandard } = this.findNearestStandards(ageFilteredStandards, metrics.weight, m => m.weight);
const weightLower = lowerStandard.metrics.weight;
const weightUpper = upperStandard.metrics.weight;
let weightRatio = weightUpper === weightLower ? 1 : (metrics.weight - weightLower) / (weightUpper - weightLower);
weightRatio = Math.max(0, Math.min(1, weightRatio));
return this.interpolateLevels(lowerStandard.levels, upperStandard.levels, weightRatio);
}
// Interpolate by age
let ageRatio = ageUpper === ageLower ? 1 : (metrics.age - ageLower) / (ageUpper - ageLower);
ageRatio = Math.max(0, Math.min(1, ageRatio));
const levels = this.interpolateLevels(lowerLevels, upperLevels, ageRatio);
// Post-process
const expandedLevels = this.expandLevels(levels, this.cfg.expandIters);
const compressedLevels = this.compressLevels(expandedLevels, this.cfg.compressTo);
interpolatedStandard.levels = compressedLevels;
interpolatedStandard.levels = this.interpolateLevels(
interpolateByWeight(ageLower),
interpolateByWeight(ageUpper),
ageRatio
);
return interpolatedStandard;
}
@@ -191,14 +188,6 @@ export class LevelCalculator {
// Level calculations (public API)
// -------------------------------------------------------------------------------------------------
// public generateTableData(metrics: Metrics) {
// const tableData: Record<Activity, Object> = {};
//
// for (const activity of Object.values(activities)) {
// const activityLevels = this.interpolateActivityLevels(activity, metrics);
// }
// }
public calcAttributeLevel(
attribute: Attribute,
player: Player,
@@ -224,7 +213,7 @@ export class LevelCalculator {
const activityLevels = activityPerformances.map((p) => {
const interpolatedStandard = this.getInterpolatedActivityStandard(p.activity, player.metrics);
return this.findLevel(interpolatedStandard.levels, p.performance);
return this.findLevel(interpolatedStandard, p.performance);
});
const activityLevelsAvg = Math.round(
-19
View File
@@ -135,22 +135,3 @@ export const msToTime = (ms: number, includeMs = false): string => {
}
return formattedTime;
};
// -------------------------------------------------------------------------------------------------
// Misc. utilities
// -------------------------------------------------------------------------------------------------
export const findNearestPoints = (value: number, arr: number[]): { lower: number; upper: number } => {
if (arr.length === 1) return { lower: arr[0], upper: arr[0] };
if (value <= arr[0]) return { lower: arr[0], upper: arr[1] };
if (value > arr[arr.length - 1])
return { lower: arr[arr.length - 2], upper: arr[arr.length - 1] };
for (let i = 1; i < arr.length; i++) {
if (arr[i] >= value) return { lower: arr[i - 1], upper: arr[i] };
}
// Fallback, though logic should always return above
return { lower: arr[0], upper: arr[0] };
};