# Fantasy football scoring rules

Verified 2026-09-04 against 11 Scoring Settings screenshots supplied by the league owner. The
canonical JSON and generated CSV contain every active rule shown, plus explicit zero bands needed
by the scorer. The screenshot transcription and comparison are in
`audit-GPT/SCORING_RULES_SCREENSHOTS.md`.

The operational 2025 scorer uses nflverse weekly statistics plus snap-count participation. The
bonus model uses 2021–2025. Participation-corrected predictor research can use 2013–2025, the
years for which the required snap-count files exist. Older narrative research is clearly marked
archival rather than presented as current draft guidance.

| File | What it is |
|---|---|
| `scoring_rules.json` | The canonical record. Every rule with its type, its points, and its ranges. Read this from code. |
| `scoring_rules.csv` | The same rules flattened to one row per rule (tiered rules get one row per band) for spreadsheets. Rebuild it with `python3 score.py --csv`. |
| `score.py` | A scorer that reads the JSON. `score_player(stats)` returns the total and a line-by-line breakdown. Running it plays seven example stat lines against hand-computed answers. |
| `players_2025_scored.csv` | **Every snap-bearing player's 2025 season scored under these rules** — 1,721 players, 1,237 with 8+ active games. `active_games` and `stat_games` make the denominator auditable. |
| `analyze_2025.py` | Builds that file and prints the positional analysis. Run `python3 analyze_2025.py`. |
| `data_2025_weekly.csv` | The raw 2025 weekly stats it reads (from nflverse). |
| `DRAFT_GUIDE_2026.md` | **Current operational draft guidance** from the repaired 2026 board, with limitations. |
| `STRATEGY.md`, `PROJECTING.md` | Archived pre-audit research narratives. Kept for provenance; do not treat their numerical recommendations as current. |
| `README.md` | This summary. |

## Draft day: the Big Board and the draft-state server

`projecting.html#big-board` opens a full-screen board for the draft itself. The player table
(search, position pills, WATCHLIST / SHOW DRAFTED / ROOKIES ONLY) sits beside the drop-off
plot; each row has **TAKEN** (another team drafted him) and **MINE** buttons and a watchlist
star. A watchlisted player is drawn as a star on the plot and a rookie carries an **R**. A player
taken by another team disappears from the plot; your own picks remain with a gold marker ring and
gold name outline. Each position also has a dashed L-shaped guide from its best available player
to the eleventh available player, with the exact next-10 points/game drop shown in the legend.
Below 900 px wide the board becomes two tabs, BOARD and PLOT.

**The state is on this machine, not in the browser.** `draft_server.py` (standard library only)
serves the site on port 8124 and owns `draft_state.json`. Every open page — phone, desktop, LAN
or Tailscale — sees the same board within a second, through a live stream from the server.

| File | What it is |
|---|---|
| `draft_server.py` | The server. Static files plus `api/state`, `api/mutate`, `api/clear`, `api/undo`, `api/restore`, `api/events` (live stream), `api/state.csv`, `api/history`. |
| `draft_state.json` | **The draft.** Hand-editable; see below. |
| `draft_keepers.json` | Keeper baseline restored whenever the board is cleared. |
| `draft_log.jsonl` | One line per change, never rewritten: who marked whom, when, from which device. Any state can be recovered from it. |
| `snapshots/` and `draft_state.bak.json` | Written automatically before every clear (newest 20 kept). "Restore previous board" in the board's ⋯ menu reads these. |
| `board.py`, `chartjs.py` | The board's markup, styling and browser code, assembled into the page by `build_html.py`. |

The live `Proj Pts` and `Proj PPG` columns are literal applications of the league rules to the
four-source statistical blend. Published shrinkage slopes are retained only in the CSV's
`research_calibrated_*` fields and do not drive the live rank. `vorp_ppg` is value above the first
projected nonstarter at the starter cutoff, not a claim about who will remain on waivers after backups are
drafted. `market_adp` uses PPR ADP for offensive players and IDP ADP for individual defenders.
The live `Wait 10` value is the projected PPG lost if ten more players at that same position go;
it is an urgency signal to use alongside static VORP and expected availability.

**Editing the state file by hand.** Open `draft_state.json` in any text editor. Each player is a
line like

```
"RB|Jahmyr Gibbs": {"drafted": "other", "at": "…", "by": "phone-k3f", "watch": false, "sleeper_id": "9221"}
```

`drafted` is `"me"`, `"other"`, or `null`; writing `true` also works and is read as `"other"`.
`watch` is `true` or `false`. Save the file and every open page updates within about a second.
If you save a file that is not valid JSON (a missing comma is the usual cause), the server keeps
the last good state, the board shows a red band naming the line, and picks keep working; fix the
file and everything is re-applied. A key that matches no player on the board is listed in an
amber band, which is how a typo shows itself.

