> Current2026-10-25 dated build has separate Full/Light/Mini battery slots.

## Three/four in original playoffs (2026-10-26-r21)

Original BO1/BO7 and Continue Playoffs now offer Three/Four - Teammates, save
that choice separately from the seasons and retain Two - Head to Head.
PASS10 native result cases/18 cold resumes, independent Full1/Light2/Mini3/
native4 roundtrip/reboot, Continue player edits with unchanged password;
season24/48starts,slots20,nav14/266,font14/3124,goalie88+41 and3 four-pad
season shootout results. Exact rebuild, guarded1691-byte diff, IPS verified.
Source/storage/tests: [NATIVE_PLAYOFF_PLAYERS.md](NATIVE_PLAYOFF_PLAYERS.md).
Replaces r20 everywhere with saves/remaps carried unchanged.
SHA25637feaa90d54806517026d26e67e5fb77ff4992408f3ce26c458f2302f0c38759;
CRCADE85330.

## Saved player counts, including Continue Playoffs (2026-10-26-r20)

Players is now reachable and editable in a season's Continue Playoffs menu.
Choose One/Two/Three/Four - Teammates; each Full/Light/Mini slot remembers its
own count across slot changes and cold boots, in both league and playoffs.
The optional PC/count/complement record at logical SRAM1EF0..1EF3 follows
existing snapshots/checksums; old saves remain compatible. Without an adapter,
a saved Three/Four runs as Two and returns when the adapter is restored.

New seasonplayers.s at2EB000 (622bytes), cursor hookF7782, seasonmenu2002 and
slots1556. Only Players is unlocked; club/results/tree/statistics stay intact.
PASS24 native player-choice cases/48 cold match starts, mixed Full4/Light2/
Mini3 roundtrip and reboot,18 played playoff OT/shootout result/save cases,
goalie88+41,slots20,navigation14/266,finished9/108,club labels18/240,font14
cases/3124 calls/zero wrong fonts. Exact rebuild, guarded3579-byte diff,
original-base IPS230941 bytes verified. See [SEASON_PLAYERS.md](SEASON_PLAYERS.md)
for source, RAM, storage and reproduction. Replaces r19 everywhere with matching
save/state/remap contents preserved.
SHA256a67ecaffe77beb7c13071caee9f507767a4efb7471af3c9ff9487d3b3fb9ae3e;
CRC086E5C8E.

## Four-pad goalie hold, B request and Y control (2026-10-26-r19)

The goalie patch now handles all four physical pads in cooperative 4-Way Play:
AI holds on pickup, B requests a pass to the requester, Y explicitly takes the
keeper. Native P3/P4 reuse D4=0/2 with temporary control banks; requests now
retain physical IDs 0/2/4/6. Short hardware B taps survive the catch/recovery;
first valid request wins, and pause/replay input cannot queue an outlet.
Also fixed: switching from Four into a One-player season could still start
four humans (stale TM), and shootouts left pads 3/4 active with only two actors.
Ordinary two-pad switch/Y code is byte-identical. No SRAM format change.

PASS 88 adapter goalie +41 ordinary goalie cases; 7 two-match lifecycles
(14 games, 14 cold boots, P3/P4 movement, pause/resume, menu circles); a complete
natural two-game Mini run produced 27 keeper passes/17 receptions. PASS 50
OT/shootout cases across ordinary/three/four pads and all playoff formats;
slots20, adapter navigation14/266, completed-season9/108, club labels18/240,
menu font. Exact rebuild, guarded2870-byte diff, original-base IPS verified.
Full source/RAM/test details and the known RAM-only playoff player-count
limitation: [FOUR_PAD_GOALIE.md](FOUR_PAD_GOALIE.md).
User-requested private r18 ZIP backup and hash manifest verified. r19 replaces
r18 locally/site/Bodin's same playlist item, carrying save/state/remap hashes.
SHA2567262f8e4a39d6970b660e24803f1d7212b29aed26a933202296b788aed8d9ec6;
CRC D82B8F78.

## Completed slots open results (2026-10-26)

See Old Full/Light/Mini Season replaces Continue for finished slots. Start
opens saved results; after leaders, view again, start a new selected-slot
season (Xbox X+B confirms, Start cancels), or return. Finished slots retain
the saved club and show Season finished. Fixes fictitious/unrecorded mirror
matches; r30 fixed only the stale Team text. finished.s1104 at2E6000, Start
hook F843E before settings save; seasonmenu1966, results3926 with +8 browser
export and both-pad Start-release wait. No SRAM format change.9 native flows /
108 checks,18/240 menu,12/228 navigation,347 browser pass; source rebuild,
guarded3547-byte diff and IPS verified. Actual user saves unchanged. New dated
2026-10-26 replaces25-r30 locally/site/Bodin, remaps carried. See
FINISHED_SEASONS.md and release.json for full implementation and hashes.


## Completed-season team label (2026-10-25-r30)

Main-menu Team text now follows native Home when the completed season has
FORCED=0; USERW is used only for locked/pregame season selection. This fixes
logos changing while the old name stayed. Active away games retain the user's
club. No new RAM/save field; seasonmenu1894 bytes. 18 native cases / 798 club
checks, 12 navigation cases / 228 checks, exact rebuild and IPS pass. Current
user SRAM stays byte-identical after ordinary menu selection. Statistics code
unchanged. New r30 replaces r29 everywhere, with remaps/saves/states carried.
Full cause, source, evidence and hashes: MENU_TEAM_LABEL.md and release.json.


## Statistics alignment and footer spacing (2026-10-25-r29)

Club skater and goalie headings move one character left to match the values:
column ends23/27/31/35. Leader column ends26/30/34/38 remain correct. Both
screens move headers to row6 and all18 skater rows to7..24, leaving row25
completely free of text above Xbox help26 and footer27. Page2's goalie block
also moves up one row. All players, numeric positions, inputs and saves stay
unchanged. Orange selection now uses row6 C600 and club PTS29/G23/A27;
leaders use +3 columns. seasonend3926 bytes,2E3000..2E3F55,170 bytes below trail.
347 native browser checks pass, including selected glyphs, both planes' empty
spacer row, boundary redraw counters and unchanged SRAM. Fresh production-safe
champion preview, exact rebuild, guarded591-byte UI/title/checksum diff and
original-base IPS reconstruction pass. Source/site/screenshot/docs updated;
r29 replacesr28 locally, Downloads, site and Bodin's same item with saves/remaps
carried. Private evidence:work/stats-spacing-20261025-234630/.
SHA256bd4eb6465d81e72b7a07dcffcee1093719a370eb049307ecf5d100007946bcc7,
CRC8170380A. release.json is authoritative for the current version.


## Xbox sorting-button help (2026-10-25-r28)

Both end-of-season statistics pages now explicitly show
"Sort: Xbox B / Mega Drive C" above the original footer. This is the same
sorting action on the existing six-button remap; the input code is unchanged.
Retains r27's orange G/A/PTS heading and no page-boundary redraw. Module3928
bytes,2E3000..2E3F57,168 bytes below trail.347 native browser checks, fresh
production-safe champion preview, exact rebuild and original-base IPS pass.
Guarded UI/title/checksum diff:1896 bytes vsr27,3215 vsr26. Details and memory
map:STATS_DISPLAY.md. Newr28 replacesr27 locally, Downloads, site and Bodin's
same item; saves/remaps carried.
SHA256990a751d77fe5b7a4b2fdb46c606ce36e9a604ba34ffe6a75e1c3346be505e81,
CRCF96D9E75. release.json is authoritative for the current version.


