Add documenation of all apps/packages
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user