**Clearing the board.** The CLEAR BOARD button removes draft-day picks and restores the 13-player
baseline in `draft_keepers.json`: 12 league keepers remain TAKEN and De'Von Achane remains MINE.
Watchlist flags are preserved. An UNDO button stays on screen for fifteen seconds, and “Restore
previous board” stays in the ⋯ menu for as long as a backup exists. Undo merges: a pick made
between the clear and the undo is kept.

**Position pills.** The small number under each pill is *my picks at that position / how many
start each week*; the top can exceed the bottom as you draft backups. The starter counts and the
roster size live under `settings` in `draft_state.json` and can be edited there.

**Running it.** Loopback-only is the safe default:

```bash
python3 draft_server.py --port 8124
```

Binding to `0.0.0.0` exposes draft data and mutation endpoints to the network. It now requires an
explicit `--allow-unauthenticated-network` flag and should be used only on a trusted private
network (or behind an authenticated reverse proxy). Rebuild the page with
`python3 build_html.py` after changing the board or page sources.

## Analytical scripts

These scripts print results. Pre-audit `_output.txt` files were moved to
`archive/pre-audit-outputs/` so stale numbers cannot be mistaken for current output.

| Script | What it measures |
|---|---|
| `predictors.py` | Year-over-year correlations by player position; which statistics repeat; the team-change penalty; the second-year gain; age curves; vacated targets |
| `best_predictor.py` | Which single input from last season best predicts next season's points |
| `second_year_leap.py` | The second-year gain, tested at four minimum-games thresholds |
| `variation.py` | How much a single player's points per game move from year to year |
| `uncertainty.py` | 95% confidence intervals on the correlations; age curves with sample sizes |
| `yardstick.py` | The startable span, computed three ways across configured research seasons |
| `span_stability.py` | How much that span moves between seasons |
| `decision_odds.py` | Correlations translated into draft-table odds: how often last season's higher scorer wins again |
| `check_team_env.py` | Whether bad teams produce more completions (they do not) |
| `rigor.py` | The one-step model, the measured gain from applying the adjustments, and player-clustered confidence intervals |
| `bonus_rates.py` | How often a player with a given season average actually clears each per-game bonus |
| `td40.py` | 40+ yard touchdown bonuses, read from play-by-play (the weekly files cannot supply them) |
| `draft_board_2026.py` | Scores retained 2026 projections and writes the top 500 → `draft_board_2026.csv` |
| `build_html.py`, `charts.py`, `md2html.py` | Render `PROJECTING.md` as `projecting.html` with inline charts |
| `missing_rows_sensitivity.py` | What the possibly-missing scoring rows would be worth |
| `audit/` | Scripts from the adversarial review: significance tests, clustered standard errors, survivorship checks |
| `team_level/` | Team-level analysis of 2010-2025 play-by-play: pass volume, coaching continuity, completion screens |

## The player file

`players_2025_scored.csv` has one row per player. The columns that matter:

| Column | Meaning |
|---|---|
| `league_ppg` | Points per game under **your** rules |
| `std_ppg` | Points per game under standard full PPR, for comparison (blank for defenders, who score nothing there) |
| `vorp_ppg` | **Sort by this.** Points per game above the last startable player at that position — the number that decides draft order |
| `pos_rank` / `pos_rank_standard` / `pos_rank_change` | Where the player ranks here versus in a normal league. A large positive change means your league values him and nobody else does |
| `week_sd` / `cv` | Week-to-week swing. Low `cv` is a steady player |
| `completions_pg`, `return_yds`, `tackles`, `sacks` | The inputs your unusual rules pay for |

Sorted by `vorp_ppg`, the top of the 2025 board was McCaffrey, Nacua, Smith-Njigba,
Robinson, Gibbs, Taylor, McBride, Chase. The first quarterback appears 14th, despite
quarterbacks occupying every one of the top 13 spots by raw points. That gap between raw
points and value over replacement is the whole argument in `STRATEGY.md`.

These are 2025 **results**, not 2026 projections. Use them to see what the rules reward,
not to predict who scores next.

## The one thing that is easy to get wrong

The recorded settings show **25** next to Passing Yards and **10** next to Rushing Yards. Those are
divisors, not point values: 25 yards per point and 10 yards per point. Points accrue per
yard rather than in whole blocks, so 312 passing yards is 12.48 points, not 12. The JSON
stores the per-yard value (`points_per_unit`), which is the number to compute with.

## The league in one line

Full point-per-reception, plus **a full point per pass completion**, 4-point passing
touchdowns, 6-point rushing and receiving touchdowns, tiered kicking, tiered team
defense, and individual defensive players.

That completion point is worth noticing. A quarterback who completes 24 passes for 312
yards and 2 touchdowns scores **44.48**, and 24 of those points — more than half — come from
completions alone. Volume passers are worth far more here than in a standard league.

## Which platform this is

