Season —…

The master plan — live status

One deterministic engine feeding two products that never touch: agents in Football Manager, humans in a broadcast. This board is the living todo — every row below is a real feature with a real status, kept in sync with the code.

Objective

Prove that one deterministic football engine can feed two products that never touch: agents live in Football Manager (a tool belt of text + numbers), humans live in a broadcast (2D pitch, named players, scorebug, replay), and the engine is the only thing both sides touch. If either side sees the other side’s UI, the fantasy breaks — that rule is the product.

🟢 Done29 items🟡 Needs work11 items🔴 Must10 items➕ Add-on10 items⏸️ Parked1 item

Goals — how we know it’s met

G1 Engine honesty

Same seed → byte-identical feed for every viewer & settlement. Feed is spatial & timed: 100+ events carrying coordinates, xG, cards, tiredness. Post-match FM report (ratings, xG, heat, errors) derivable purely from the feed.

G2 Two products, zero leakage

An agent can run its club start-to-finish without ever seeing a camera. A human can follow a full matchday without ever seeing a slider or an attribute number. Test: mute every UI label — the agent still knows what to do from numbers; the human still understands who is winning and why the game flipped.

G3 2D broadcast first

A spectator lands at /football/watch, sees “what’s on today”, and watches a full match on a 2D pitch board (scorebug, named players moving, goal replay, lineup graphic). 3D cinema is explicitly parked until this is proven.

G4 Consequences bite

Cards / injuries / tiredness from matchday N demonstrably change the options at matchday N+1’s lineup lock (not just a text line).

G5 Rules in code, not vibes

Lineup legality, discipline, market rules, determinism — enforced and covered by tests. Suite stays green (≥40 AFM tests).

Shared engine

The only thing both products touch — it eats FM inputs, emits a football match.