## Statistics heading colour and page boundaries (2026-10-25-r27)

Selected G/A/PTS is orange in the end-of-season club browser and scoring
leaders. Up at page1 / Down at page2 no longer redraw/fade the same screen.
seasonend3880 bytes at2E3000..2E3F27; cloned glyphs use disabled-window VRAM
B040..B09F, not the live front plane. No SRAM or gameplay change.347 native
browser checks, VRAM glyph/palette assertions and redraw-count regressions
pass; paired capture changes only selected glyphs. Exact rebuild, guarded3170
byte UI/title/checksum diff and IPS verified. Fresh local Dallas champion
preview tested against production code; battery progress/remaps carried.
Replacesr26 locally, Downloads, site and Bodin's same item. STATS_DISPLAY.md
has implementation, memory map and reproduction details.
SHA25668f8f60bd60e9058eb2a9d277ada8672ca5bca8fa66b05dfc23b9a2b368ce7e6,
CRCB7B97FA0. release.json is authoritative for the current version.

> Left/Right switches without resetting other leagues or their playoffs. Restart
> Season resets only the selected slot and works from its playoff pre-game menu.
> Legacy saves migrate automatically. SRAM is now32KB logical/64KB physical;
> older8KB layouts below describe the active window retained inside that file.
> Full source, exact addresses and20-case regression:EXPERIMENTAL_BUILD.md.

> Latest dated Season - jdc build (2026-10-24): **New Full Season** starts 84
> games; Continue keeps old 82-game saves at 82. Light is 31, Mini is 8.
> Full playoffs are best-of-seven; Light/Mini are single-game. The generic
> pre-game restart entry is **Restart Season**. See EXPERIMENTAL_BUILD.md for
> current source, hashes and validation. Earlier 82-game release notes follow.

# Project MEGAdriveHARDJOB — a season mode inside the ROM (option A)

