case studyUpdated Aug 7, 202635 min read$0C

Alan Is Gone. I'm Trying to Keep His Fighting Style Alive.

Thousands of replay records held his habits. An eight-frame alignment error forced the memorial AI to start over before it could claim to play like him.

Jump to evidence
ai-for-goodmachine-learningbehavioral-cloningreinforcement-learningmemorial-ai
On this page
Status correction // August 7, 2026

The chapters below preserve the route that led here, including the failed models and apparently encouraging dashboards. A later trace audit proved that every pre-v10 trace-derived checkpoint is invalid as evidence of Alan fidelity: replay state was shifted eight frames relative to its label, and a post-frame observation was trained against the action that had already changed it. The clean contract is now s_i → a_(i+1)..a_(i+16). The v10 run reached decoder initialization but stopped before its first epoch checkpoint. A later developer-menu audit corrected +$80 from false attack strength to a move-property row code, so the schema-v8 state cache is now invalid and must be rebuilt under schema v9. No training process, closed-loop result, or blinded-human result is active or complete. Nothing on this page should be read as a claim that the current bot passes for Alan.

Some projects are technical exercises. This one is personal.

alanmargolies88 — Alan — was one of the most distinctive Marvel vs. Capcom 1 players on Fightcade. Morrigan/Ryu, with 7,554 replay records now catalogued. Every regular knew his style: the patient air-throw setups, the hit-confirm into magic series, the tag-to-heal habit at low HP that most players never bothered to learn. He didn't rushdown. He punished.

Alan isn't around to play anymore.

This is the evolving story of building something so the community can still spar against his style. Not a "good MvC1 AI," and not a claim of completion. The failed versions matter because they show how easily an imitation project can produce plausible metrics from a broken observation/action contract.

The Turing test isn't "can it win" — it's "does this feel like Alan."

evidence funnel

What entered the frozen v10 candidate

A replay record is not automatically a faithful trace.

validated alignment-v2 tracemissing at freeze
  1. Alan catalogue7,554 replay records97.06% accepted

    The 222-record gap remains explicit; the candidate does not silently treat discovery as usable training data.

Frozen manifest candidate-69be7435c697. Coverage is a data boundary, never a fidelity score.

Chapter 1 — The data is the player

The first model's vocabulary was never keypresses. It was motions: walk_F, super_jump, qcf_p (Soul Fist), magic_series_5hit. A frame-expander translates each token into per-frame button masks using Alan's actual recorded timings, mined from his replays — not hand-coded interpretations of strategy guides.

This is what "behavioral clone" earns you that a scripted bot can't:

input piano roll

One Alan phrase, exactly 50 frames long

Width is time held. The launcher and assist are decisions a strategy-guide script would miss.

DD+HP launcherD resetneutralMP+MK assistneutral resetbackHPdowndown-forwardforward+LP
  1. super_jump_combo_4hit50 frames

    The long back hold is part of the recorded phrase, not padding added by the model.

Decoded from one real replay phrase. Durations sum to 50 emulated frames.

Every strategy guide says the MvC1 launcher is LP+LK, or standing Fierce. Alan presses crouching D+HP, held exactly 3 frames, then calls an assist mid-combo. If I had built this bot from FAQs, it would feel wrong in a way players couldn't articulate but would absolutely notice.

The training data tells you the truth; the documentation lies.

Frequency turned out to matter more than capability. Any bot can contain every move; what makes it feel like him is how often and in what situations it chooses each one. An early version of this article compressed held directional channels into loose words such as “block” and “jump.” That was too flattering and too imprecise. The historical behavior chart in Chapter 4 now keeps the artifact's actual field meanings. The community enforces this distinction mercilessly—the first feedback on early footage wasn't “the AI is bad,” it was “that's not how Alan would have played the corner.”

Chapter 2 — Teaching the bot to see

The perception system went through ten schema revisions — box geometry, partner state, projectile pools, camera correction, signed positions — and the breakthrough wasn't a probe session. It was an 81-page community reverse-engineering paper for the exact ROM revision Fightcade ships, documenting 27 per-player struct offsets and 432 instructions that read the character identifier.

Cross-referencing it against the bot's RAM map found four channels the bot had never seen — including one genuine bug:

-- schema v5 had read 0xFF30BA as a one-byte "jump status" flag
p1_jump_status   = read_u8(0xFF30BA)                             
-- the paper documents +$BA as a SIGNED WORD: the engine's own
-- per-frame X-distance to the active opponent
p1_enemy_dist_x  = read_s16(0xFF30BA)                            