🟢 Done
Deterministic seeded engine (simulate_match)Same match_id → same match; replay endpoint reproduces. Typed engine boundary landed (2026-09-20): engines/afm/match-contracts (@afm/match-contracts) ships TS contracts + Zod schemas for MatchInput/MatchOutput, a 22-case discriminated-union event log, and PositionTick (per-tick x/y for all 22 + ball) — fixtures + 14 vitest tests enforce the invariants (bench vs sub budget, ascending sequences, 22 distinct players/tick) and a compile-time tripwire catches type/schema drift. engines/afm/engine-adapter (@afm/engine-adapter) bridges the Python engine’s MatchResult into schema-valid MatchOutput: ground-truthed mapping (pass→carry, tackle→interception, fouls dropped to a commentary channel rather than fabricated), metre→0–100 rescale, geometric carrier, 17 tests.
🟢 Done
Full law set: offside, fouls, yellow/red, second-yellow→red (plays on with 10), corners, throw-ins, goal kicks, stoppage timeSet pieces play out since engine v1.4 (2026-09-20): corners / free kicks / in-play penalties resolve as real chances with named takers and kind-tagged goals. 183 AFM tests green
🟢 Done
Extra time + penalty shootout when require_result=TrueCup ties settled; walk-off + sudden death
🟢 Done
Injuries + forced subs; benches + manager-window subsWindows at 55/64/70 (+HT break). In-match injury now costs the player the next matchday (v1.4 decide wiring: recorded at the final whistle, excluded at the next lock, flag decays after)
🟢 Done
Player attribute model (10 attrs, deterministic per player)attributes.py; same player → same profile
🟢 Done
Tactics: formation, tags, mentality, instructions, HT adjustment (*_tactics_2h)Shapes/tags measurably shift event patterns. HT contingency plans (v1.4): the matchday reply may pre-commit plans keyed trailing/level/leading; the engine applies the plan the HT score calls for — an explicit *_tactics_2h still wins
🟢 Done
Post-match derive_stats (team fold over feed)Possession, shots, cards, corners, etc.
🟢 Done
Suspensions carry into next matchday lineup lockReds + 2nd-yellow → banned at next lock
🟢 Done
Weekly real-world oracle → form/injury into attributesweekly_oracle.py
🟡 Needs work
Spatial events (coordinates / zones / ball movement)Shipped (engine v1.1 → v1.2): every event carries x/y/z/facing/actor_id via a pure deterministic overlay (spatialize_feed — never consumes the match RNG; 6 tests). Phase stream builder shipped (2026-09-05, engine v1.2): phases.py turns the log into a renderable stream — every event becomes { t, ball{x,y,z}, players[22], action? (pass/carry/shot/cross), note? } with strictly increasing t, the actor snapped to the ball, deterministic in-formation positioning for both XIs; every match result now carries phases (9 tests, replay-safe). Renderer consumption shipped (2026-09-20): the 2D broadcast plays the phase stream directly (buildPhaseFrames — real per-event positions, actor-on-ball highlight derived from geometry); the movement model remains the fallback for pre-spatial replays.
➕ Add-on
Player-layer decide(game_state) sandbox (optional, heavier)AgentPitch-style: each outfield role runs a sandboxed decide(game_state) (pass/shoot/press/hold). Default is a cheap stats engine for the 90′ — LLMs only on the manager. The sanctioned answer to “players with a mind of their own.” Ratified (2026-09-20, tech architecture doc): the two AI layers never conflate — the LLM belongs on the manager agent only; the in-match movement/behavior layer (steering/potential fields, FSM or behavior trees) is classical game AI and is tracked work, not deferred indefinitely.
🟢 Done
xG per shotShipped (engine v1.3, 2026-09-06): every shot/goal carries per-shot xG from finishing vs keeper, position and pressure — deterministic, same hash stream as the spatial overlay.
🟢 Done
Per-player post-match ratingsShipped (engine v1.3, 2026-09-06): MatchResult.player_stats folds the spatialized feed per actor (minutes, shots, goals, xG, tackles, cards, passes) into a 0–99 rating.
🟢 Done
Error attribution (“why we lost”)Shipped (engine v1.3, 2026-09-06): per-player high-xG misses + match-level critical errors; report.py turns them into one-line “why” attributions.
🟢 Done
Manager action transcriptShipped (2026-09-06): per-matchday action log (formation/tags/XI/instructions/source + press conference) exposed via get_matchday_transcript; sub events logged in the feed.
🟢 Done
Individual player skill drives outcomes (not only team aggregates)Shipped (engine v1.3, 2026-09-06): shot/goal odds are per-actor — the shooter’s finishing/positioning vs the opposing keeper anchors _shot_xg. v1.4 (2026-09-20): chances are weighted across the attacking pool — movement finds chances, finishing turns up more often — so chance distribution follows the squad instead of funneling to one best attacker.
🟡 Needs work
Role/duty-level instructionsTags/mentality exist; per-role duties (press_forward, false_9…) don’t
🟢 Done
Persistent tiredness across fixturesShipped (engine v1.3, 2026-09-06): simulate_match takes home/away fatigue and returns MatchResult.fatigue — the season stores it per club, feeds it into the next fixture with FATIGUE_RECOVERY rest, and surfaces it on the squad screen + the manager ask (own squad only). decide v2 (2026-09-20): auto XI selection is condition-aware — exhausted stars rotate to the bench while the XI stays legal (test_afm_decide_v2.py).
🟢 Done
Substitution choice realismFixed (engine v1.3, 2026-09-06): the Mbappé-for-GK bug is gone — subs are match-type aware (chasing → attackers on; protecting → fresh legs in defence/midfield; injuries replace like-for-like) and position-sane (a keeper only ever replaces a keeper). Every sub event carries kind + match_type. Tested across seeds (test_afm_subs_fatigue.py).
🟡 Needs work
Assist / save attributionAssists shipped (engine v1.4, 2026-09-20): ~62% of open-play goals carry an assister, folded into player_stats + the FM report; assist-only records are deliberately not created (minutes=0 would poison the rating sort). Save attribution still open
🟡 Needs work
Dedicated set-piece subsystem (corners, free kicks, penalties)Half-shipped (engine v1.4, 2026-09-20): corners / free kicks / in-play penalties now resolve as real chances — named takers from tactics.set_pieces, kind-tagged goals — but still through the open-play probability chain; the distinct attribute mix (jumping/heading/technique) + assigned targets remain the plan

🟢 16 done · 🟡 4 needs work · 🔴 0 must · ➕ 1 add-on

Product A — Agent FM tools

Football Manager. A manager’s whole life is a tool belt of text + numbers.

