# The Level Calculator ## Introduction The level calculator turns a player's raw performance on a handful of physical tests — how much they can squat, how fast they run a mile, how far they can broad jump — into a single, easy-to-understand number: their **level** for each attribute, and an overall player level. The intent is to make performance legible and comparable across people of different ages, weights, and genders. A 45-year-old squatting 225 lb and a 22-year-old squatting 225 lb are not doing the same thing physiologically, so raw numbers alone aren't a fair yardstick. Instead, each activity is measured against **standards**: tables of "what performance corresponds to what level" for a given age/weight/gender, built from published strength and athletic performance research. A player's level for an activity is found by comparing their performance against the standard for players like them; their level for an attribute (e.g. Strength) is the rounded average of their levels across that attribute's activities; their overall player level is the rounded average across all four attributes. The guiding principle is: **use real external standards as ground truth wherever they exist, and only ever generate/extrapolate around that ground truth** — never invent numbers from nothing. Standards data is also fully config-driven (via `apps/portal`'s calculator config/dataset pages), so it can be tuned or replaced without a code change. ## Attributes and activities | Attribute | Activity | Unit | | --- | --- | --- | | Strength | Back Squat, Deadlift, Bench Press | kg | | Power | Broad Jump | cm | | Endurance | 1 Mile Run | ms | | Agility | 3 Cone Drill | ms | ## Sources The raw standards tables (before any generation/extrapolation) come from: | Activity | Source | | --- | --- | | Back Squat, Deadlift, Bench Press | [Lon Kilgore Strength Standard Tables (2023)](http://lonkilgore.com/resources/Lon_Kilgore_Strength_Standard_Tables-Copyright-2023.pdf) | | 1 Mile Run | [runninglevel.com — 1 mile times](https://runninglevel.com/running-times/1-mile-times) | | Broad Jump | [nrpt.co.uk — broad jump power test](https://nrpt.co.uk/training/tests/power/broad.htm) | | 3 Cone Drill | [nflsavant.com combine data](https://nflsavant.com/combine.php) | These are also recorded per-activity as a `source` field on each activity's metadata in the seeded standards dataset (`apps/api/src/database/seed-data/standards-config.json`), which is what's actually loaded at runtime — the config is editable from the portal, so that seed file (and this document) may drift from whatever standards are live. ## How a level is calculated `LevelCalculator.calculate` (`packages/calculator/src/index.ts`): 1. For each activity performance the player submitted, look up the standard for that activity at the player's exact age/weight/gender (interpolated — see below), and find the level whose value is numerically closest to the player's performance (`findLevel`). 2. Average the levels of all activities belonging to the same attribute, rounded to the nearest whole level. That's the attribute level. 3. Average all four attribute levels, rounded, for the overall player level. If any required input is missing (a metric, or a performance of `0` or less), the calculator returns level `0` rather than guessing. ## How the standards tables are built The raw source data only covers a handful of discrete levels, ages, and weights. `Standards` (`packages/calculator/src/index.ts`) expands that into a continuous table through a fixed pipeline, run once per config: 1. **Stretch** — the raw data defines 5 base levels. To support fewer/more levels below/above those 5, an exponential-decay curve (`A·e^(-B·i) + C`) is fit (via Levenberg-Marquardt) to the ratio between consecutive levels, then used to extrapolate additional levels in either direction, per `stretch.lower` / `stretch.upper` config. 2. **Expand / compress** — every standard's level count is resampled to a single configurable `maxLevel`: expansion linearly inserts intermediate levels, compression proportionally resamples down. 3. **Skew** — each activity has a `difficultyModifier` multiplier applied uniformly across its levels, to make an activity easier or harder relative to its source data. 4. **Age generation** — for ages missing from the source data, a standard is derived from the nearest reference age using a parabolic falloff centered on a configurable `peakAge` (steepness controlled by `ageModifier`, clamped to a `[0.2, 10.0]` multiplier), scaled against average bodyweight for that age (`avg-weights.json`). Real data always takes precedence over generated data. 5. **Weight generation** — for weights missing from the source data, a standard is derived from the reference weight using allometric scaling: `newLevel = refLevel * (weight / refWeight) ^ weightModifier`. Again, real data always takes precedence. ## Finding a player's standard Given a player's exact age/weight/gender, `interpolateByAgeAndWeight` performs bilinear interpolation across the nearest surrounding age and weight entries in the (by now fully generated) standards table, producing a standard specific to that player. `findLevel` then maps their submitted performance onto the nearest level in that standard. ## Where this lives in code - `packages/calculator/src/index.ts` — `LevelCalculator`, `Standards` - `packages/calculator/src/models.ts` — schemas/types (`StandardsData`, `StandardsParams`, etc.) - `packages/calculator/src/avg-weights.ts` + `data/avg-weights.json` — average bodyweight by age/gender, used in age generation - `apps/api/src/services/calculator.ts` — loads the active `StandardsConfig` from the database and constructs `LevelCalculator` - `apps/api/src/database/seed-data/standards-config.json` — seeded standards data/params, including per-activity `source` citations