Eight weeks of training runs had consumed the high byte of a signed enemy-distance word as if it were a status enum — positive distances ≥ 0x80 sign-flipped, small positives collapsed to zero. The other finds: a 32-bit move-class bitfield (armor active, wall-jump capable, post-super state), the frame-exact chain-cancel mask, and the per-frame animation variant selector.

For every project that touches a community-maintained binary, assume someone has produced a structured reference and go find it before you write another probe.

By the end, perception was a 268-dimension state vector with real hitbox geometry — the bot judging reach the way the engine does, not by eyeballing sprite distance.

How much of the fight the policy could see
  • First motion clone · schema v3153ordered features

    movement and coarse combat state, before the full collision and partner model

  • Causal candidate · schema v9268ordered features

    sanitized collision geometry, partners, projectiles, camera, reactions, pushbox modes, and correctly decoded move-property row context

More dimensions are not automatically better. The gain matters because offline and live builders now emit the same ordered schema.
How it works
Drawing the diagram…

Chapter 3 — The ceiling of imitation

Held-out token accuracy looked great. The behavioral-fidelity metrics looked great. Live?

Live, it was a statue with good taste.

Four sessions, four honest verdicts from the person holding the P2 stick: "not Alan at all." Three dissected root causes, each a lesson:

The state and action never belonged to the same moment. The old replay runner began consuming recorded inputs, then spent eight frames "settling" before emitting row zero. A cross-correlation audit measured exact controller agreement at lag +8, not zero. Worse, row i is post-frame state s_i, after input a_i has already executed, yet the old target began with a_i. The replacement trace builder uses zero post-start settle frames, records both observed controllers, rejects any mismatch, and supervises only the action a live policy could still choose: a_(i+1) onward.

causal contract

The label moved from the past to the future

Pre-v10 · rejected

future-state / past-action leakage
label: recorded action ai
observed RAM state si+8

Row zero was emitted only after the replay runner had already consumed 8 inputs.

v10 · live-compatible

target offset +1
aiexecutes
observe post-frame si
predict ai+1 … ai+16

A live policy sees si only after ai changed the game, so ai+1 is the first action it can still choose.

The dual-controller audit found exact agreement only at lag +8 in the legacy corpus. v10 requires same-frame input agreement, then shifts the supervised target forward by one.

The copycat lock. The decoder conditioned on its own previous chunk token, and the conditioning self-reinforced: token 236 predicted token 236 at 98%, forever. The literature calls it causal confusion — 7,300 replays cannot cover an exponentially growing history space, so the model latches onto the one feature that always correlates: itself.

The button-deletion bug class. Two input-bit layouts differ by a two-bit shift, and a single wrong constant silently stripped the MK and HK buttons from every emitted action. This same class of bug shipped three times in different subsystems. The bot literally could not press a third of its buttons, and no offline metric noticed — the training data and the metrics lived in one layout; the emulator lived in the other.

Open-loop chunks can't block. Committing to 16 frames of pre-planned input is a ~270ms reaction floor in a game where blocking a jump-in gives you 3–8 frames. The fix that transfers: predict 16, execute 8, re-decide — with careful bookkeeping so the conditioning still looks like training.

The reaction budget, in emulated frames
  • Predicted motor chunk16frames

    266.7 ms at 60 Hz if executed open-loop

  • Receding execution budget8frames

    133.3 ms before the policy observes again

  • Tight end of a jump-in block window3frames

    the measured decision window spans roughly 3–8 frames (50–133 ms)

Predicting sixteen frames preserves a coherent motion. Executing only eight creates another chance to react before the whole plan is spent.

Those older failure modes were real, but none could be interpreted cleanly until the causal alignment defect was removed. A better decoder cannot recover Alan's timing from state/action pairs that are eight frames apart.

How it works
Drawing the diagram…

You can't patch your way from a passive average to a fighter. The policy itself has to change — which means reinforcement learning, with everything the literature says about how RL destroys the thing you're trying to preserve.

Chapter 4 — RL with Alan inside the objective

Warning

This chapter documents the pre-v10 RL experiments. Their control and reward lessons remain useful, but their Alan prior was trained from the invalid old trace contract. The v10 program returns to causally correct offline imitation first; no RL result below is a current Alan-fidelity baseline.

The naive pivot fails predictably: reward damage-and-wins and the policy drifts into whatever degenerate strategy beats the opponent, shedding every trace of the human it cloned. So the objective carries three tethers:

J(π)=E[Rgame]  +  ws(ϕtϕt1)    βDKL(ππAlan)  +  λLanchorJ(\pi) = \mathbb{E}\big[R_{\text{game}}\big] \;+\; w_s\,\big(\phi_t - \phi_{t-1}\big) \;-\; \beta\, D_{KL}\big(\pi \,\|\, \pi_{\text{Alan}}\big) \;+\; \lambda\, \mathcal{L}_{\text{anchor}}

where ϕt=d(bt,bAlan)\phi_t = -\,d\big(b_t, b_{\text{Alan}}\big) is a potential over the distance between the bot's rolling 9-component behavior vector and Alan's measured profile, and Lanchor\mathcal{L}_{\text{anchor}} is an advantage-weighted cross-entropy on Alan's own best replay moments — because the frozen prior is his passive average, but the dataset contains his wins.

This design came out of ~34 adversarially-verified arXiv papers (every ID fetch-checked; the ones that changed decisions: PostBC on coverage, FASTER on receding horizons, "KL-Regularized RL is Designed to Mode Collapse" on why the naive dial was dead on arrival, and behavior-conditioned PPO — the basis for the style term).

Here is that historical target with its actual field meanings preserved. These channels may overlap on one frame, so they do not sum to 100%. down is not a synonym for blocking, and up is not a jump-event counter.

Alan's historical behavior target · 400-replay artifact
  • Attack press2.14% of frames

    between-replay standard deviation ±0.54

  • Multiple attack buttons4.53% of frames

    ±2.01; a separate held-input channel

  • Toward opponent held25% of frames

    ±7.03

  • Away from opponent held31.94% of frames

    ±8.23

  • Down held23.05% of frames

    ±8.78; crouch or crouch-block context is not distinguished here

  • Up held8.73% of frames

    ±2.96; held direction, not jump frequency

  • No direction or attack28.1% of frames

    ±10.62

Historical pre-v10 reward target—not current fidelity evidence. Exact labels replace the earlier, misleading attack/jump/block shorthand.

The shape still matches the defensive, distance-controlling player people remember, but the labels now say only what the artifact measured. A 68-guide GameFAQs corpus from his own era independently names many of the same tactical ideas; that agreement is context, not a substitute for the blinded evaluation.

Chapter 5 — The honest scoreboard

The scoreboard below was honest about its original arcade-CPU protocol, but it predates the causal trace audit. It measures that historical agent's strength, not whether the v10 policy resembles Alan.

The RAM archaeology chapter first: the byte the whole match-end detector trusted, 0xFF4008, read 0x99 in every replay-derived savestate — so it was mapped as a "fighting" flag. A fresh capture read 0x92… then 0x93. Then a new run fell: 0x88, 0x87, 0x86. The truth, settled by reading the byte as a time series: it's the round timer, in binary-coded decimal, counting down from 99. One mislabeled byte had quietly ended every arcade training match ~10 game-seconds in — run v1's "wins" were really "who's ahead on health 10 seconds in." The kicker: the game's real timeout rule awards the round to the HP leader, so the synthesized outcomes were accidentally aligned with the real rules. Sometimes you get lucky in the exact shape of your bug.

Run v2, with the timer bug dead, played full matches against a two-stage arcade-CPU curriculum. The honest result:

RL v2 win rate by training window
  • iterations 0–4950.42% win rate

    121 wins / 119 losses

  • 50–9949.6% win rate

    125 / 127

  • 100–14950.42% win rate

    119 / 117

  • 150–19947.3% win rate

    114 / 127

  • 200–26742.68% win rate

    140 / 188; training longer made it worse

Historical pre-v10 arcade-CPU outcome. The match accounting is valid; the Alan prior underneath it was not.

Flat, then declining. Entropy healthy, no token locks, style distance oscillating instead of converging. That plateau is evidence: more PPO on this prior will not produce a winner. The bot blocks more than it ever did, presses buttons it literally couldn't press a month ago, and loses full matches it used to "win" by phantom timeout — progress of the unglamorous kind, the kind you only get by auditing your own work adversarially (two campaigns, 27 confirmed bugs) and believing live evidence over dashboards.

Chapter 6 — v10: fix the contract before the model

The July experiments above ran, and some produced interesting negative results. Then a more fundamental audit invalidated the prior they all shared. That is why v10 starts by freezing causality and provenance, not by adding another loss term.

The earlier eval-harness audit remains the historical record of the July failures. This article is the single canonical home for the current Alan bot, including every later correction.

recorded action a_i  -> FBNeo emulates frame i -> observed post-frame state s_i
live policy sees s_i -> earliest possible output is a_(i+1)
 
training sample: s_i -> [a_(i+1), a_(i+2), ... a_(i+16)]