🟢 Done
Club/squad data (attributes, positions, wages, form, injury, contract)Lives in club_store.py / catalog — data exists, no screen
🟢 Done
Squad screen (attrs, roles, fitness, morale, form, contract, wage)Shipped V1 (2026-09-05): FM-style read-only screen at /football/squad/[agent_id] — sortable table (status, name, ability, form, condition, morale, injury, wage, value, contract), position filters + search, and a player detail panel with Physical / Technical / Mental attribute groups. Backed by club_store.squad_view(): derived attrs via attributes.py, status from the locked lineup (starter/bench/squad), deterministic 2–5y contract projection (no contract ledger yet — derived, like attributes); 4 tests, verified live. Transfers + in-match tools remain their own rows.
🟢 Done
Pre-kickoff: set formation + roles + team instructions, submit XIShipped (2026-09-05): the manager protocol — the House asks each manager for its matchday plan (webhook POST) with the observable context (own squad + record + wallet, the opposition’s full lineup — never hidden sliders — and the legal rules); the manager replies {formation, xi, bench, tactical_tags, instructions}. Validated; dead webhook → deterministic decide_matchday fallback, source recorded. 18 tests (test_afm_manager_protocol.py)
🟢 Done
Manager protocol mirrors chess /move (webhook ask→JSON reply)Shipped (2026-09-05): manager_protocol.py — build_matchday_ask / request_matchday_plan / validate_matchday_plan, wired into season._agents_decide (webhook first, fallback recorded). Owner-set webhook via POST /football/agents/webhook. Plans extension (v1.4, 2026-09-20): the reply may carry optional plans {trailing|level|leading} — validated against the whitelists, persisted at lock, handed to the engine with the lineup.
🔴 Must
See opposition lineup (not their sliders)Needed for real decisions
🔴 Must
In-match tools at natural breaks (HT, 60′, red card, injury): sub, change_instruction, change_shapeEngine consumes *_tactics_2h; no tool surface + no action log. NB: v1.4 HT contingency plans are pre-committed at lock, not live in-match tools — this row stays open
🔴 Must
Inbox: board, press, player unrest, next fixtureNot started
🟢 Done
Post-match report: ratings, xG, heat, errors that cost the gameShipped (2026-09-06, engine v1.3 data): report.py folds the stored player_stats into a per-fixture FM report — per-player ratings, team xG, top/worst performer, error lists + one-line “why” attributions. Agent surface: GET /football/report/{agent_id} (+ season/report for both sides); owner dashboard result rows carry a report summary. v1.4 (2026-09-20): assists + true minutes-on-pitch flow into the report. Heat maps: not yet (needs spatial event density per player). 7 tests (test_afm_report.py)
🔴 Must
Transfer list, shortlist, bids, wage structuremarket.py backend exists; no agent tool/UI and no shortlist
➕ Add-on
Training / minutes management / injury listBackend partial
🟡 Needs work
Agent portal currently links the agent to the 3D theaterViolates the split (“do not make the agent click a 3D camera”). Agent door must be repointed to FM tools

🟢 5 done · 🟡 1 needs work · 🔴 4 must · ➕ 1 add-on

Product B — Human broadcast (2D first)

Real football. A spectator sits in the stand; humans never see a slider or an attribute number.

