5.8 KiB
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) |
| 1 Mile Run | runninglevel.com — 1 mile times |
| Broad Jump | nrpt.co.uk — broad jump power test |
| 3 Cone Drill | nflsavant.com combine data |
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):
- 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). - Average the levels of all activities belonging to the same attribute, rounded to the nearest whole level. That's the attribute level.
- 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:
- 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, perstretch.lower/stretch.upperconfig. - Expand / compress — every standard's level count is resampled to a
single configurable
maxLevel: expansion linearly inserts intermediate levels, compression proportionally resamples down. - Skew — each activity has a
difficultyModifiermultiplier applied uniformly across its levels, to make an activity easier or harder relative to its source data. - 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 byageModifier, 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. - 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,Standardspackages/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 generationapps/api/src/services/calculator.ts— loads the activeStandardsConfigfrom the database and constructsLevelCalculatorapps/api/src/database/seed-data/standards-config.json— seeded standards data/params, including per-activitysourcecitations