Summary
Analysis of frame-by-frame prediction errors from demo playback reveals three categories of systematic model failures traceable to missing input features. The raw Slippi replay data contains the signals needed to fix them — they were excluded during initial encoding design but should be reconsidered.
Evidence
Single-match error log (~7800 frames) shows:
| Error category |
Frequency |
Severity (pos error) |
Root cause |
| Death → respawn transition |
4 per match |
226–316 units |
No death/respawn state signal |
| Knockback trajectory drift |
~30 sequences |
3–22 units |
No hitstun countdown |
| Offscreen death position drift |
~200 frames total |
5–7 units sustained |
Model doesn't know death resting position |
| Shield/hitlag transitions |
~15 instances |
0.4–1.9 units |
Missing hitlag/shield interaction flags |
The death→respawn errors are the largest in the entire match by an order of magnitude.
Missing features
1. state_flags (5 bytes, currently unused)
Raw arrow struct: post.state_flags.{0,1,2,3,4} — 5 uint8 fields, all populated with real data.
Investigated on a real replay (13,607 frames). Key bit meanings decoded:
Byte 4 (bits 6–7): death state — clean 2-bit encoding:
0b11 (192) = dead (only with action states DEAD_DOWN, DEAD_LEFT)
0b10 (128) = dying/in kill trajectory (knockback fly states)
0b01 (64) = respawning (ENTRY, ON_HALO_DESCENT)
0b00 (0) = alive and normal
Byte 1: combat interaction
- Bit 5 (
0x20) = in hitlag — perfectly correlated with hitlag > 0
- Bit 0 (
0x01) = shielding — correlates with shield < 60
- Bit 3 (
0x08) = appears during attack animations (active hitbox?)
Byte 3: movement state
- Bit 1 (
0x02) = in knockback/hitstun — distinct from just "airborne"
- Bit 7 (
0x80) = appears across many action states (possibly "actionable" flag)
Old decision notes said: "Most of these are inferrable from action_state + character. Not worth the encoding complexity." This was wrong — byte 4's death state is NOT inferrable from action_state alone (the model currently proves this by failing at every death transition), and byte 3 bit 1 distinguishes "airborne and can act" from "airborne and in knockback" which is a critical distinction the model doesn't have.
Recommendation: Expose ~6 individual binary features extracted from specific bits, not raw byte embeddings.
2. misc_as / hitstun (currently unused — believed to be garbage)
Raw arrow struct: post.misc_as — float field. Contains hitstun frames remaining.
Old decision notes said: "Data quality issue. Produces garbage values like 5.6e-45 (bit-reinterpreted)." This is half right. Investigation shows:
- ~42% of non-zero values are subnormal floats (e.g. 5.6e-45) — these are integers bit-reinterpreted as float32. Reinterpreting the bytes back as uint32 recovers clean values: 1, 2, 3, ... 20+.
- ~58% of non-zero values are valid floats, 90.9% of which are integer-like.
- Combined, we get a clean countdown timer: e.g. frames 330–349 show hitstun counting down 33→14 perfectly.
Recovery method:
raw = post.misc_as.to_numpy() # float32
subnormal = (np.abs(raw) < 1e-10) & (raw != 0)
hitstun = np.where(subnormal,
np.frombuffer(raw.tobytes(), dtype=np.uint32).view(np.float32), # reinterpret
raw)
This directly addresses knockback trajectory errors. The model currently doesn't know how long a character will be in hitstun — hitstun is a frame countdown that tells it exactly when the character regains control.
3. flags (post.flags) — confirmed absent
Field does not exist in the peppi_py arrow schema. The Slippi spec defines it but the Rust parser doesn't populate it. Nothing to recover. Can be removed from the "unused fields" tracking.
Impact on specific error patterns
| Model failure |
Feature that fixes it |
| DEAD_LEFT → ON_HALO_DESCENT (226+ unit error) |
state_flags byte 4 death/respawn bits |
| 50-frame position drift at death location |
state_flags byte 4 (model would know player is dead, position frozen) |
| Knockback trajectory undershoot (3–22 units) |
Hitstun countdown (model knows knockback duration) |
| SHIELD_STUN → SHIELD_REFLECT confusion |
state_flags byte 1 hitlag/shield bits |
| Landing frame off by 1–3 frames |
Possibly byte 3 bit 1 (knockback vs normal airborne) |
Proposed encoding additions
New EncodingConfig flags, all defaulting to False:
state_flags: bool = False # 6 binary features from state_flags bits
hitstun: bool = False # 1 continuous feature (normalized countdown)
When state_flags=True, add per-player binary features:
is_dead (byte4 bits 6–7 == 0b11)
is_dying (byte4 bit 7)
is_respawning (byte4 bit 6 only)
in_hitlag (byte1 bit 5)
in_knockback (byte3 bit 1)
is_shielding (byte1 bit 0)
When hitstun=True, add per-player continuous feature:
hitstun_remaining scaled by ×0.02 (range 0–50ish → 0–1)
Total: +7 floats per player when both enabled. Current baseline is 32 floats/player.
Scope
Requires changes to:
build_dataset.py / parse_archive.py — extract state_flags bytes and misc_as from raw arrow struct (same pattern as item extraction)
parse.py — add fields to PlayerFrame dataclass
encoding.py — new config flags, encoding logic
dataset.py — pass new fields through
Does NOT require model architecture changes — these are additional input features in the continuous/binary tensor.
Priority
High — the death→respawn error is the single largest systematic failure mode in the current model, and state_flags byte 4 is a direct fix. Hitstun recovery is the second highest value for knockback prediction.
Summary
Analysis of frame-by-frame prediction errors from demo playback reveals three categories of systematic model failures traceable to missing input features. The raw Slippi replay data contains the signals needed to fix them — they were excluded during initial encoding design but should be reconsidered.
Evidence
Single-match error log (~7800 frames) shows:
The death→respawn errors are the largest in the entire match by an order of magnitude.
Missing features
1.
state_flags(5 bytes, currently unused)Raw arrow struct:
post.state_flags.{0,1,2,3,4}— 5 uint8 fields, all populated with real data.Investigated on a real replay (13,607 frames). Key bit meanings decoded:
Byte 4 (bits 6–7): death state — clean 2-bit encoding:
0b11(192) = dead (only with action states DEAD_DOWN, DEAD_LEFT)0b10(128) = dying/in kill trajectory (knockback fly states)0b01(64) = respawning (ENTRY, ON_HALO_DESCENT)0b00(0) = alive and normalByte 1: combat interaction
0x20) = in hitlag — perfectly correlated withhitlag > 00x01) = shielding — correlates withshield < 600x08) = appears during attack animations (active hitbox?)Byte 3: movement state
0x02) = in knockback/hitstun — distinct from just "airborne"0x80) = appears across many action states (possibly "actionable" flag)Old decision notes said: "Most of these are inferrable from action_state + character. Not worth the encoding complexity." This was wrong — byte 4's death state is NOT inferrable from action_state alone (the model currently proves this by failing at every death transition), and byte 3 bit 1 distinguishes "airborne and can act" from "airborne and in knockback" which is a critical distinction the model doesn't have.
Recommendation: Expose ~6 individual binary features extracted from specific bits, not raw byte embeddings.
2.
misc_as/ hitstun (currently unused — believed to be garbage)Raw arrow struct:
post.misc_as— float field. Contains hitstun frames remaining.Old decision notes said: "Data quality issue. Produces garbage values like 5.6e-45 (bit-reinterpreted)." This is half right. Investigation shows:
Recovery method:
This directly addresses knockback trajectory errors. The model currently doesn't know how long a character will be in hitstun — hitstun is a frame countdown that tells it exactly when the character regains control.
3.
flags(post.flags) — confirmed absentField does not exist in the peppi_py arrow schema. The Slippi spec defines it but the Rust parser doesn't populate it. Nothing to recover. Can be removed from the "unused fields" tracking.
Impact on specific error patterns
Proposed encoding additions
New
EncodingConfigflags, all defaulting toFalse:When
state_flags=True, add per-player binary features:is_dead(byte4 bits 6–7 == 0b11)is_dying(byte4 bit 7)is_respawning(byte4 bit 6 only)in_hitlag(byte1 bit 5)in_knockback(byte3 bit 1)is_shielding(byte1 bit 0)When
hitstun=True, add per-player continuous feature:hitstun_remainingscaled by ×0.02 (range 0–50ish → 0–1)Total: +7 floats per player when both enabled. Current baseline is 32 floats/player.
Scope
Requires changes to:
build_dataset.py/parse_archive.py— extract state_flags bytes and misc_as from raw arrow struct (same pattern as item extraction)parse.py— add fields toPlayerFramedataclassencoding.py— new config flags, encoding logicdataset.py— pass new fields throughDoes NOT require model architecture changes — these are additional input features in the continuous/binary tensor.
Priority
High — the death→respawn error is the single largest systematic failure mode in the current model, and state_flags byte 4 is a direct fix. Hitstun recovery is the second highest value for knockback prediction.