Seventeen contiguous in-fight rows are required for one sample. Idle is kept. Gaps, side changes, round boundaries, mismatched observed controls, and partial tails are rejected. Both players' inputs are replayed in lock-step. Live FBNeo controls and offline GGPO masks cross exactly one named conversion boundary; directions are mirrored from the immediately preceding observed facing.

The frozen candidate contains 7,332 validated traces from 7,554 catalogued replays (97.06%; 222 missing) and 18,046,213 causal 16-frame chunks. Its residual VQ-VAE completed all 30 epochs, and the causal token and 268-feature state caches were built. The developer-menu audit then proved that two entries in that state cache carried the wrong semantics: the byte at fighter-object +$80 is a latched seven-bit move-property row code, not an LP/MP/HP attack strength. Schema v9 keeps the width at 268, corrects the names and normalization, and deliberately invalidates the v8 cache. At this August 7 snapshot, the autoregressive VQ-BeT decoder had initialized but had not produced its first epoch checkpoint; training is held until the schema-v9 state cache is rebuilt. The decoder, source-bound prior, deployment bundle, and held-out behavioral evaluation therefore remain unfinished. A low reconstruction loss is only the motor-codebook ceiling; it is not proof of Alan-like decisions.

replay-disjoint split

18 million action chunks, split by whole replay

No replay is allowed to put neighboring windows on both sides of validation.

train · 6,868 replaysvalidation · 362 replays
  1. decoder corpus18,046,213 chunks7,230 represented replays

    These are overlapping causal windows, not 18 million independent decisions or matches.

Exact decoder split manifest: 17,145,521 training chunks and 900,692 held-out chunks.

motor codebook

Thirty epochs compressed the action language

Reconstruction error falls quickly, then settles into a narrow validation band.

train reconstructionvalidation reconstruction
Thirty epochs compressed the action languagetrain reconstruction: 30 observations, from 0.0202685784 at training epoch 0 to 0.0013310952 at training epoch 29. validation reconstruction: 30 observations, from 0.0049745597 at training epoch 0 to 0.0013424957 at training epoch 29. Annotations: best validation · epoch 28 at training epoch 28, reconstruction loss · log scale 0.0013389821.0.00110.00520.0301529best validation · epoch 28training epochreconstruction loss · log scale
Best validation reconstruction: 0.001338982 at epoch 28. This proves a compact motor vocabulary can reconstruct held-out action chunks; it does not prove Alan-like choices.

release gate

Built is not the same as proven

The candidate advances only when the artifact at each gate is present and independently verifiable.

nativeverifiedbridgeddonorpending
routetrace freezecausal chunksmotor VQtokens + v9 statedecoderbundle + evalblind FGC
v10 candidateverified7,332 alignment-v2 tracesverifiedtarget offset +1verified30 epochs completependingtokens built; invalid v8 state cache must be rebuilt under schema v9pendinginitialized; no epoch checkpointpendingno deployable manifest or held-out resultpendingno blinded whole-match verdict
Green means a finished, hash-bound artifact—not merely a directory, log line, or stale pipeline status.

Capcom left an instrument panel inside the ROM

The public starting point was Ryou's CPS2 Debug Switches and the Games That Love Them. It documents the shared Marvel-family menu through X-Men vs. Street Fighter, then calls out the two game-specific last tools: MSHvSF adds STAR TEST; MvC adds HIT TEST. The consolidated CPS-2 Character Lab is the second witness: clean stock-ROM MAME runs, controlled input sweeps, exact glyph decoding, ROM correlation, and live RAM probes against the Fightcade revision.

Evidence boundary // family resemblance is not a memory map

The linked article establishes the menu family and documents XvSF's controls and debug-switch effects. It does not prove that XvSF's 24-switch map can be copied bit-for-bit into MvC. Every statement below is labelled as external family evidence, locally verified MvC evidence, or unresolved. The debug menu is an oracle only where two independent witnesses agree.

Marvel vs Capcom SECRET TEST MENU with HIT EDITER, CATCH EDITER, ENEMY SENSEI, ENDING TEST, MESSAGE TEST, four scroll tools, KAO TEST, HIT TEST, and EXIT
MvC // twelve entries; HIT TEST occupies slot B
Marvel vs Capcom HIT EDITER with two fighters on a red coordinate grid and live animation names, frame counts, graphics pointers, flip flags, and HIT slot values
HIT EDITER // engine names and live fields, not a ready-made replay label

capability census

Same twelve-slot shell, one decisive game-specific swap

The categories describe safe use in this project—not how important the tools may have been inside Capcom.

