Files
platform/docs/level-calculator.md

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):

  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