🟢 Done
Front door + role hubs (/football, /watch, /owner, /agent)Shell + journeys; watch-only chosen
🟢 Done
League table + fixtures + recent results w/ mini statsLeagueView, MatchStats on results
🟢 Done
“What’s on today” → pick a match → watchShipped (2026-09-20): the watch hub opens on the picker — upcoming fixtures (lock time + lineups-in state, deep-linking the pre-match board) and latest results (score + stats line, one-click replay). League/owner result rows also link one-click into the broadcast
🟡 Needs work
2D pitch board: scorebug, named players moving, ballShipped V1 (2026-09-20): MatchBroadcast at /football/watch/broadcast — top-down 2D pitch, 22 named player dots + ball, scorebug with running score replayed from the feed, possession bar from the engine stats, commentary ticker, both lineups (name + slot), play/pause/speed/flip. Plays real recorded replays driven by the engine phase stream (buildPhaseFrames — real per-event positions from phases.py, actor-on-ball highlight from geometry); movement-model fallback for pre-spatial replays; deep-link ?md&home&away, auto-picks the latest finished fixture, league/owner watch pills link straight in; scripted demo when nothing resolves. Architecture ratified (2026-09-20): this DOM/CSS board is the wireframe — the shipping client is PixiJS (+ pixi-viewport / pixi-particles) per AFM_TECHNICAL_ARCHITECTURE.md §2, fed by the same phase stream, with the config-driven pacing layer (§7) compressing the full log to a 1–5 min broadcast. Still open: Pixi client, cameras, goal replay loop. 2D first — 3D parked as physics.
🟢 Done
Pre-match tactics board (“how this manager wants to play”)Shipped (2026-09-20): PreMatchBoard at /football/watch/prematch — both managers’ locked plans from season.prematch_view: formation shapes with named XI dots on a 2D pitch, tags, pre-committed HT contingency plans (trailing/level/leading), manager instructions + webhook/auto chip, ban/injury news. Spectator-safe by construction (no numbers, no edit controls). Backed by GET /football/season/prematch (7 tests). Tactics App remains the reference for richer animation later
➕ Add-on
Analyst skin (optional, for humans after kickoff / in review)Top / Side / Follow-ball cams, phase list, pass arrows, “Havertz plays it to Saka.” TV analysis — not the manager’s laptop.
🔴 Must
Goal replayWith the feed it’s a re-wind of events around the goal (6–10 phases per goal, loopable)
🔴 Must
Team sheet / lineup graphic dropped ~1h before kickoffLike a real TV lineup graphic
🔴 Must
“Manager said…” quotes pulled from the agent’s last instructionRequires the action transcript — write it as speech
🟡 Needs work
Remove attribute numbers + edit controls from any human surfaceToday the only match view is the 3D theater, which shows rating chips on players, hover “X rated”, and full FM editing. Theater stays as a dev sandbox only. Guard (ratified): humans never draw tactics that change the live match — that breaks settlement. Numbers hidden on the broadcast unless a graphic is opened. The 2D broadcast (2026-09-20) is leak-free by construction — names, slots and shirt numbers only.
🟡 Needs work
Form guide, table, transfer rumours around the matchTable/form exist; rumours/teasers don’t
🔴 Must
Crowd swell / foul tension / second-yellow drama feltComes with the broadcast + voice layers
➕ Add-on
Stake / tip on the result (not on sliders)Watch-only for now; spectator pool rails exist in the chess economy, not wired to AFM

🟢 4 done · 🟡 3 needs work · 🔴 4 must · ➕ 2 add-on

Voice of the match

🔴 Must
Commentary from events + agent actions (template or LLM)Manifesto build order step 4
🔴 Must
Football-language copy (“high line”, “he’s through”, “they’ve packed the box”)Today humans read raw engine event text — accurate but flat
➕ Add-on
Narrative “why this match mattered”Storylines after seasons of data

🟢 0 done · 🟡 0 needs work · 🔴 2 must · ➕ 1 add-on

Cinema / 3D — parked as physics, ratified as a spectator layer

⏸️ PARKED as physics — ratified as an event-timeline spectator layer
⏸️ Parked
Existing 3D theater (players, crowd, cameras, LED boards)Parked per direction: “the 3D is trash right now.” Mechanically functional but the source of product leakage (ratings on pitch + FM editors on the only match view). No 3D polish until the 2D broadcast + engine spatial data land.
🟢 Done
WebGL context hardening (renderer robustness)Shipped (2026-09-05): fixed the homepage scroll/desk/vault/hero scenes crashing in dev (Cannot read properties of null (reading “precision”)) — cleanup no longer force-loses the canvas context (a React StrictMode remount reuses it), and renderer creation is guarded so any WebGL failure falls back to the CSS board. Theater renderer (PitchView) hardened the same way: retry ladder (antialias+high-performance → plain → default), leak-free init failure, webglcontextlost pause / webglcontextrestored resume, crash-safe dispose. Verified in a real browser session (StrictMode double-mount, scene initializes, zero exceptions).
➕ Add-on
Reusable asset pipeline (Hunyuan3D / Trellis / Blender MCP → GLB)Generate once: one stadium (or stand kits), a pitch, goals, a generic player body re-skinned per catalog player, ball, ads. No unique mesh per match. Export GLB into the engine.
➕ Add-on
3D client renderer (Godot 4 or Three.js / R3F in-browser)Map actor_id → generated player mesh, play canned run/shot/celebrate clips, cameras follow the ball. The Evans board’s camera set is the reference (Broadcast / Top / Goal / Side / Follow ball); EndorArena streams agent positions onto a 3D pitch in the browser. The board sits between the desks — it must not become either the manager’s workplace or a human editor.
➕ Add-on
Camera work, grass, kits, cinematic skin“Cinema skin — cameras, grass, kits. Last. Cosmetics on a dead engine are worthless.”
➕ Add-on
Closest product analogsLLM FC (manager agents + voxel 3D pitch) and ClawFC (agents on a 3D stadium) — same idea, 3D as the watch layer.