training candidatesengine diagnosticsasset / identity viewersnavigationunresolved
  1. Marvel vs. Capcom12 menu entriesHIT TEST

    Candidates: HIT EDITER, CATCH EDITER, HIT TEST. Diagnostics: ENEMY SENSEI plus the four SCRL tools. None becomes supervision without a verified join.

  2. Marvel Super Heroes vs. Street Fighter12 menu entriesSTAR TEST

    STAR TEST replaces MvC's HIT TEST. Both the public research and the local reproduction reach a black, input-inert screen that can still return to the menu.

A menu entry is not automatically a policy feature. The green segment means worth joining and validating, not safe to train on today.
The only two routes from Capcom's screens into causal state
Drawing the diagram…
Every route needs a ROM-specific join. The rest of the menu stays in the diagnostic lane.
ToolSafe role hereWhat it can contributeBoundary that stays visible
HIT EDITERsemantic oracle / training candidateCapcom's animation name, Char Gp, position, frame counters, live CG_Ptr, flip state, and HIT_0…HIT_7 box IDs; useful for move phase, startup/active/recovery, and named-action validationThe name index is not yet proven to equal the animation tree's group index; the hit slots are IDs, not box coordinates
CATCH EDITERsemantic oracle / training candidateSynchronized CATCH nn and HOLD nn programs, relative positions, and frame progression for thrower and victim; useful for throw-state and tech-window validationIt does not expose the separate live throw-range hook, and a menu pair is not evidence Alan attempted that throw
ENEMY SENSEIengine diagnosticRuns TAIKI, OKIAGARI, and LIBRARY table checks and carries explicit malformed-command / bad-parameter errors; potentially a validator for altered fighter and CPU dataIts exact implementation is still unresolved. It describes Capcom's CPU/fight-engine tables, not Alan's decisions
ENDING TESTasset-onlyCharacter-id and ending-variant routing, useful for catching roster and identity-table driftStory playback supplies no combat supervision
MESSAGE TESTasset-onlyMessage-bank ids, option paths, and language/string integrity; useful for ROM-build regression testsText content supplies no policy signal
SCRL MOVE TESTdiagnostic, unresolved detailA possible live camera/scroll-motion probe for super-jump tracking and world-to-screen transformsThe linked XvSF investigation reaches a black screen; local MvC navigation reaches the tool, but its fields are not fully decoded
SCRL 1 BLOCKstage diagnosticScroll-plane 1 bank/image browsing; candidate reference for foreground/background origin and wrap checksThe public XvSF result is partly broken and cannot be treated as an MvC address map
SCRL 2 BLOCKstage diagnosticScroll-plane 2 bank/image browsing; useful for camera-parallax and stage-boundary testsVisual/asset evidence, not player intent
SCRL 3 BLOCKstage diagnosticScroll-plane 3 bank/image browsing; useful for independently checking layered camera transformsVisual/asset evidence, not player intent
KAO TESTasset / identity diagnostickao means face: the portrait browser can verify character-id, face tile, and palette joins—the same identity needed to select the right property tablePortrait correctness does not prove gameplay-state correctness
HIT TESTMvC onlyproperty oracle / training candidateA 128-selector UI page per stock character; the underlying $20-byte property records now have eight exact field offsetsIt is not a hitbox-coordinate viewer, physical table lengths vary, tail selectors can expose adjacent ROM data, and row id is not the animation-name index
STAR TESTMSHvSF onlyunresolvedEstablishes a real version difference in the shared menuPublic and local runs show a recoverable black screen; no training or diagnostic claim is justified yet
EXITnavigationReturns from the secret menu to the surrounding service flowNo data product

HIT TEST is a move-property table, not a box drawing

The public source carefully described HIT TEST as a grid “possibly related to hit boxes.” The full local sweep resolves the ambiguity. MvC renders 23 character pages × 128 selector cells = 2,944 captured UI cells. That is the screen's addressable selector envelope—not 2,944 valid move records. Physical property tables vary by character; alternate forms may share pointers, and unsafe tail selectors can run into adjacent tables or trailer ROM data. Correlating 125 independently decoded, in-range records against the program ROM produced one exact layout: a $20-byte stride with all eight displayed fields at fixed offsets.

The selector always reaches 128; the physical tables do not
  • HIT TEST UI selector envelope128records / selector cells

    capture ceiling on every character page; not a validity guarantee

  • Ryu physical property table127records / selector cells

    Alan's partner; nearly fills the selector space

  • Spider-Man physical property table103records / selector cells
  • Morrigan physical property table84records / selector cells

    Alan's point character; selectors 84–127 are not valid Morrigan move rows

  • War Machine physical property table71records / selector cells
  • Dachi template physical table30records / selector cells

    the shortest verified example; the visible page still offers all 128 selectors