Current 2026-10-24 test profile: New Season Mini (8 games, single-game playoffs),
Game-row first-opponent selection (before game 1 only, since 2026-10-25-r2) with a persisted unplayed-round mask, instant match
simulation and further statistics fixes. All source and a full reproducible
builder are on the site. See [EXPERIMENTAL_BUILD.md](https://dela.jdc.se/nhl94/notes.html#EXPERIMENTAL_BUILD.md) for
current behavior, addresses, format and test evidence; older sections below are
the chronological development notes. Public ROM/IPS downloads remain the prior
release.

Goal: a full regular season with standings, then playoffs, built into the ROM itself so it works in any emulator and on
real hardware. Working copy: `~/.config/retroarch/roms/claude/MEGAdriveHARDJOB/NHL94-season.bin` (copy of the patched ROM,
md5 bc92d62b…, 2026-10-21). Not installed for play; the normal ROM is untouched.

**2026-10-23 update:** the published season ROM and installed season ROM now include New Season Light. It plays 31 rounds,
one meeting for every pair of the 32 clubs, then a single-game playoff for each pairing. Header +10 stores 31 instead of
82. The game's New Playoffs mode (Play Mode 2, `$CEEA=7`) supplies the single-game progression; the 82-game season
continues to use Play Mode 3 (`$CEEA=0`, best of seven). Both use the same 16-team season seeding. Previous installed
season ROM is backed up next to it as `NHL 2026 - 94 Edition v5.1W - Seasons.bin.bin.before-light.bak`.

The menu entry after the final league game now reads **Continue to Playoffs**. Start opens the game's cup-and-bracket
screen with the season's qualified teams. This was verified with jdc's actual save after match 31, including all 16
teams and Edmonton's first pairing against Los Angeles. After a playoff game the entry reads Continue Playoffs.



## Goalie catch input and all-team end statistics (2026-10-25-r26)

B held across a native goalie catch now reaches the outlet request before the
previous switch's HELD latch. Reproduced r25's skater4/1→0 then goalie5 catch,
with no pass; r26 serves the pending B in One and both Teammates pads. No B still
means hold. switch510/outlet750, no new RAM/save field. After Season Summary,
Left/Right cycles32 teams; A changes combined/league/playoffs tab, C sorts,
Up/Down pages; Start opens leaders, B there returns to the selected club.
seasonend3618 bytes; BTEAM FFE6FC/SCREEN FFE6FE, actual TEAM and all saves intact.
41 goalie cases,682 native browser checks with exact source totals/unchanged
SRAM,14/266 navigation, clean CPU Mini0–1, exact rebuild/guarded3313-byte diff/IPS
pass. Full cause, controls, sources and fixtures:GOALIE_AND_STATS.md.
SHA2564b42e4b6fa9079bb3bdb40376ff473bed4a0ef6116d9df98f5fef123d1047c8a,
CRC4ACB9735. Read release.json for the current version; older hash claims below
refer to their historical snapshots.

## Local penultimate-game test fixture (2026-10-25)

At jdc's request, installed three fresh randomized-club test seasons locally:
Full Seattle next83/84, Light Utah next30/31, Mini NY Islanders next7/8. Real
native simulation recorded82/29/6 league rounds (1872 matches in total), with
consistent team/player/keeper totals and checksums. Native slot roundtrip/reboot
checks preserve each season. Single-player preference retained; all three can
be changed to Teammates in the menu. User explicitly declined an additional
backup for this disposable test setup. Bodin's saves and downloadable ROMs do
not contain this local test progress. Evidence/commands/native validation:
`work/penultimate-three-20261025-113235/`. Full selected and launched from Downloads.

## Correct player-card portraits (2026-10-25)

The dated build now maps all832 cards to the reviewed player identity, instead
of retaining the pre-roster-update face.702 slot associations reuse existing
portraits,123 use new real NHL photos and7 have an explicit neutral silhouette.
586 pointers repaired;130 new874-byte records at2B0000..2CBBD3. Shared palette,
all gameplay code, roster/stat/save data and SRAM layout are unchanged.
Source: experimental/player_cards.py, prepare_player_cards.py, player_cards/
manifest.json and native verify_player_cards.py. Builds are offline; optional
--no-player-cards reproduces the preceding OT/shootout ROM exactly. Unknown
identities fail rather than assigning an unrelated face. Detailed IDs, sources,
seven missing photos, aliases, ROM format and validation: PLAYER_CARDS.md.
832 native lookups/decodes,28 paired WSH and28 EDM screenshots, byte-identical
rebuild and clean CPU1–4 pass. Current SHA256
0e5c8ee01f69d7219456d414a15b58fe593c0b957c5630886d138027822d0461,
CRC694C73C2. Dated filename/save matching and earlier releases are retained.

## Ten-minute OT, then shootouts (2026-10-25)

The dated build now uses10-minute sudden-death OT in played league and playoff
games, followed by three shootout attempts per club, early clinch and paired
sudden death. In Teammates, P1/P2 alternate their club's attempts on either side.
The native shootout panel displays attempts/score; the winner adds one final
team goal, without adding player/keeper goals or shots. League OTW/OTL and
native BO7/single-game playoff progression work normally. No SRAM layout change.
Full implementation,900-byte2AC000 code, FFF090..0AF reservation, nine native
hooks, Three Stars/late-penalty guards and18 integration cases are documented in
EXPERIMENTAL_BUILD.md. Source: experimental/shootout.s, verify_shootout.py and
testcases/shootout_hook.py. Save reader accepts shootout deciders/empty-net GA.
Overtime snapshot SHA25627349dff9ef46db0b1115f607535c72e03573a91896af03fa25df1b16f6443f7,
CRC57008214. Earlier releases are historical; dated filename/save matching stay.

## What exists already (found 2026-10-21)

- **Battery RAM (SRAM):** 8 KB, declared in the header (`RA`, $200001–$203FFF, odd bytes). Bank switch with `$A130F1`
  (1 = SRAM on, 0 = ROM).
  - At boot the hack copies all of SRAM to RAM `$FF0000–$FF1FFF` (`$1F9A00`, called via `$1A050`). A checksum is kept in the
    last two bytes (`$1FFE/$1FFF`, check `$1A14E`, write `$1A206`). Bad checksum → SRAM cleared (`$1A19C`).
  - Write: `$1A1E4` → `$1F9B28` (d0 = SRAM offset, d1 = byte count, a0 = source). Read: `$1A244` → `$1F9B5A` (a0 = destination).
  - In use by the hack: `$0000–` about `$0DA0` (User Records, playoff data; e.g. `$0D20` 128 bytes from `$FFCF36`, `$0DA0` from
    `$FFD45A`), `$1EF6–$1FF5` (256 bytes from `$FFD076`), checksum `$1FFE`. **Free: about `$0E00–$1EF0` (≈ 4 KB).**
- **Menu:** Play Mode has Regular Season (one game), New Playoffs, New Playoffs/7 game, Continue Playoffs, Shootout.
  The playoff code already has a bracket ("Semifinals", "Finals", "Champions" at `$17706`).
- **Game stats in RAM:** two team blocks, `$FFC6CE` and `$FFCA32` (= +$364). Goals at `+$0C`, goals per period at
  `+$342 + 2·period`. Goal routine `$1481E`. Home/away team index `$FFC331/$FFC333`. Referee messages "Period Over" /
  "Game Over" at `$18E50` (indexed table, caller not found yet).
- **Test rig:** MAME keeps SRAM per ROM name; `run.sh` now clears it unless `KEEP_NVRAM=1`.

## Plan

1. **Result capture** — find the end-of-game point (the "Game Over" call or the switch to the post-game screens) and hook it:
   add the final score to a standings table in free SRAM (`$0E00–`): per team GP, W, L, OTL, GF, GA (32 × 12 bytes) plus
   game number. Test: two CPU games in a row with `KEEP_NVRAM=1`, check SRAM.
2. **Schedule** — generated in the ROM from a seed (e.g. 82 games for the user's team, other games simulated by a quick
   formula from team ratings when the user's game is played), or a fixed table in ROM built by a tool.
3. **Menu** — a "Season" choice in Play Mode: new season / continue, pick your team; the next opponent is set automatically.
4. **Standings screen** — reuse the hack's text drawing (menus, "Other Scores" panel) to show the table.
5. **Playoffs** — seed the top 16 into the existing playoff mode.

Hardest parts: 3 and 4 (new screens in 68000 assembly). 1 and 2 are straightforward.

## Step 1 — results into battery RAM (done 2026-10-21)

- Code: `tools/season/season.s` (GNU as, 68000), assembled with `tools/m68kasm.sh` (SGDK docker image
  `ghcr.io/stephane-d/sgdk`, `m68k-elf-as/ld/objcopy`). Built into the season ROM with
  `python3 tools/season/build_season.py <patched rom> <out>`; code at `$2FF400` (324 bytes), hook `$7C2E`.
- End of game: `$7BE0` compares `$C6DA` (home goals) with `$CA3E` (away); not tied → `$7C16` game over
  (`bset #0/#6,$C2EA`, `jsr $FF88E`, message 4). Tied after overtime (`$C466` = 3) in Regular Season (`$D048` = 0) → also game over.
- Highlights from other games during intermissions run the same path; they have bit 4 of `$FFC2EA` set and are skipped.
- **Don't call the hack's `$1A206`** (checksum): it reloads all 8 KB of SRAM into RAM `$FF0000`, which the game uses for
  replay data → "ADDRESS ERROR" on the post-game screen. The hook updates the checksum itself (old sum − old block + new block).
- Table: SRAM offset `$1000`, 'SESN', games played, 32 × (GP, W, L, OTL, GF, GA, T). Read it from a save file with
  `python3 tools/season/readsram.py file.nv` (MAME `.nv` = 16 KB with data on even bytes).
- Tested: 2 CPU games in one run (EDM–CBJ 4–1, 4–2), restart with the battery RAM kept, a third game (CBJ won in overtime):
  EDM 3 GP, 2 W, 1 OTL, 10–6; CBJ 3 GP, 1 W, 2 L, 6–10. Checksum OK after every write, no crashes, highlights not counted.

## Step 2 (part) — the rest of the league plays too (done 2026-10-21)

- After every finished game the other 30 teams are shuffled into 15 pairs and each pair gets a simulated result
  (`simleague` in `season.s`). Goals per team: 8 tries, each scores if `rnd(256) < 60 + strength/4` (game RNG `$11086`).
  A tie goes to overtime: one extra goal, coin flip, the loser gets an OT loss. So every team has the same number of games.
- Strength 0–255 per team is computed by `build_season.py` from the ROM's ratings (top 12 skaters + best goalie) and
  written to `strength.inc`.
- Tested: after 2 games all 32 teams had 2 GP with plausible scores; checksum OK; the game returns to the menu normally.

## Dated build: season team logos match the fixture (2026-10-24)

Full, Light and Mini now update both main-menu logos immediately when Team is
changed. Previously both could show Edmonton while Game already said vs DET;
actual game clubs were already distinct. The shared menu now forces the opponent
before native logo loading, including One/Teammates changes in a continued away
game. Schedule, saved club, opponent selection and season data are unchanged.
See EXPERIMENTAL_BUILD.md for reproduction, ordering and all-format regression.

## Dated build: CRT in player statistics (2026-10-24)

Only fully wound-up forehand slapshots (including forehand one-timers) can now
crit; quick wrists/backhands do not. A crit plays the selected Twin Cobra
digital effect13 (local clip19) once; see CRITICAL_SLAP.md.

Actual critical shots now briefly flash the puck red/ordinary. This is a visual
cue for the same power bonus; CRT values, chance and all saved statistics remain
unchanged. Possession/dead puck/new shot/expiry ends the flash. See the complete
experimental guide for renderer hooks and tests.

The dated Season - jdc build adds CRT (0..100) beside SOG on both skater pages.
It is a permanent player attribute from ROM, not a count of critical hits and
not part of the SRAM record. CRT100 gives10% chance per full forehand slapshot of25% extra
power;50 gives5%. Better offensive ratings produce higher values. Goalies retain
their native statistics, and all save fields/totals are unchanged. The native
Team Roster attribute selector also has Crit Chance. The statistical league
simulator below does not run shots and keeps its existing goal model. Exact
source/table/formula/test details are in EXPERIMENTAL_BUILD.md.

## Simulation audit — scoring and line selection (2026-10-24)

The other fixtures use a statistical result generator, not CPU-versus-CPU
emulation. `goals` in `season.s` uses eight independent RNG trials with threshold
`60 + (strength >> 2)` out of 256. A tied fixture gets one deciding OT goal.
Since 2026-10-25-r4 the number of scoring tries (4/6/8/16) and shots per simulated game follow Per. Length (5/7/10/20 min), calibrated from CPU-vs-CPU games; see EXPERIMENTAL_BUILD.md. Team strength uses the twelve highest-rated skaters plus the best goalie.
`dist` assigns goals/assists/shots using `pw_goal`/`pw_ast` weights across all
roster skaters, with zero goalie weights. It does **not** restrict simulation to
the selected first line or consult the Line Changes setting, fatigue, actual
ice time or the opponent's defensive/goalie ratings. This is a realism limitation,
not evidence of duplicated goals. User-played fixtures record actual game stats.

Audit of current dated ROM SHA256
`bb08ce17e7c760920dbded861387bfbce5711c581fad495ef03bcc0056db3322`:
a fresh headless eight-round Mini season generated 120 other-team fixtures,
675 goals across 240 team appearances (2.8125 per team/match). Eleven of those
240 team scores were six or more; maximum eight. Team/player/goalie totals,
equal GP and SRAM checksums passed after every round. An earlier complete
84-round fixture averaged 2.8404 goals per team/match across the whole league.
These tests did not reproduce excessive average scoring; the user's specific
reported scores remain unconfirmed. No gameplay, download or live save was
changed. Private evidence: `work/season-scoring-20261024-200805/report.json` and the
per-round SRAM fixtures, hook, commands and checks beside it.

## Step 4 — "Season Standings" screen (done 2026-10-21)

- `standings.s` at `$2FF800`. Replaces "Other Scores" in all 7 pause / post-game menus (entry = word $14, 18 chars, long handler).
- Built like the game's own screens: `jsr $9BD8` (panel), `$11B92`/`$119B8`/`$11DE2` (title, inline text), then one string
  printed with `$11A48` (a1 = word length + text). Text codes: `F8 06 03 col row` start, `FC row`, `FD column`,
  `F9 01`/`F9 00` colour on/off, `[` and `]` show as arrows. Pad strings to even length *before* `F9 00`.
- Pad loop like Record Holders: `jsr $80BA`, `jsr $A41E` (d3 = pressed, d1 = direction), `jsr $11318`;
  d3 bit 7 Start → `jmp $9CD8` (back to the menu); d1 bit 3 right / bit 2 left; d3 bits 6+5 = A+C.
- Sorted by points, then goal difference. 16 teams per page, Left/Right for the page, **A+C starts a new season**
  (clears the table and fixes the checksum).

## Step 3 — your team and the schedule (done 2026-10-21)

- Season header in the SRAM block: `+6` your team + 1 (0 = none), `+8` your games played (= next game index), `+10` length (82).
- **Starting a season:** the first Regular Season game a human plays (Players One - Home / One - Visitor) sets your team.
  Before that (jdc: "you should only be able to pick one team") only Home = your team can be changed; Away follows as
  game 1 of that team's schedule and Players is locked too (`FORCED` = 2). During the season all three are locked (`FORCED` = 1).
  Small glitch: the away logo can show the previous opponent for one redraw after Home changes. A+C on the standings screen clears the season, so the next game starts a new one.
- While a season runs, only Regular Season games with your team count (`season.s`); demo/other games are ignored.
- **Schedule:** game k: opponent = (your team + `sched[k mod 31]`) mod 32, a fixed shuffle of 1..31 (`sched.inc`, seed 1994);
  even k at home, odd k away. 82 games = every opponent 2–3 times.
- **Menu lock** (`seasonmenu.s` at `$2FFD00`): hook `$F7BAA` (value printer, runs for every row on every redraw) calls `force`
  before fix 8: reads the header from SRAM and writes Home `$D04C`, Away `$D04E` and Players (`$D04A`, only if it is
  One - Home/One - Visitor) for the next game. Hook `$F7ABA` (value writer) ignores Left/Right on Players/Home/Away while forced.
- Tested: season set to Edmonton → menu shows EDM home vs NYR, Left/Right on Home/Players do nothing; the game (1–1 after OT)
  was recorded, game counter 1, and the menu then showed game 2: at Calgary. Test helper: `season/mknv.py` makes a MAME
  battery file with a season for a given team.

## "Whole Season" in Play Mode (jdc's idea, 2026-10-21)

- Regular Season is a normal single game again (free choices, nothing recorded). The season is its own Play Mode choice
  **Whole Season**, after Shootout (Right from Shootout, Left from Regular Season).
- Not a real 6th value: the game sees Regular Season (`$D048` = 0) and header `+12` = 1 in battery RAM says Whole Season.
  `seasonmenu.s` shows "Whole Season" for row 0 and maps Left/Right. The menu writes every row again after a change
  (d7 = `$FFFF00xx`), so only a real change of value counts.
- In Whole Season: Players = One - Home / One - Visitor and Away follow the schedule (locked); Home = your team can be picked
  until the first game is played, then it is locked too. Only games played in Whole Season are recorded.
- Menu in Whole Season (jdc 2026-10-21: "One or Two - Teammates is enough, remove the Away row"):
  Players shows **One** (= One - Home / One - Visitor by the schedule) or **Two - Teammates**; Left/Right toggles.
  Two - Teammates always controls the home team, so in co-op every game is played at home.
  (2026-10-23: a co-op first game now starts the season with the home team — it used to record only, so the season never
  started; and Players stays switchable while the season runs, since the game boots with One.)
  The "Home" label reads **Team** and always shows your team; the "Away" label and value are empty (the opponent is in the
  logos at the top). Hooks: `$F7C72` (team rows, `teamrow`), `$F80BC` (label loop, `labelhook`, labels redrawn via `$F8070`
  after Play Mode changes). The cursor still stops on the empty row (the cursor position wasn't found in RAM).
- Lessons: the menu writes **every row again** after a change (d7 = `$FFFF00xx`) — a hook that changes a value on those
  rewrites makes the menu loop forever; and the value printer needs **a0 = `$FFD048`** going into `$20961A` (clobbering it
  crashed into the exception loop at `$18CD8`).
- Code moved to `$2F8000` (season.s), `$2F8800` (standings.s), `$2F9000` (seasonmenu.s); `$2FF400–` was too small.

## Locked settings, "Game" row, scoring leaders (2026-10-21)

- jdc: after the first game nothing should be changeable. While a season runs (`FORCED` = 1) the value writer blocks every
  row except Play Mode (so you can still switch to a normal Regular Season game; the season stays saved).
- The empty Away row is now **Game**: "2/82 at CGY" / "1/82 vs NYR" (next game, home/away, opponent). Not changeable.
- **Player stats:** SRAM `$1220`, 32 teams × 26 roster slots × (goals, assists) bytes. Header `+14` = 1 when valid.
  - Real games: the game's team stat blocks hold per-player bytes indexed by roster slot: goals `+$B4`, assists `+$CE`,
    shots `+$E8`, checks `+$102` (home `$C6CE`, away `$CA32`). **Slot 0 of the goals array holds the team total** — skip it
    (slot 0 is always a goalie).
  - Simulated games: each goal gets a scorer (weight ≈ (off+acc+pwr+stk)²/6) and up to two assists (82 % each,
    weight ≈ (pas+off+stk+agi)²/6) from `pweights.inc`, generated by `build_season.py` from the ROM ratings.
- **Scoring Leaders** screen (`leaders.s` at `$2FA000`) replaces "Record Holders" in all 9 menus: top 32 by points (goals
  break ties), 16 per page, names read from the team records via the table at `$30E` (max 16 characters).
- Where to find it: from the main menu press Start → (Matchups screen, Start again) → the pre-game menu has
  Start Game / … / Season Standings / Scoring Leaders. Also in the pause menu and after the game.
- Layout now: `season.s` `$2F8000` (~2.9 KB incl. weights), `standings.s` `$2F9000`, `seasonmenu.s` `$2F9800`, `leaders.s` `$2FA000`.
- Assembler gotcha: in GNU as `02:` is the same local label as `2:` — use named labels for anything not tiny.
- Tested: season EDM, CPU game EDM–NYR 1–1 recorded (McDavid goal, Hyman assist), 92 goals / 108 assists league-wide
  after one round, checksum OK; Per. Length can't be changed after the first game; leaders and standings open from the
  pre-game menu.

## Stats from the main menu (tried 2026-10-21)

- jdc asked for standings/stats straight from the main menu. Main menu facts: loop at `$F7464` (`bsr $F8304` reads the pad:
  d1 bit 7 Start → `$F843E` → `jmp $17D80`; bits 1/0 Down/Up → cursor `d7` moved by `$F76D4` with skip rules; bits 3/2
  Right/Left → `$F75AE`). The whole menu is built by `$F739E` (called from `$17312`; labels `$F8070`, values `$F7B20`).
- **Tried:** Right/Left on the Game row jumping into the standings/leaders screens and returning with `jmp $F739E`.
  Returning works, but the screens draw garbage over the main menu: they need the scoreboard screen setup (VDP/planes/font)
  that the pause and pre-game menus have; loading `$A892A` with `$11738` was not enough. Dropped.
- **Done instead:** after Start, `$17312` → `jsr $FBB88` → `jsr $FCC76` = the Matchups screen. In Whole Season that call
  (`$1732C`) is skipped, so Start goes straight to the pre-game menu: Start Game, Change Goalie, Edit Lines, Team Roster,
  Season Standings, Scoring Leaders, Player Cards. Tested headless.

## "New Season" in the pre-game menu (2026-10-21)

- jdc: a menu choice to start over after games have been played. The Regular Season pre-game list (`$19A84`, pointer at
  `$131CC` entry 0) is copied to `$2FAC00` with an 8th entry **New Season** → `newseason.s` at `$2FA800`.
  Menu lists: 8-byte header `00 04 FE 05 00 04 FE 04`, entries word `$14` + 18 chars + long handler, end `00 04 FF 00`.
- The screen asks first: A+C = yes (clears team, results and player stats, keeps Whole Season chosen, fixes the checksum),
  Start = back. Yes → `jmp $172FA` (fade, clear game state, build the main menu — `$172F0` would show the title screen first).
- A+C on the standings screen used to do the same without the question; removed 2026-10-22 (jdc: confusing, New Season is in the menus).
- Tested: Start backs out unchanged; A+C → main menu with Game 1/82 and the team choosable again; header cleared, checksum OK.

## Regular Season is the season mode; Continue / New Season (2026-10-21)

- jdc: no separate Whole Season; Regular Season *is* the season. Play Mode row 0 shows **Continue Season** when a season
  is saved and, one step right, **New Season**; with no season only **New Season**. Header `+12` = 1 means "New Season
  chosen": the menu is then like before the first game (team choosable) and the **next finished game clears the old season**
  (`season.s`) and starts the new one. Going back to Continue Season before playing keeps the old one.
- While New Season is chosen (+12 = 1) the standings, leaders and team screens show an empty season (jdc saw the old
  season's table after choosing New Season — fixed 2026-10-21).
- Every Regular Season game is recorded (no separate on/off flag any more). The "New Season" entry in the pre-game menu
  (asks first) still clears at once.

## Player stats format 2: goals, assists, shots (2026-10-21)

- SRAM `$1220`: 32 rows of 100 bytes (team stride 100), slots 1..25 × (goals byte, assists byte, shots word). Header
  `+14` = 2 when valid; anything else → all rows cleared first. RAM was too small for the whole block (and `$FFF000-$FFF005`
  are the blue-line variables), so `season.s` reads/writes one row at a time (`rowload`/`rowsave`, row buffer `$FFF100`) and
  keeps the checksum change in `DELTA` (`$FFF0F0`).
- Real games: goals `+$B4`, assists `+$CE`, shots `+$E8` per slot from the game's team blocks (slot 0 = totals, skipped).
- Simulated games: 22–37 shots per team, each goal is a shot by its scorer, the rest by the scorer weights; the overtime
  goal gets a scorer too, so player goals always add up to the team's GF.
- `readsram.py` checks: player goals = team GF for every team, assists ≤ 2 × goals, shots ≥ goals per player.
- **Standings → team:** Up/Down moves an arrow (`]`) over the teams (edge-detected: one step per press — the pad routine
  repeats held directions), C opens `teamstats.s` (`$2FB000`): the team's skaters sorted by points with #, name, G, A, PTS, SOG,
  16 per page; Start returns to the standings. Goalies are left out (no goalie stats yet).
- Leaders (`leaders.s`) read the rows into 800 keys (points×256 + goals + 1) at `$FFE400` and pick the top 32.
- Tested: 5 CPU games from an empty battery file (4 recorded in time): 64 team games, 372 goals, 513 assists, 3730 shots,
  0 problems in the checks; standings arrow, C → Seattle's players (2 pages), Start back.

## Divisions (2026-10-21)

- Season Standings shows one NHL division per page: 0 Atlantic (BOS BUF DET FLA MTL OTT TBL TOR), 1 Metropolitan
  (CAR CBJ NJD NYI NYR PHI PIT WSH), 2 Central (CHI COL DAL MIN NSH STL UTA WPG), 3 Pacific (ANA CGY EDM LAK SJS SEA VAN VGK).
  Table `divs` (per team ANA..WPG) in `standings.s`. Opens on your team's division; Left/Right = division (wraps),
  Up/Down = team (0..7 in the division), C = that team's players. Conferences for the playoffs: East = 0+1, West = 2+3.

## Build and site (2026-10-21)

- `tools/build.sh` now builds the plain patched ROM, then the season ROM (`season/build.sh`), then the team pages and the
  IPS — **the IPS on the site is the season ROM** (base v5.1W → `MEGAdriveHARDJOB/NHL94-season.bin`). `teams.json` has
  `rom_md5` (season ROM) and `plain_md5`.
- Season code layout: `season.s` `$2F8000`, `standings.s` `$2F9000`, `seasonmenu.s` `$2F9800`, `leaders.s` `$2FA000`,
  `newseason.s` `$2FA800`, pre-game list copy `$2FAC00`, `teamstats.s` `$2FB000`.

## Playoffs locked like the season (2026-10-22, after jdc's test of game 82)

- jdc after game 82: the menu showed New Playoffs/7 games but everything could be changed, and after a power-off before the
  first playoff game the playoffs were gone (the flag was set once; the game only saves its playoff state after a game).
- Now the season menu checks first, on every row: a valid season, not New Season, your games ≥ the length → `spcheck`
  (playoffs.s +20) with the flag at SRAM `$1210`: 0/1 = set up New Playoffs/7 games (Play Mode 3, Home = your team,
  Players 7 = One / 6 = Two - Teammates — the playoff modes use 7 − Players as their player count, Regular Season's 1 broke
  the saved state — and `$17D16`); 2 = a playoff game was played: Continue Playoffs (Play Mode 1, `$17CA0` reads the game's
  saved state); 3 = over (after a game the game sets Play Mode 2/3 instead of 1) → menu free. `season.s` calls `played`
  (+24) after every playoff game (flag 2). FORCED = 3: every row but Play Mode is locked; Play Mode Left = New Season
  (header +12 = 1, menu free, like the season); Away shows your opponent of this round (`$CEF4` + 0/16/24/28 by `$CEEC`,
  slot `$CEEE >> round`, xor 1).
- Tested in Genesis Plus GX from jdc's save after game 82: New Playoffs, Edmonton vs Vegas (2nd vs 3rd Pacific), locked;
  a playoff game (score poked so it doesn't go to sudden death) → flag 2, Play Mode 1, password saved; power-off and on →
  Continue Playoffs, Edmonton vs Vegas, Players One, locked; Left → New Season, free. Demo games don't count in the game's
  playoffs (`$180FC` returns when Players = Demo).

## Playoff band in Season Standings (2026-10-22)

- jdc: mark the teams that make the playoffs in Season Standings too, with a light background.
- That screen is drawn in the **window plane** (reg 18 = `$1C`: the whole screen; `$B00C` = `$F000`, text code `F8 attr 03 …`),
  pattern and text in the same plane; plane B behind is empty. The text's background is its own colour 13 (palette 2,
  `$0180`). Palette 3 is palette 2 with a darker colour 13. So a team that is in (QUAL from `playoffs.s`) gets its row
  printed with attribute 7 (palette 3) instead of 6, the row is first filled with 39 spaces (no gaps between the fields),
  and colour 13 of palette 3 is set to light blue `$0E42` in CRAM and in the palette buffer (`$BD28+$7A`, the fade-in reads
  it); the game's value is kept and put back when the screen is left (Start, or C to a team screen).
- Plane codes on menu screens: 01 = `$B004` (`$E000`, plane A, hidden under the window), 02 = `$B008` (`$C000`, plane B),
  03 = `$B00C` (`$F000`, window).

## Division table blank after a human game (2026-10-22)

- jdc's RetroArch: after his own game the table showed only the row colours, no banners, ranks or numbers. MAME and a CPU
  game were fine. Reproduced in Genesis Plus GX with `gpgx.py` (human game, 6-button pad): the "Exit Game" pause menu leaves
  the front plane's horizontal scroll at `$FE6E` (VRAM `$0000`, reg 13 = 0), and the table had turned off the VBlank sprite /
  scroll upload (`$C2E8` = 0). Fix: the table writes hscroll 0 (VRAM `$0000`) and vscroll 0 (VSRAM) itself, both window
  registers (`$9100`, `$9200`), and clears `$BF78` bit 2 before the fade-in.

## Standings and table polish (2026-10-22, later)

- Season Standings: title "Standings" (the long title ran past the box), no A+C shortcut and no "A+C: new season" text.
- Division table: the help line on the bottom row (27), under the last team.
- jdc's pad in RetroArch: X = Genesis A, A = B, B = C (Genesis Plus GX default with an Xbox-style pad).

## Playoffs after the season (2026-10-22)

- jdc: after the season the game's own playoffs, best of 7, with the teams that made it; and a colour for the teams above
  the line in the division table.
- **Format (NHL):** 16 teams, top 3 of each division + 2 wild cards per conference (best of the rest). East = Atlantic +
  Metropolitan (tree slots 8–15, right side), West = Central + Pacific (slots 0–7, left). The better division winner of a
  conference meets the second wild card, the other winner the first; 2nd meets 3rd. Tree pairs (0,1)(2,3)(4,5)(6,7);
  the winners of (0,1)/(2,3) meet in round 2. Ties in points: goal difference, then the lower team index (as the tables).
- **The game's playoffs** (`$17C00–$18520`): `$CEF4` = the 16 teams, then the slots of later rounds (8, 4, 2, 1), copied by
  `$17D80` from the ROM table `$5576` + 16 × `$CEE8` (32 presets); winners follow from the bit mask `$CEF2`.
  `$CEEA` series mode (7 = New Playoffs, 0 = New Playoffs/7 games), `$CEEC` round, `$CEEE` your slot, `$CEF0` players,
  series wins at `$CE66` + 16 × series (+4 / +6). Play Mode 0 Regular Season, 1 Continue Playoffs, 2 New Playoffs,
  3 New Playoffs/7 games, 4 Shootout. The state is the game's password at `$D088` (`$18192` encodes, `$1803E` decodes),
  kept in the settings block in battery RAM (`$1EF6`), so Continue Playoffs survives a power-off. After a playoff game the
  game sets Play Mode 1 itself (`$18116`).
- New Playoffs set-up `$17D16` runs when Play Mode is changed to it in the menu (jump table `$F76C8`): picks a random preset
  with your team (Home) and your slot. Start then runs `$17D80` (`$F849C`).
- **`season/playoffs.s` (`$2FC800`):** `seeds` builds the tree and `QUAL` (1 top 3, 2 wild card) from the season table;
  hook `$17D8E` (`fetch`): when the season is over the tree comes from `SEEDS` instead of the presets; hook `$17D4A`
  (`newpo`): your slot in the season's tree, preset 0. `autopo` / `clrflag`: SRAM `$1210` = 1 once the menu has switched.
- **Season menu:** when your season is over (first time, and your team made it) it sets Play Mode 3, Home = your team,
  Players One, and calls `$17D16` itself. The labels use the same check (`checksel` = `force`), so they read Home/Away.
  If your team missed the playoffs the menu is simply free after the season. A new season clears the flag (`season.s`).
- **Division table:** a row background in the back plane — own solid tiles at VRAM `$B000` (tile `$580`, colour 15) and
  `$B020` (`$581`, colour 14) of palette 0 (the window plane's area; the window is off on this screen): green `$0060` = top 3,
  dark teal `$0440` = wild card, during the season too (where the line is now). The numbers moved to the front plane.
- `readsram.py` prints the tree the same way. Test: `mknv.py out.nv TEAM 82 fill` (a finished season).

## Division table after every game (2026-10-22)

- First built as one list of all 32 teams with Up/Down scrolling; jdc: after a game you should see the teams in their
  divisions, not one long list. Now one division per screen (exactly 8 teams, no scrolling), opens on your division,
  Left/Right = division, gap = to the division leader. The rest below still describes the screen.
- jdc's polish: no abbreviation, title on row 0, the rank in the front plane (no dark cells), and a darker font shadow:
  the font's shadow is colour 2 of palette 0 (`$0ECC`, light lavender; measured from jdc's screenshot: 206,207,239), set to
  `$0222` in the palette buffer (`$FFBD2C`) before the fade-in. Text plane per print in `PLANE` (`$FFE368`).
- MAME's `videoram` space read from Lua returned only zeros here (read_u8 and read_u16) — measure colours from screenshots.

## League table after every game (2026-10-22, first version)

- jdc: after a game, show the whole league like the game's playoff tree screen (team banners), without the cup, with the
  points and how far ahead / behind, scrollable. `season/table.s` at `$2FC000`, hook `$17336`.
- `$17336` is the playoff tree screen; it is called after a game (`$172E0`, returns to `$172E4`: fade, then the main menu) and
  before one (`$1731E`). In Regular Season it only did `rts` (`$15464`). The hook shows the table when Play Mode is Regular
  Season, the return address is `$172E4` (only after a game) and the season block is valid with games played.
- Set-up copied from the tree (`$1733E–$17432`): plane addresses `$B000–$B00E`, `$11594`, `$11B92` codes FF/FD/CE, fills with
  `$1197E`, `$11F20`/`$11F04`, background map from ROM `$2000` via `$1169A`, palettes from `$F3098`. The window plane (`$9202`)
  is turned off (`$9200`) and the horizontal scroll set to 0 (`$D5AC` = `$160`, the VBlank handler `$17692` writes
  `$FEA0 + $D5AC`).
- Banner: `$1169A` with the map at `$1DD366` (+ its offset at +4), rows 2·team and 2·team+1, 2 rows high, tile offset 2, at
  (`$B028`, `$B02A`) in tiles (`$17652` without its highlight test). Text: `$11BA4` with a1 = length word + text at the same
  position. Wait for the frame: `$175D8` (also blinks palette 3). Pads: `$11340`/`$11358` (d1 = new presses).
- Text plane: `$11BA4` writes one map cell per character into the plane picked by `$B02E` (`$B004 + $B02E`: 0 = `$E000`,
  4 = `$C000`); the `$11B92` control code FF sets plane 0, CE plane 4. At first the rank went to one plane and the rest to the
  other, so after a scroll old digits showed through (jdc: numbers on top of each other, blurry). Now every text goes to
  plane 4 (solid dark cells that a redraw replaces) and the banners to plane 0.
- Layout: title row 1, heads row 2, 8 teams at rows 4 + 3i (banner one row above), help row 26. Rank, `]` arrow at your team,
  banner (cols 4–13), abbreviation (both New York banners say NEW YORK), GP, PTS, gap (leader: + lead over number 2,
  others: − behind the leader). Opens with your team on screen. Up/Down scroll one team, Start = on.
- Test without playing: `mknv.py out.nv TEAM GAMES fill` makes a season with random results; a test copy of the ROM with the
  return-address check removed (`bne` after `cmpi.l #$172E4,(sp)` → `nop`) shows the table before the game too. The copy
  must be named `NHL94-season.bin` (MAME names the battery file after the ROM).

## OTW / OTL columns (2026-10-22)

- jdc: the standings should say OTW and OTL. The team row's spare word `+14` now counts overtime wins (your games:
  winner when `PERIOD` = 3; simulated: winner when `OTF`). W still includes them (points = 2 × W + OTL + T, as in the NHL).
- Standings columns: GP, W (regulation = W − OTW), OTW, OTL (+ ties), L, GF, GA, PTS; columns 4 apart from col 8,
  rank at col 1 with the selection marker drawn over its blank, team at col 5. Seasons started before this have no OTW
  for their earlier games.

## Goalie stats (2026-10-22)

- **Where the game keeps them:** in the team stat blocks (`$C6CE` home, `$CA32` away) slot 0 of the per-player arrays is the
  goalie: goals `+$B4` = goals against, shots `+$E8` = shots against (checked mid-game: home slot 0 shots = the away team's
  shot sum). So slot 0 is not a team total, and roster index 0 (the first goalie) has no row in the player block.
- **Goalie in net:** line 1's G in the team block (`+$16A`, 1-based roster index; the RAM copy of the lines, `+$16A` = 8 lines × 8).
- **Storage:** SRAM `$E20–$FFF` (the hack uses `$0–$E1F` and `$1EF6–$1FFF`: records `$0`, `$B60`, `$D20` 8×16, `$DA0` 128 bytes,
  settings `$1EF6` 256 bytes). 32 teams × 3 goalies (roster index 0..2) × 5 bytes: GP, W, GA (12 bits), SA (12 bits).
  Header `+14` = 3 means it is valid; a format-2 season keeps its skater stats and gets an empty goalie table on its next game.
- **Your games:** both goalies get GP, W (team won, overtime included), GA and SA from slot 0.
- **Simulated games:** the starter (line 1 G) plays 4 games in 5, the backup the rest (`gk.inc`, made by `build_season.py`);
  SA = the other team's simulated shots (`dist` leaves them in `SHOTSV` `$FFF0F2`), GA = its goals.
- **Team screen** (C on a team): page 2 has the rest of the skaters and then the goalies: GP, W, GA, SV% (".912", "---" before
  the first shot). Two pages always.
- `readsram.py` prints the goalies and checks per team: goalie GP = team GP, W = team W, GA = team GA, SA ≥ GA.

## Y button for the goalie

Done 2026-10-22 as fix 9 in `patch.py` (works in both ROMs), see CHANGELOG 12.

## How to play it now

1. Start `MEGAdriveHARDJOB/NHL94-season.bin`. Play Mode **Whole Season** (Right from Shootout), Home = the team you want.
   Play the first game: that team is now your season team.
2. From then on the menu shows your next game (opponent, home/away) and locks those rows; press Start to play it.
   Every finished game is recorded, and the rest of the league plays one round at the same time.
3. Start in the main menu → pre-game menu: **Season Standings**, **Scoring Leaders**, **New Season** (asks first), Start Game.
4. The table is kept in the game's battery save (`.srm` in RetroArch), so it survives turning the game off.

Build: `tools/season/build.sh` (on top of the installed patched ROM). Read a save: `python3 tools/season/readsram.py <file>`.

## Status

- 2026-10-21: steps 1, 2 (league simulation) and 4 done and tested headless.
- Step 3 done too (your team, 82-game schedule, locked menu).
- Scoring leaders, game N of 82, locked settings, Continue/New Season, shots, team screens and divisions done.
- Y = goalie done (fix 9), goalie stats done. Next: playoffs after game 82 (top 8 per conference).

## Season-to-playoffs test run; finished season takes no more games (2026-10-25-r18)

jdc: test that every season reaches the playoffs, the playoffs run to the end.
GPGX driver (work/season-end-test-20261025/po_hook.py): Simulate Match through
menu input for the league, then real playoff games (clock held at 1 s, score set,
Start outside play), plus fully natural games without any pokes. SRAM validated
after every game (checksum, GP, W/L, scorers, goalie GP/GA).
- Full 84/84, Light 31/31, Mini 8/8 simulated: all checks ok, Continue to Playoffs.
- Playoffs: Mini and Light single games won all four rounds (round 0..4, flag 3);
  Full best-of-seven advanced round by round to the final; Mini/Full loss in
  round 1 ends the playoffs (bracket shown, flag 3).
- After a Cup win NO champion screen appears (Stars of the Game, Exit Game, then
  the main menu with New Playoffs); after elimination the bracket is shown.
- After the season's playoffs the menu shows native New Playoffs; starting it
  sets the season's playoff flag $1210 from 3 back to 2 (standalone and season
  playoff state still interact; to be fixed).
- BUG fixed: a season that missed the playoffs still counted real Start Game
  games (natural Light run reached 45/31, team GP 44..59; Mini 10/8 in r17).
  season.s haveuser now returns when games >= length. r18 natural Mini: 8/8, all
  teams GP 8. The menu after such a season still shows "Continue <slot>" with a
  meaningless "Game 1/8" line (to be replaced by the end-of-season flow).
- Expected, not bugs: natural games show team GF > scorer goals (shootout winners
  add a team goal only) and team GA > goalie GA (empty-net goals).
SHA256 33baa63541d44a71f2e43d88a0dfcb8bcf752611598462794feaece74c7f13ef, CRC BE3CEBD9.

## End-of-season summary and whole-run player statistics (2026-10-25-r19)

jdc: after the Cup (won) or the bracket (out), go on with a button to a screen with
all the statistics of the whole run. New experimental/seasonend.s at $2E1400 (+0 hook
$172E4 after the post-game tree/table and before the $172FA RAM reset, +4 from the
Simulate Match result screen, whose single jmp $172FA seasonend.py redirects).
classify (season view, valid finished season): Regular Season last game and the team
not in SEEDS -> missed (flag 5); the season's playoffs on (flag 2) and the game has
switched back to New Playoffs (D048 2/3) -> out (flag 3, round = $CEEC-1, the game
advances the round before) or champion (flag 4, your score > theirs in this game).
The flag is written once (playoffs.s new +28 setflag), so the summary shows once.
Screens use table.s's set-up (new table.s +4 = setup: background $2000, palettes,
scroll, VSRAM, no window/sprites); the native panel ($9BD8, simulate/teamstats) is
invisible in this post-tree context (wrong palette; $11594 clears VRAM). Season
Summary: banner, record W-L-OTL, points, GF-GA, result; Start -> "Season + Playoffs"
list: skaters by points (as teamstats build), G A PTS SOG, page 2 rest + goalies GP
W GA SA, Left/Right page, Start -> main menu. Start waits release/press (no skip).
A Cup win first shows the game's own Playoff Stats and Champions page (now reached).
Whole-run stats: season.s playoff games of the season (playoffs.s +32 poseason: in
progress, your team, flag 1/2) go through the normal save path (addreal/gkreal +
checksum) without round marking and league simulation; team table rows unchanged.
So teamstats/leaders also include playoff points after the season.
playoffs.s: played no longer reopens finished playoffs (flag >= 3; the post-season
standalone New Playoffs set 3 -> 2 before); spcheck treats >= 3 as over.
Tests (GPGX, work/season-end-test-20261025): Mini win -> Champions page, summary
"Stanley Cup champions", players, page 2 goalies (Thompson 11 GP = 8 + 4); Mini
loss -> bracket, "Out in the first round"; Mini missed via Simulate Match -> summary
"Missed the playoffs" (Vancouver 0-6-2); Full BO7 champion flag 4, goalie GP 84+16.
verify_season_navigation 18/342, verify_critical 10, verify_critical_flash PASS.
Known: after a season the menu still reads "Continue <slot>" (games no longer count).
SHA256 a877c327a8c66674172fce3a72359d24185ee37aeb99494efaf9febb561e7291, CRC C8CC833B.

## Co-op game row without at/vs (2026-10-25-r20)

jdc: switching Players One <-> Two - Teammates changed the Game row from "at" to "vs"
(co-op always plays at home), which looked misleading. seasonmenu.s gameinfo now
writes "84/84 FLA (co-op)" when Players = 3 (Two - Teammates); One keeps "at"/"vs".
Tested from a Full save at 84/84 (Edmonton, 83 simulated games): One "84/84 at FLA",
Two - Teammates "84/84 FLA (co-op)", back to One "at FLA".
SHA256 f073a6928cf8e9ade13c003d6ef94fcbe72c4030aa188a651f2d9a0c1d3828df, CRC 85089A9A.

## Playoff statistics apart, three tabs, sorting, scoring leaders (2026-10-25-r21)

jdc: see the scoring leaders too; with playoffs, one tab with ALL stats, one with only
the league, one with only the playoffs; if you go out, the playoffs are simulated to the
end in the background so all stats are there; filter goals / assists / points.
- Playoff block: SRAM $0002 "PO", $0010 + 100*slot player rows (slot = place in SEEDS,
  16 = scratch), $06B4 + 15*slot goalies, to $07A2. $0002..$0D1F is free: native code
  only writes the whole 8 KB on a format, User Records are off; the slot banks copy and
  checksum it with the rest. Cleared by playoffs.s setflag(1) (playoffs set up).
- season.s rowload/rowsave/gkput take their offset from postats.s ($2E2000 +0 ROWOFF,
  +4 GKOFF; same size as the replaced mulu/addi): POGAME set -> playoff block. So the
  season's own playoff games now go there, not into the league rows (r19 merged them).
- postats.s +12 others: after each of your season playoff games one game for every
  other pairing of that round; +16 finish: when you are out, every later round plays its
  series to the winners the game has already drawn ($CEF4 holds the whole drawn tree,
  re-drawn each round), one game or best of seven (0..3 losses). Uses season.s
  goals/dist/gksim via season-symbols.inc; checksum via DELTA (fixsum).
- seasonend.s moved to $2E3000 (hooks $172E4 / Simulate exit updated). After the summary:
  your team (banner, tab title, sort, skaters, Up/Down page 2 with goalies) and then the
  league's top 18 Scoring Leaders (rank, player, team, G A PTS SOG). Left/Right = tab
  (Season + Playoffs / Regular Season / Playoffs), C = sort (points, goals, assists),
  Start = next / main menu. ALL = league + playoff rows added on display.
Tests: Mini out in round 1: 15 playoff games (30 goalie GP), league rows/goalies
unchanged, checksum ok; tabs/sort/pages/leaders captured (Thompson 7 league + 1 playoff
= 8 GP). Full best-of-seven champion: 60 playoff games (16 yours + 28 + 12 + 4), league
GP 84 for all, checksum ok. verify_season_navigation 18/342, verify_critical 10,
verify_critical_flash PASS. The font has no < > (footer: "L/R tab").
SHA256 3115bde11904a3a00cb4201fa11cef8f7eff810573aed220d14417f46a803e87, CRC 3693B1E0.