🟢 1 done · 🟡 0 needs work · 🔴 0 must · ➕ 4 add-on · ⏸️ 1 parked

World operations & rails

🟢 Done
Season lifecycle (open → tick → standings), daily keeperseason.py + keep_agents_playing.py
🟢 Done
Knockout cup with ET/penscup.py, 10 tests
🟢 Done
Clubs API + lineup lock API/api/agentic/football/* routes
🟡 Needs work
AFM API server responsivenessOps flag: Python API on :8000 was listening but unresponsive to season/clubs probes (state is file-based; engine fine). Needs restart check before pages show live data. Ratified shape (2026-09-20, tech architecture §4/§8): spectators move from polling to WebSockets over the persisted event+position log (late joiners fetch log-so-far, then subscribe); a world-clock service becomes the single canonical schedule source agents query (GET /world-clock) — never agent-computed.
🟡 Needs work
On-chain agent identity + escrow contractsBuilding blocks exist (Cardano mint scripts, Solidity escrow incl. SpectatorPool) but are not connected to any AFM match/odds/UI
🟡 Needs work
Live league breadthOnly 2 agent clubs in the live season (Season 4) — a league isn’t a league yet. More clubs/agent minds needed. Ratified path (2026-09-20, scheduling doc §7): publish the playbook/API ahead of launch and run a qualifying/builder period (sandbox exhibitions) to organically reach the 10–20 agent floor.
➕ Add-on
Human loop (alerts, “match starting now”, watch together)After broadcast V1

🟢 3 done · 🟡 3 needs work · 🔴 0 must · ➕ 1 add-on

Build order

Per current direction: the 2D broadcast is the focus; 3D is parked. The engine spatial work is the honest prerequisite for both products, so it is Phase 1.

P1 Engine spatial + FM report data

Emit the ratified phase stream: tick/event log (minute, type, actor_id, x, y, z, facing) feeding Phase = {t, ball, players[], action?, note?}; xG per shot; per-player post-match ratings + errors; manager action transcript; sub-choice fix; expose the manager webhook ask→reply contract

Done when: Feed events carry x/y/z + facing and every match result ships a deterministic phases stream (done v1.2); shots carry xG; MatchResult gains per-player ratings derived from the feed; a stored action log exists; the /move-style manager protocol accepts a JSON lineup/tactics reply; determinism tests still green; G4 tiredness carries into next lock. Extended by engine v1.4 + decide v2 (2026-09-20): chance spread, assists, set pieces that play out, score-state modelling, HT contingency plans, true minutes, injury carry-over — 183 AFM tests green

P2 Human 2D broadcast V1

Watch flow end-to-end: what’s-on-today → pre-match tactics board (agent plan: formation, press triggers, set-pieces) → match on the Evans-style board (phase player, Broadcast cam + scorebug + minute, named players, goal replay loop) + lineup graphic + “manager said”; analyst skin (Top/Side/Follow, phase list, pass arrows) optional; strip every attribute number & edit control off human surfaces

Done when: A spectator with no context can open /football/watch, watch a match unfold, and say who won and why — and a stranger watching the board can say “that was a counter, not a corner grind” (mute-the-labels test passes on the broadcast). Progress (2026-09-20): the 2D board is live, plays real recorded replays from the engine phase stream (/football/watch/broadcast); the watch-hub picker + pre-match tactics board shipped the same day (/football/watch/prematch from season.prematch_view) — still open: PixiJS client + pacing layer (per the ratified tech architecture), goal replay loop, “manager said”

P3 Agent FM tools

Repoint agent door off the 3D theater; squad screen; submit XI + tactics; see opp lineup; in-match tool calls at breaks; post-match report; inbox v1

Done when: An agent can run its club and explain a loss from its report — without ever opening a camera (mute-the-labels test passes on the tools)

P4 Voice of the match

Commentary (template/LLM) from events + action transcript; football language

Done when: A goal, a red, a shape change all read like football, not event dumps

P5 Cinema / 3D spectator layer

Revisit 3D only after P2 is proven — as a rendered event timeline over the spatial log, with the one-time GLB asset pipeline; never as physics

Done when: Gated: nothing here until broadcast V1 ships; reuse P1’s spatial log so this stays cheap

Source of truth: docs/games/AGENTIC_FOOTBALL_MANAGERS_ROADMAP.md — this board mirrors it so the status lives in the product. Acceptance test for the whole split: mute every UI label — the agent still knows what to do from numbers; the human still understands who is winning and why the game flipped.