Selected verified lengths. Onslaught/Hulk2 tails remain unsafe, and alternate characters may share a base pointer. Every lookup must carry a character-specific bound.
One MvC HIT TEST move-property record
8AB$000000$000020
  • 1
    Dmg · +$00
    $000000$000001 · 1 B
    HP damage.
  • 2
    Piy · +$06
    $000006$000007 · 1 B
    Dizzy contribution.
  • 3
    Sto · +$07
    $000007$000008 · 1 B
    Hitstop.
  • 4
    Snd · +$08
    $000008$000009 · 1 B
    Standing hit-stun duration; despite the label, this is not a sound id.
  • 5
    Slp · +$09
    $000009$00000A · 1 B
    Prone/knockdown recovery seed.
  • 6
    Shc · +$10
    $000010$000011 · 1 B
    Super-meter charge/gain.
  • 7
    Grd · +$18
    $000018$000019 · 1 B
    Guard-stun override; zero asks the engine to derive its normal value.
  • 8
    Mut · +$1A word
    $00001A$00001C · 2 B
    Low six bits seed the multi-hit rehit-vulnerability window. It is not post-hit invulnerability.
  • A
    +$12 attack-box selector
    $000012 · marker
    Hidden from the HIT TEST columns; geometry lives in the separate box tables.
  • B
    +$14 attack-box selector
    $000014 · marker
    A selector, not coordinates.
Exact ROM correlation across 125 decoded, in-range records. The hidden +$12/+14 selectors choose attack boxes; their geometry is a different data source.

The row number lives in its own move-property id space. Two independent tests rejected the tempting shortcut “row id = animation-name or pattern index,” so the data does not get move names pasted onto it. The safe join is a stock character's table pointer and verified physical length plus the seven-bit code the fight engine latches at fighter object +$80; any out-of-range code must fail closed.

Why +$80 could never have been LP / MP / HP strength
  • P1 values greater than 281.87% of sampled rows

    108 distinct codes in the first 16 full-candidate row groups

  • P2 values greater than 285.15% of sampled rows

    95 distinct codes in the same sample

  • Nonzero codes outside canonical strike frames96% of sampled rows

    approximately 96%; evidence that the byte is latched move context, not attack-box liveness

The old state builder divided this byte by two and clipped it, collapsing almost the entire move-property vocabulary to 1.0.
Schema v9 correction // training is intentionally held

The live and offline builders now expose indices 210/211 as self_move_property_code_norm and opp_move_property_code_norm, computed as (raw & 0x7F) / 127. They are deliberately not gated on an active strike: the code persists through move context, while attack liveness comes from the separate canonical collision gates. Historical trace/parquet wire fields keep their old p*_atk_strength names for compatibility, but the model-facing meaning is corrected. Schema v9 carries the normalized selector only; it does not silently add the eight decoded static fields. A bounds-checked static lookup is the next controlled A/B experiment after provenance and live/offline parity gates. The existing schema-v8 state cache is invalid; it must be rebuilt before decoder training resumes.

The two editors can name behavior—after a join

HIT EDITER exposes Capcom's animation/group label, group value, position, frame counters, live CG_Ptr, flip flags, and eight hit slots. CATCH EDITER exposes synchronized CATCH nn / HOLD nn pairs for thrower and victim. ENEMY SENSEI exposes named idle, wake-up, and library-table passes plus error messages that make it promising as a validator, not a replay-label source.

The automated stock-MvC sweep's main roster rows contain 30 distinct (label, CG_Ptr) HIT observations and 31 CATCH observations for Morrigan, and 30 HIT / 12 CATCH for Ryu. Those are deduplicated observations from an oversampled editor sweep—not proof that either character has exactly that many complete moves.

What the editor sweep actually observed
  • Morrigan · HIT EDITER30deduplicated observations
  • Morrigan · CATCH EDITER31deduplicated observations
  • Ryu · HIT EDITER30deduplicated observations
  • Ryu · CATCH EDITER12deduplicated observations
Main-row, stock-MvC sweep observations. Oversampling and deduplication make these evidence of editor coverage, not complete move inventories.

The live collision representation is more immediately actionable:

MvC fighter object · collision-relevant fields
out of bounds$0D75E8$000000$0000B8
  • 1
    +$06…+$07 · hurt/reaction code
    $000006$000008 · 2 B
    Observed values 0/4/8/12 are preserved; only code 8 has a documented grabbed meaning.
  • 2
    +$6C…+$6F · box-table pointer
    $00006C$000070 · 4 B
    Resolves box ids into signed center offsets and unsigned X/Y radii.
  • 3
    +$70…+$73 · two attack ids
    $000070$000074 · 4 B
  • 4
    +$74…+$7B · four vulnerability ids
    $000074$00007C · 8 B
  • 5
    +$7C…+$7D · defender throwable region
    $00007C$00007E · 2 B
    Vulnerability to throws, not an active grab-range box.
  • 6
    +$82…+$83 · viewer gates
    $000082$000084 · 2 B
    Necessary inputs to canonical classification, never sufficient on their own.
  • 7
    +$B4 · pushbox mode
    $0000B4$0000B5 · 1 B
    Byte values 0 ground, 2 super-jump air, 4 normal-jump air, 6 knockdown/hit.
  • !
    ROM throw-check hook
    $0D75E8 · out of bounds (past $0000B8)
    The canonical viewer creates active throw range here, outside the fighter object and current raw trace.
One fighter object, four semantic traps // scroll to isolate each contract
box identity

Attack ids at +$70/+$72, four vulnerability ids at +$74…+$7A, and the pointer at +$6C are one system. An id is resolved through the table into signed center offsets plus unsigned X/Y radii.

throw semantics

+$7C belongs to the defender and means throwable vulnerability. The active throw box comes from a separate ROM hook at $0D75E8, which the raw replay trace does not yet capture.

strike semantics

A damaging strike requires an open +$82 gate, a nonzero low-bit attack id, and sanitized nondegenerate geometry. High-bit marked geometry is kept in a separate potential-throw-box channel and never drives reach or overlap.

movement semantics

+$B4 is one byte of pushbox mode, not stance or facing: 0 ground, 2 super-jump air, 4 normal-jump air, 6 knockdown/hit.

That last correction matters: +$B4 is the engine's pushbox selector, not stance or facing. Its observed modes are 0 ground, 2 super-jump air, 4 normal-jump air, and 6 knockdown/hit. Together the eight collision roles are push, four vulnerable slots, throwable, and two attack slots. Nonzero bytes at +$82/+$83 are not, by themselves, evidence that an attack is live. Starting with schema v8, a slot is a damaging strike only when its raw ID is nonzero with bit 0x8000 clear, its sanitized geometry is nonzero, and the +$82 gate is open. Only that strike mask drives attack presence, reach, overlap, pressure, and the tactical governor; schema v9 retains this contract. Crucially, +$7C describes whether the defender can be thrown; it is not an active grab-range box. Fightcade's canonical training-mode script creates the active-throw box separately at a ROM throw-check hook, and that signal is not yet resolved in the raw trace. That collision revision grew the state to 268 ordered features; schema v9 keeps the same width while correcting the separate +$80 move-property semantics. A potential-throw-box signal requires attack-ID bit 0x8000, sanitized nonzero geometry, the +$82 gate, and +$06 == 0, matching the canonical viewer's hurt suppression. It is box/type presence—not intent, an attempt, or a connection. The same schema preserves all observed +$06 hurt codes (0/4/8/12) and exposes the documented code-8 grabbed state without inventing a meaning for code 4.

Why marked collision geometry had to be split
  • self · potential-throw-marked box present59.1% of fight rows
  • opponent · potential-throw-marked box present58.77% of fight rows
  • self · damaging strike box present5.38% of fight rows
  • opponent · damaging strike box present7.19% of fight rows
  • self strike · geometric overlap2.2% of fight rows
  • opponent strike · geometric overlap2.32% of fight rows
115,593 validated opening-band fight rows (fighting byte 0x90–0x99) from the first 16 full-candidate row groups. This is a semantic smoke test, not a population estimate: the ~59% marked-box prevalence is exactly why that channel cannot mean throw intent or an active damaging strike.

This lets v10 derive box presence, overlap margins, contact approach, throw-vulnerability windows, marked attack-slot states, and corner/push spacing from the same replay frame the policy sees without pretending to measure grab reach.

The editor names are not training labels yet. Char Gp has not been proven to equal the animation tree's L1 index, and the name-index-to-tree mapping is still unresolved. They become safe replay labels only after a stock-ROM-hash- specific join from CG_Ptr and frame position into the live traces. Until then, replay RAM geometry is supervision; the editor is a reference oracle.

What the newer arXiv sweep changed