**Probably Sleeper.** Identified from option labels that match Sleeper's published list word for word:
"Pass Completed", "Hit on QB", "200+ Combined Rush + Rec Yards", "Less Than 100 Total Yards
Allowed", "Special Teams Player Solo Tackle". It is not ESPN, which uses −2 for interceptions
thrown (this league uses −1), different field-goal bands, and has no "Hit on QB" category.

The new screenshots authenticate the point values. They also show that this summary contains
active/nonzero settings only: the zero 21–27 points-allowed tier is omitted between the visible
14–20 and 28–34 tiers. The same convention confirms zero for longer missed-field-goal penalties,
the middle defense-yardage bands, lower yardage milestones, a 20-carry bonus, TFL, and QB-sacked.

## Passing

| Rule | Points |
|---|---|
| Passing yards | 0.04 per yard (1 per 25) |
| Passing touchdown | 4 |
| Two-point conversion | 2 |
| Interception thrown | −1 |
| Touchdown pass of 40+ yards | 2 extra |
| **Pass completed** | **1 each** |

## Rushing

| Rule | Points |
|---|---|
| Rushing yards | 0.1 per yard (1 per 10) |
| Rushing touchdown | 6 |
| Two-point conversion | 2 |
| Rushing touchdown of 40+ yards | 2 extra |

## Receiving

| Rule | Points |
|---|---|
| Reception | 1 |
| Receiving yards | 0.1 per yard (1 per 10) |
| Receiving touchdown | 6 |
| Two-point conversion | 2 |
| Receiving touchdown of 40+ yards | 2 extra |

## Kicking

| Rule | Points |
|---|---|
| Extra point made | 1 |
| Field goal 0–19, 20–29, 30–39 yards | 3 |
| Field goal 40–49 yards | 4 |
| Field goal 50+ yards | 5 |
| Extra point missed | −1 |
| Field goal missed 0–19 yards | −1 |

## Team defense

| Rule | Points |
|---|---|
| Interception | 2 |
| Fumble recovery | 2 |
| Forced fumble | 1 |
| Safety | 2 |
| Blocked kick | 2 |
| Defensive touchdown | 6 |
| Sack | 1 |
| Special teams touchdown | 6 |
| Special teams forced fumble / fumble recovery | 1 each |

Points allowed, once per game:

| Points allowed | 0 | 1–6 | 7–13 | 14–20 | 21–27 | 28–34 | 35+ |
|---|---|---|---|---|---|---|---|
| Points | 10 | 7 | 4 | 1 | 0 | −1 | −4 |

Total yards allowed, once per game:

| Yards allowed | under 100 | 100–499 | 500–549 | 550+ |
|---|---|---|---|---|
| Points | 5 | 0 | −1 | −4 |

## Individual defensive players

| Rule | Points |
|---|---|
| Solo tackle | 1 |
| Assisted tackle | 0.5 |
| Sack | 2 |
| Quarterback hit | 0.5 |
| Interception | 2 |
| Fumble recovery | 2 |
| Forced fumble | 1 |
| Safety | 2 |
| Blocked punt, extra point or field goal | 2 |
| Defensive touchdown | 6 |
| Pass defended | 1 |
| 10 or more tackles in a game | 2 bonus |
| 2 or more sacks in a game | 2 bonus |

## Miscellaneous

| Rule | Points |
|---|---|
| Special teams player touchdown | 6 |
| Special teams player forced fumble / fumble recovery / solo tackle | 1 each |
| Punt return yards | 0.1 per yard |
| Kick return yards | 0.1 per yard |
| Fumble lost | −2 |
| Fumble recovery touchdown | 6 |
| 200+ rushing yards in a game | 2 |
| 200+ receiving yards in a game | 2 |
| 400+ passing yards in a game | 2 |
| 200+ combined rushing and receiving yards | 2 |
| 25+ pass completions in a game | 2 |

## Interpretation note

The 10+ tackle bonus is calculated from total tackles (solo plus assisted). The screen separately
names solo and assisted tackle scoring, then names the threshold simply “10+ Tackle Bonus.” The
scorer retains an explicit override for historical what-if analysis.

## Using it

```python
from score import score_player

total, breakdown = score_player({
    "reception": 9, "rec_yd": 168, "rec_td": 2, "rec_td_40plus": 1,
})
# total = 39.8
```

Field goals take a list of distances (`"fg_made": [23, 47, 52]`), and the two team
defense tiers take the game total (`"dst_points_allowed": 14`). Every other input key is a
plain count or a yardage total; derived per-game bonus keys must not be supplied. The keys are
the input `key` values in the JSON.

## Rebuild and test

```bash
python3 score.py
python3 score.py --csv
python3 analyze_2025.py
python3 bonus_rates.py
python3 draft_board_2026.py
python3 build_html.py
python3 -m unittest discover -s tests -p 'test_*.py'
```

The larger historical inputs live under `data/nfl`; refresh them with
`python3 scripts/download_nflverse.py --from-year 2013 --to-year 2025`. See `data/README.md`
and `audit-GPT/FIX_RESULTS.md` for provenance, approximations, and verification results.
