# Separate penalty frequency — 2026-10-31-r8

## Menu and rule discovery

The last start-menu row is **Penaltys: 100% / 50% / Off**, default 100%.
The pre-existing combined Penalties option is renamed **Rules**. Its three
original values remain Off, On, and On, Except Off-sides. It still controls
native foul eligibility and whether offside and awarded penalty shots are active.
The percentage is independent. 100% retains the existing frequency under the
chosen Rules; it does not turn every check into a foul. Rules Off still blocks
the native calls it already blocked. Icing keeps its own separate On/Off row.
The [initial discovery](https://dela.jdc.se/nhl94/notes.html#PENALTYS_RESEARCH.md)
is retained, with the original source locations and explanation.

50% uses one native random draw for each eligible ordinary foul, retaining
approximately half. Off admits none of those new ordinary calls. A free queue
slot and no already pending offender are required before drawing. Duplicate,
blocked and full-queue requests retain their native behavior without rerolling.
The same setting applies to both teams, CPU and human players, at every difficulty.
Already queued penalties finish normally; this menu is not a cancellation tool.

Ordinary events are elbowing `0x12` and charging `0x16`, slashing `0x18`, roughing `0x1A`,
cross-checking `0x1C`, hooking `0x1E`, tripping `0x20`, interference `0x22` and holding `0x24`, in hex.
An injury-associated elbowing call suppressed by the percentage becomes native
zero-duration event `0x14`, the existing injury-without-penalty report. The injury
stoppage remains. Injury and checking physics are not changed.

Fighting must remain one incident: 50% gates an entire eligible new fight after
the original fighting-rating and HV-counter chance checks. Off prevents new
fights even when the independent Fighting choice remains saved as On. 100% keeps
the existing Fighting setting and chance. Fighting `0x26/0x28` and instigator `0x2A` awards
are never filtered individually, so paired resolutions retain their original code.
A fight already underway can finish normally. Icing `0x0C`, offside `0x10`, faceoffs,
other zero-duration events and negative-duration penalty-shot variants are not
percentage-filtered. P1–P4 awarded-shooter ownership is unchanged.

All five percentage/Height/Speed/Mode/Cone choices save independently for each
Full/Light/Mini season and lock after its first completed match. The lock survives
reboot, slot changes and playoffs. Confirmed restart resets only that season;
standalone preferences remain independent and editable. Controller mappings,
pass behavior and the always-switch-nearest-other policy retain their code.

## Native hooks and storage

| ROM offset | r7 bytes | r8 bytes | Purpose |
|---|---|---|---|
|11F78|067800C8B89C|4EF9002EC800|High-event admission, after existing native guards|
|2004F0|284B7005267CFFFFB04A|4EF9002EC8044E714E71|Whole-fight commit after original HV chance|
|2EC800–2ECFFF|Free|Bounded helper|Exports+0 event admission,+4 whole fight|
|2E1088|610000DC4A40|40E7007C0700|Protect the human owned contact-to-shot transition|
|2E1260|Penalties label|Rules label|Same native D056 option and values|
|2E1310 /2E1484|Free|Penaltys label /100%,50%,Off strings|Eighteenth row, logical34|

The helper preserves registers and SR around classification. Accepted calls replay
native ADDI#200,B89C and rejoin11F7E; suppressed ordinary calls unwind the existing
native frame at11FEC. Injury-only reports use event `0x14` and that native continuation.
100% and invalid values draw no filter RNG.50% calls native11086 with bound2 once
for eligible events or a whole fight. Existing sound, queue, delayed-penalty flags,
duration, clock, PIM and scoring paths run for admitted calls. Native Rules and
penalty-shot checks, original fight eligibility/HV read, icing and pass bodies
remain byte-exact. Extra instructions can affect later native HV-based decisions.
No general animation/controller/frame hook, rating or physics change is added.

RAM `F006` is one active setting word. There are no additional SRAM bytes. The
existing complemented two-byte pass preference record is extended: per-season
logical `1EEE–1EEF` and standalone `4D20–4D21` in Light's excluded shared-profile gap.
Legacy `50–53` and r7's `80–9F` tags load100%. Bits 0–4 retain Height/Speed/Mode/Cone;
bits 5–6 encode rate 0=100%, 1=50%, 2=Off. Tags `E0–FF` reserve rate 3 and default all five
choices on load; a bad complement also defaults. Returning to legacy combinations
writes their original encoding. Existing mapped put/load/writer ABI, checksums,
adjacent season snapshot and Full goalie data are tested.

Build normally with `--version 2026-10-31-r8`. `--legacy-penalty-rate --version
2026-10-31-r7` reconstructs the exact released r7 image. The website exposes named
hooks, exact guarded bytes, the complete base diff and line-addressable sources.

## Evidence and limits

The complete `penalty_rate` regression requires seven sections:

- 564 actual native admission contexts cover both teams, every event class,
 default/invalid values, paused/stopped/native Rules-Off, duplicate and full-queue
 guards, real queue/actor/delayed flags, complete register/SR/stack ABI and RNG.
- 54 series of 2000 evolving native event decisions each:108000 controlled draws
 exactly match a separate original-native-RNG oracle, including the final seed.
 Every series stays within 45–55% acceptance. This is not 108000 matches.
- 48 paired whole-fight commit contexts cover both teams, CCR extremes, all values,
 real RNG and start/reject endpoints. Admitted fights retain native register output;
 rejected fights do not partially transfer actor registers or start one fighter.
- 48 actual native 1446A injury-caller cases compare against the original Rules-Off
 injury-only oracle, including keeper/skater, both teams, reports and stoppage.
 The audio sink and final bench continuation are stopped after the tested outputs.
- 1216 exhaustive tag/complement/five-choice/load-ABI contexts and 40 actual five-
 setting writers verify independent companions, native preferences, locks and
 adjacent-byte preservation.
- 30 real native menu/lifecycle cases cover both controller configurations, rendered
100%/50%/Off text, slot circles/reboot, legacy/corrupt data, separate standalone
 preferences, Full goalie storage, restart and actual first-match save/lock.
 A declared final-goal puck/clock fixture lets native scoring/save complete.
- Three cold games driven by actual emulator controller input exercise all settings
 and P1–P4, with actual native foul decisions and queues observed. Only observer
 counters are initialized: no game positions, fouls, queue entries, scores or RNG
 are injected. Private preserving probes may alter native HV timing. Final counts
 are bound to the release hash in PENALTYS_RESULT.json; these are not physical
 human-controller recordings or a competitive balance claim.

The untested first prototype hooked all event entries; it was retained before
moving the filter past low event handling. A later working prototype placed its
helper at2DA000, which collided with existing private reset-driver probes. The
partial shared report and exact ROM/source/proofs are retained; no failed check
was accepted. The final helper uses the declared free 2EC800 reservation and all
89 shared suites run freshly again against its exact ROM/source hashes.

Early host failures remain recorded too: menu rendering was initially checked
before the bottom row was visible, one global-record expectation was calculated
incorrectly, and the first short input run saw only two half-rate decisions, both
rejected. The final driver extends all three games and sends real Start input
through native intermissions, without picking seeds or forcing outcomes. An
initial AI comparison used the wrong pre-game state and failed its baseline;
the standard immutable trail fixture is used in final shared QA.

All 89 suites execute: 88 pass, only the retained finisher_hold geometry failure
from jdc's selected 38-win base; the strict gate remains FAILED. The current
40scene comparison records all exact frames and every divergence against fresh
r7 controls; those controls retain every earlier strict qualifier. Full numeric
results are in QA_STATUS.json and PENALTYS_RESULT.json. Native event output/ABI
at100% is separately paired; no universal unchanged-gameplay, new Hard win-rate
or improved pass-completion claim is made. Independent rebuild, exact r7 rollback,
matched ablations and original-base IPS reconstruction pass.

## Buffered-shot regression guard

The first complete 89-suite r8 run exposed a human buffered-shot gap: contact was
accepted at the correct skating hotspot, but the shot observer later saw a different
sprite hotspot 42.047592 pixels away from the same puck/body positions. The exact
pre-fix ROM is retained as an independent negative control. A late accepted-branch
observer confirms valid contact before setup; extra probe timing changes the
interleaving, so both the original and SR-aware observers are retained in tests.

The owned buffered-contact branch now saves SR and masks interrupts from contact
validation until native shot completion, restoring the original upper SR and the
actual post-shot CCR. The wait path restores both too. This does not change the
4-pixel contact radius, puck position, native shot physics or CPU shooting code.
The module remains bounded below the menu labels at 2E1200. The pre-fix 42 px release
reproduces with the original observer; the corrected original-observer run and
independent SR-aware mash/hold runs stay within 4 pixels. 192 paired frame contexts
cover all 12 actors, near/wait branches and all 8 initial IRQ-mask levels, comparing full registers,
CCR, restored IRQ mask, stack, pending request and shot outcome with the native
pre-fix contract. SR-aware actual shot entries must observe mask level 7.
The original r9 negative control and all 432 contact boundary cases remain.

Build `--legacy-contact-atomic` to reconstruct the exact private pre-fix r8 control.
`--legacy-penalty-rate` also restores the previous unprotected code for exact r7
rollback. Current QA binds this corrective change to the same released image;
the failed whole run and all prototypes/source remain retained.

## Delivery

The dated r8 replaces current Downloads, canonical and website ROM/IPS with fresh
game progress, per jdc's standing policy. Controller/config/history hashes are
preserved. A running RetroArch stays open; progress cleanup waits for normal
exit. Bodin's existing playlist item receives the same ROM when reachable.
Queued offline work is recorded as pending until independent verification.
No RetroArch launch/closure, forum message or Hard-strength search is performed.