Behavior Transformers and VQ-BeT support modeling multiple action modes instead of averaging turtle, movement, pressure, assist, and super play into one median action. Keyframe-Focused Imitation and the 2026 revision of Balanced Behavior Cloning justify measuring and reweighting rare decisive states—but only after causal traces exist. OCAtari is the closest architectural analogy for extracting object-centric game state rather than learning from pixels; MvC's native fighter, projectile, and box structs let this project do that without a vision detector.

BeTAIL motivates a future bounded residual for states outside the replay distribution, while Adaptive Q-Chunking makes a particularly relevant prediction: near contact, shorter commitments can be better; in free space, longer chunks can preserve coherent plans. Neither is implemented in the current candidate. They are closed-loop experiments to attempt only after the imitation policy passes its offline and local gates.

Finally, fighting-game style needs its own evaluation. The Universal Fighting Engine study evaluates human/AI style and diversity, while the Navigation Turing Test and Navigates Like Me show why an automated discriminator is only a diagnostic. The release gate here is randomized, blinded whole-match comparison against held-out Alan footage, with confidence, judge rationales, uncertainty, and controls—not a cherry-picked clip.

The replay collector is now part of the evidence chain

The Chrome extension was rebuilt as Mission Control v8.0.0. SQLite is the authority for durable jobs, receipts, events, and controls; one same-origin Web Lock owns each scope; Cloudflare cooldowns are explicit; completion requires two clean receipt-bound terminal sweeps; and discovery, queue, download, and faithful-trace counts are no longer collapsed into one flattering number. At the August 7 snapshot, the MvC frontier held 253,234 known rows across 19,097 players and the SFIII database held 2,512 rows across 1,748 players. Frontier rows are discovery evidence, not automatically playable or faithful traces.

Mission Control v8 keeps collection state outside the tab
Drawing the diagram…
Fightcade remains the authenticated executor. SQLite owns durable truth, receipts, pause authority, and completion evidence.

The health HUD now has an explicit delivery contract

The August 7 UI work hardened how live state reaches Mission Control. A view binds to the owning run id instead of whichever output happens to look current; its JSONL follower can attach after a file appears and continue across rotation; and the SSE path sends keepalives and reconnects without silently abandoning the run. In the browser, a monotonic-revision reducer rejects older snapshots so a delayed event cannot roll HP backward, while a 2.5-second freshness window exposes a visible stale signal instead of leaving an old health bar looking live. Pushbox-mode labels now use the verified engine meanings—ground, super-jump air, normal-jump air, and knockdown/hit. These are delivery and observability guarantees, not live-emulator accuracy proof: end-to-end emulator validation is still pending, and the underlying RAM mapping remains the boundary on whether the displayed health and state are actually correct.

A health change reaches the HUD only through a monotonic chain
Drawing the diagram…
Reconnects may replay old lines; the reducer rejects them instead of letting HP roll backward.

The rebuild order, and the stop condition

The v10 runner is content-addressed and resumable, but the old one-line resume command is intentionally withdrawn at this snapshot. The schema-v8 state cache was built before the +$80 correction; resuming the decoder against it would either fail the manifest contract or, worse, train against the collapsed feature. The safe order is verify no duplicate process → rebuild only the frozen candidate's schema-v9 state cache → verify version, dimension, ordered- name hash, and offline/live parity → resume the existing decoder in a persistent session. Do not launch the top-level pipeline, which would create a different candidate rather than repair this one.

The production path still refuses to train until every catalogued replay validates. The existing Spar with the Ghost browser arena remains a historical sandbox built from the older policy; it is not the v10 candidate and is not evidence that the memorial bot is finished.

The real stop condition is stricter: manifest verification, replay-held-out metrics, deterministic local-match probes, stratified closed-loop evaluation, then blinded judgments from people who knew Alan's play. Only after those gates pass can this project say the ghost moves like the man.

Why this is AI-for-good, not AI-for-novelty

Communities like the FGC are cultural archives. They preserve game knowledge, matchup theory, and individual playstyles that exist nowhere else. When a player who shaped that culture is gone, the knowledge he embodied goes with him — unless you have the recorded games and the will to build something with them.

The FGC isn't going to get a memorial bot from a big research lab. If anyone builds a behavioral clone of alanmargolies88 — or of the other players the community has lost — it's the people inside the community who knew them and have the technical chops to do it. The infrastructure here is open-source and the pipeline generalizes to any fighting game with recorded matches.

The goal is for nobody who was important to a community to have to disappear from it completely just because they're gone.

That's the project.

Built with FBNeo, MLX, PyTorch, and an unreasonable number of verified arXiv abstracts. Final FGC validation is still a release gate. EOF.

Written by Daniel Plas Rivera · 7,743 words · $0C

ShareXLinkedIn