M/L THE MICROLEAGUE PROJECT
← Research overview

SOURCE DOCUMENT · 24 OCTOBER 2026

Batter–pitcher arithmetic

# Batter versus pitcher matchup calculator

2026-10-24. Static reconstruction for ATR SHA-256 `2f94eb9a5fa06408c46ad24cf5b8d7398de195d7354f5734dea48c03045cca75`. No ATR modification, original application execution or emulator operation.

## What is now usable

`MATCHUP_CALCULATOR.py` provides an importable initial-outcome engine and a separate command-line layer. It calls the existing verified arithmetic module rather than rewriting its BCD/division routines. It needs only the standard library and `MICROLEAGUE_ALGORITHMS.py` beside it; normal calculation does not read the ATR or other evidence files.

The exact deliverable is **the original threshold and initial-class arithmetic for supplied record/baseline bytes**. Converting historical fractional innings and choosing appropriate modern normalization baselines remain separate input-model decisions. They are explicit, never silently filled in. Percentages enumerate all 256 initial random bytes under a uniform-byte assumption; POKEY timing/distribution and complete final plate appearances are not reproduced.

## 1. Pitcher records: conventional fields and denominator

### Verified interpretation from the statistics display

This session reuses the saved arithmetic listing/indexes. To resolve the pitcher-stat labels it re-examines the specific sector-198 substitution/statistics overlay identified in `PITCHER_FATIGUE_ANALYSIS.md`, rather than redisassembling the disk. This view loads at `$8700–$98FF` from ATR offset `$006290`. Its code is distinct from both startup and final-text overlays at the same addresses.

Pitching display entry `$95FB` formats the heading `P -W-L- -ERA- -IP -H-B` from the text at `$92FE`. The field-reader `$96ED` reads record offset Y and Y+1; formatter `$96F9` formats one BCD byte with carry clear or both bytes with carry set. The display takes:

| Pitcher field | Record offset(s) | Display reference | `$0CEC` use |
|---|---|---|---|
| W / L | 20 / 21 | `$95FB/$960B`, one byte each | None |
| ERA, two decimal places | 22 / 23 | `$9616/$962B`, inserts decimal point at `$9621` | None |
| IP | **25–26** | `$9636 LDY #$19`; `$9638 JSR $96ED`; `$963B LDY #$20`; `$963D SEC`; `$963E JSR $96F9` | Common denominator |
| H allowed | **27–28** | `$9641 LDY #$1B`; `$9643 JSR $96ED`; `$9646 LDY #$24`; `$9648 SEC` | Hit numerator |
| BB allowed | **29–30** | More-statistics display `$966D–$9675` under BB heading | Walk numerator |
| SO | **31–32** | `$9678–$9680` under SO heading | Strikeout numerator |
| Extra raw field X | **33** | Semantic label not established | Fourth ratio, unused in ordinary six-weight scaling |

The heading and display do not insert a fractional-innings decimal point into the IP field. IP is a two-byte **whole-number packed-BCD** value, high byte first. The denominator is therefore **stored innings pitched**, not AB against, BF, outs, ERA, H, or IP×3. The formula uses H/IP, BB/IP and SO/IP, not modern per-PA allowed rates. ERA is displayed from different offsets and does not enter this calculation.

For example the saved FELLER record contains IP bytes `02 50` =250; H=`02 39` =239; BB=`00 95` =95; SO=`01 11` =111. Its ERA is in `03 49` at offsets 22–23. Combining those unrelated ERA/G bytes into an apparent `349.33 IP` would be an incorrect field interpretation.

### What fraction conversion is actually known

The game consumes an integer-IP record. **An original fractional-IP import/rounding algorithm has not been recovered** from this game image. The display is not an importer. The existing 2021–2023 roster automation supplies a historical preprocessing convention, not original 1984 code: `microleague input roster.py:23–26` converts baseball fractions to decimal innings then Python-rounds; the input sequence at lines 278–280 types that rounded whole IP into an external editor. The 2023 script has analogous logic. Neither script establishes what the original roster compiler did or what every original record should contain.

For a historical `174.1`, parse exactly as 523 outs=174⅓ innings. The calculator rejects floats and ambiguous decimal strings; it accepts strings `174`, `174.1`, `174.2` only (or equivalent nonnegative whole-part strings). Fractional inputs require one of:

* `ip_policy='floor'`: store 174.
* `ip_policy='nearest'`: store 174 for .1, 175 for .2.
* `ip_policy='ceil'`: store 175.
* `record_ip=...`: supply the actual integer field from a known roster.

These policies are **explicit scenarios**, not verified original rounding rules. Already integral historical IP needs no policy. The exact modern Fraction 523/3 is not substituted into `$0CEC` under an “exact” label, because that would be a different algorithm. The constructed-baseline helper can deliberately use exact aggregate innings, clearly labeled as a model choice.

## 2. Integer formula and actual inputs

### Batter: `$0D7C/$0DAD`

Let D_B=(AB+BB) mod 10000 in two-byte decimal arithmetic, and singles=(H−2B−3B−HR) mod 10000. Six weights, in order `[BB,SO,1B,2B,3B,HR]`, are the **low bytes** of Q8(count,D_B). For normal valid positive counts, this is `floor(256*count/D_B) mod 256`.

The opportunity denominator omits HBP, sacrifices and interference. Each weight truncates separately; their sum need not equal 256. The conventional-stat adapter checks H<=AB, SO<=AB and extra-base hits<=H, with native storage capacities. The raw-record API can represent the existing module's broader byte-domain behavior, including decimal underflow/wrap, without imposing those conventional-stat sanity rules.

### Pitcher: `$0CEC–$0D79`

For valid positive operands, write Q(n,d)=floor(256*n/d), retaining the 16-bit helper result. Compute:

```
RH=Q(H_allowed,stored_IP)
RB=Q(BB_allowed,stored_IP)
RK=Q(SO,stored_IP)
RX=Q(X,stored_IP)

FH=Q(RH,header_hit_baseline)
FB=Q(RB,header_walk_baseline)
FK=Q(RK,header_strikeout_baseline)
FX=Q(RX,header_extra_baseline)
```

Header offsets 28–29 form the hit baseline, **binary high-byte-first word**. Offsets 30,31,32 are binary walk, strikeout and extra baseline bytes. These are Q8 scale values: 256 means a ratio of one; the bytes are not packed BCD. `$0D2A` obtains this header from the **batting side**, so comparing the same pitcher against hitters assigned different team headers can produce different factors.

For exact helper edge behavior, `$0F49` uses signed 16-bit operands, eight fractional quotient bits, truncation toward zero, and a 16-bit result. The calculator delegates to `fixed8_divide()`. Intermediate raw words whose sign bit becomes set must not be treated as unlimited positive real ratios in the second division. Division by zero and invalid BCD are outside the verified module contract and raise errors. No zero denominator is “repaired” by adding one.

### Scale, positioning and cumulative bytes: `$0C2E–$0C83`

Select FB for BB, FK for SO, and FH for all four hit weights. Product byte 1 is retained:

```
M[i] = (floor(W[i]*selected_factor/256)) mod 256
```

FX is calculated but not selected by `$0C84 = 02 04 00 00 00 00`. Additional conventional pitcher statistics—ERA, ER, W/L, games, saves, CG, BF, handedness or fatigue counters—are **not required for this verified initial calculation**. To reproduce FX itself for an arbitrary raw record, field 33 and header baseline 32 are needed. Field 33 is zero in all 250 pitcher records in this disk; the adapter's visible default X=0 matches those observations without asserting its unknown semantic meaning. X can be supplied explicitly and affects reported FX, not these six adjusted weights.

For offense 0:

* Normal defense 1–4: combined action B3=0, no positioning bonus.
* Corners-in defense 6: B3=8, add `floor(51*M_single/256)` to singles, wrapping the byte.
* Infield-in defense 7: B3=9, add `floor(102*M_single/256)`, with byte wrap.

These are recovered coefficients, not a new balance patch. The calculator implements only these three swing-away configurations. Pitchouts, bunts, IBB and other offensive actions are not silently routed through this formula.

Construct six cumulative sums modulo 256. Append **supplied** final cutpoints, default `(243,247,253)` from the saved disk/workspace; their lifetime across every runtime path is not globally proved. Expose `tail=` in the API and `--tail` on the CLI for known live values.

### Ten exact initial byte-domain masses: `$0CDA`

Choose the first class j with R<T[j], or class 9 if no threshold qualifies. Equality moves to a later class. The calculator enumerates all R=0…255 and returns integer counts summing to 256, including wrapped/nonmonotone cases. It never sorts cutpoints, clips probabilities or normalizes the six weights to sum to one.

If cutpoints are monotone, differences give the class counts. If cumulative sums wrap, **individual adjusted weights and initial class counts can differ sharply**. Counts/256 become probabilities only under an explicit uniform random-byte model. Live hardware sampling/correlation remains unresolved.

## 3. Normalization: evidence versus modern assumptions

### What is stored on the disk

The game loads the baselines as part of a 61-byte team header (`$0CC3`), then consumes them in `$0D2F–$0D79`. The relevant paths copy/use them; no original baseline-generating algorithm has been recovered. The record/lineup writes examined for fatigue do not recalculate header offsets 28–32.

The 25 team blocks start at sector 465, eight sectors apart. Direct inspection gives three five-byte patterns:

| Scenario name | Raw bytes [28…32] | Binary baselines H,BB,SO,X | Teams |
|---|---|---|---:|
| `stored-common` | `00 F6 5E 9D 14` | 246,94,157,20 | 23 |
| `stored-yankees` | `01 18 94 4E 0A` | 280,148,78,10 | 1, team index 4 |
| `stored-tigers` | `00 D9 55 A9 13` | 217,85,169,19 | 1 |

The scenario names distinguish observed records; they do not assert a verified year/league provenance. Common baseline rates represented by Q8 are 246/256=.9609375 H/IP, 94/256=.3671875 BB/IP, 157/256=.61328125 SO/IP; X's semantic interpretation remains unknown.

This is **not** demonstrated to be an aggregate of the selected team's ten pitcher records. For the first all-time AL block, totals are IP=2956, H=2455, BB=748, SO=1704. Floor(256*totals/IP) gives `[212,64,147]`, not `[246,94,157]`. The NL all-time block gives `[195,46,177]`, yet uses the same stored common bytes. Simple unweighted means of pitcher ratios also fail to reproduce those stored baselines. Shared patterns suggest predefined reference/era normalization, but the generator and reference population remain **unknown**. Do not replace unknown provenance with a claim that the 1984 program averaged the modern team's roster.

### Modern-team compatibility

A faithful **conditional** calculation can use known raw baseline bytes. A modern deployment must decide what normalization population to represent. Options are:

1. Explicitly retain a selected historical disk baseline as a what-if scenario.
2. Supply validated baseline bytes from a known edited roster/compiler and document its provenance.
3. Construct a new modeled reference: for specified aggregate reference H,BB,SO and exact total outs O, use `[floor(768*H/O), floor(768*BB/O), floor(768*SO/O)]`. This is implemented in `baselines_from_pitching_totals()`, visibly labeled **CONSTRUCTED SCENARIO (unverified original generator)**. It is a proposal consistent with ratio units, not recovered original baseline creation.

For option 3, one individual pitcher and batter are insufficient. Reference-population totals and the population/year/team/league choice are additional inputs. Aggregate exact outs before rounding; do not add baseball decimal strings as decimal numbers. The helper also requires an explicit extra baseline. It does not fetch modern MLB data or claim to know current league rates.

The SO and walk normalization values each occupy **one byte**. If a proposed SO/IP baseline is >=1, floor(256*SO/IP)>=256 cannot fit. Likewise a floor result of zero would create division by zero. The helper rejects such results; it does not clamp to 255, wrap, change units, or substitute an undocumented factor. A fully faithful unpatched encoding cannot represent every conceivable modern reference rate. That limitation must be resolved as a research/design choice before calling a modern baseline exact.

## 4. Complete supplied example

The batter numbers are exactly the user's supplied inputs, not a claim about a verified season: Pete Crow-Armstrong AB=626, H=175, BB=83, SO=187, 2B=31, 3B=8, HR=45. Pitcher historical IP=`174.1`, H=154, BB=37, SO=174.

**Demonstration assumptions:** store IP=174 (`floor` or nearest gives this for .1); use the observed common disk baselines `[246,94,157,20]`; X=0; offense 0/normal defense; live tail bytes equal saved defaults; uniform initial-byte sampling. None establishes an appropriate modern team baseline or the original fractional-IP import convention.

### Batter weights

D=626+83=709; singles=175−31−8−45=91.

| Weight | Count | Calculation | Byte /256 | Percentage representation |
|---|---:|---|---:|---:|
| BB | 83 | floor(21248/709) | 29 | 11.328125% |
| SO | 187 | floor(47872/709) | 67 | 26.171875% |
| Single | 91 | floor(23296/709) | 32 | 12.500000% |
| Double | 31 | floor(7936/709) | 11 | 4.296875% |
| Triple | 8 | floor(2048/709) | 2 | .781250% |
| HR | 45 | floor(11520/709) | 16 | 6.250000% |

These are starting byte weights, not six probabilities normalized to 100%.

### Pitcher raw ratios and normalized factors

| Factor | First truncation | Second truncation | Result | Relative to identity 256 |
|---|---|---|---:|---:|
| FH | floor(256*154/174)=226 | floor(256*226/246) | 235 | 91.796875% |
| FB | floor(256*37/174)=54 | floor(256*54/94) | 147 | 57.421875% |
| FK | floor(256*174/174)=256 | floor(256*256/157) | 417 | 162.890625% |
| FX | floor(256*0/174)=0 | floor(256*0/20) | 0 | 0%; not selected |

A factor of 417 is not a 162.9% strikeout probability; it multiplies the batter's SO weight. Collapsing FB into floor(37*65536/(174*94)) gives 148 instead of the correct two-stage 147. Intermediate rounding is consequential.

### Adjusted weights, thresholds and all ten initial classes

| Initial class | Adjusted weight where built | Cumulative cutpoint | Byte count /256 | Conditional percentage |
|---|---:|---:|---:|---:|
| 0 Walk | floor(29*147/256)=16 | 16 | 16 | 6.250000% |
| 1 Strikeout | floor(67*417/256)=109 | 125 | 109 | 42.578125% |
| 2 Single family | floor(32*235/256)=29 | 154 | 29 | 11.328125% |
| 3 Double family | floor(11*235/256)=10 | 164 | 10 | 3.906250% |
| 4 Triple family | floor(2*235/256)=1 | 165 | 1 | .390625% |
| 5 HR family | floor(16*235/256)=14 | 179 | 14 | 5.468750% |
| 6 Mixed out | Not a constructed seventh weight | 243 supplied | 64 | 25.000000% |
| 7 HBP/interruption | — | 247 supplied | 4 | 1.562500% |
| 8 Error family | — | 253 supplied | 6 | 2.343750% |
| 9 Groundball | — | Otherwise | 3 | 1.171875% |
| Total | — | — | 256 | 100% |

Against corners-in, the single weight becomes 29+floor(29*51/256)=34; infield-in makes it 29+floor(29*102/256)=40. In these nonwrapping scenarios the single-family counts become 34/256=13.281250% and 40/256=15.625000%; mixed-out counts fall to 59 and 53. Other initial class counts stay the same. Final runner/groundball effects of positioning remain separate.

### Alternative scenarios rather than invented unique baselines

| Baseline / stored IP | Factors FH,FB,FK,FX | Initial counts, classes 0…9 |
|---|---|---|
| Common /174 | 235,147,417,0 | 16,109,29,10,1,14,64,4,6,3 |
| Common /175 | 234,147,414,0 | 16,108,29,10,1,14,65,4,6,3 |
| Yankees pattern /174 | 206,93,840,0 | 10,219,25,0,0,0,0,0,0,2 |
| Yankees pattern /175 | 205,93,833,0 | 10,218,25,0,0,0,0,0,0,3 |
| Tigers pattern /174 | 266,162,387,0 | 18,101,33,11,2,16,62,4,6,3 |
| Tigers pattern /175 | 265,162,384,0 | 18,100,33,11,2,16,63,4,6,3 |

The Yankees-pattern/174 cutpoints are **[10,229,254,6,7,19,243,247,253]**. Once earlier thresholds cover R<254, later smaller cutpoints add no mass, so doubles/triples/HR have zero initial selections despite nonzero adjusted weights. R=254/255 become class 9. This is exact byte-wrap/first-crossing behavior, not a plausible modern forecast, an error to “fix” by sorting, or a reason to present those initial counts as final statistics. The program warns on nonmonotone/wrapped cutpoints.

## 5. Initial selections versus final plate appearances

| Initial family / handler | What is known | Why not report a final PA rate here? |
|---|---|---|
| Walk `$647C` | Force advancement on ordinary walk path | A completed PA can also arise after retries/other paths; conditional initial mass is not its overall PA frequency |
| Strikeout `$633C` | Out increment and strikeout narration | Same denominator/continuation issue; not a full PA simulator |
| Single `$5E4E` | Hit credited on entry; positioning chooses subtype tables | Later subtypes/throws/runner advances must not be labeled a guaranteed final single solely from the selector |
| Double `$4D83`, triple `$63B5`, HR `$5707` | Respective families and additional trajectories/runner logic | Final scoring, errors and runner outs need state/closure |
| Mixed out `$5B51` | Line/pop/cleanup/groundball families | Can include foul continuation and double plays |
| HBP/interruption `$506C` | Several subpaths; can leave batter active | Not simply HBP probability |
| Error `$50F8` | Error and one-/two-base branches | Need fielding and runner state; not all-cause final error probability |
| Groundball `$5CBA` | Forces, batter outs, double plays and plays at home | Outs/runners/ratings/positioning and later RNG matter |

The API returns `final_plate_appearance_probabilities=None` and an explanation. It accepts no runner-mask/out arguments and does not pretend to account for them. They do not enter this recovered swing-away initial builder but are essential to many later handlers. `SURPRISE_BUNT_GAME_THEORY.md`, `BUNT_ANALYSIS.md` and `PITCHER_FATIGUE_ANALYSIS.md` document the recovered later fragments separately; this calculator does not splice incomplete pieces into a fake complete PA model.

No batting-average, OBP, run expectancy, win expectancy or expected final hit count is inferred by adding initial families or renormalizing away interruptions. In particular, changes in win expectancy are never used as outcome probabilities.

## 6. Running and importing

From the project root:

```powershell
python -B reverse_engineering/MATCHUP_CALCULATOR.py --example
python -B reverse_engineering/MATCHUP_CALCULATOR.py --example --json
```

The example prints the complete common/174 scenario and six explicit alternatives. For a custom calculation:

```powershell
python -B reverse_engineering/MATCHUP_CALCULATOR.py --batter-ab 626 --batter-h 175 --batter-bb 83 --batter-so 187 --batter-2b 31 --batter-3b 8 --batter-hr 45 --pitcher-ip 174.1 --pitcher-h 154 --pitcher-bb 37 --pitcher-so 174 --ip-policy floor --baseline stored-common --defense normal
```

Or specify `--baseline-values 246 94 157 20 --baseline-label "Explicit historical what-if"` instead of `--baseline`. No baseline is chosen silently for custom input. Use `--record-ip 174` instead of `--ip-policy` for known stored data. `--tail 243 247 253` and `--extra-numerator 0` are exposed; both defaults are explicitly described in output. `--json` prints structured output to stdout; no source/evidence files are written.

Import with no CLI side effects:

```python
from reverse_engineering.MATCHUP_CALCULATOR import (
    BatterStats, PitcherStats, STORED_BASELINES, calculate_matchup,
    baselines_from_pitching_totals,
)

batter = BatterStats(626,175,83,187,31,8,45)
pitcher = PitcherStats('174.1',154,37,174)
result = calculate_matchup(batter,pitcher,STORED_BASELINES['stored-common'],
                           ip_policy='floor',defense='normal')
print(result.initial_counts)
print(result.to_dict())
```

Core entry points:

* `calculate_records(batter_record,pitcher_record,batting_header,...)`: original-byte evidence use; also delegates verified pitcher-batting conversion when those raw flags are present. Requires actual complete header bytes for that conversion.
* `calculate_matchup(BatterStats,PitcherStats,Baselines,...)`: conventional-stat adapter for an ordinary batter with explicit input assumptions.
* `parse_ip`, `innings_record_value`: separate historical notation from integer record encoding.
* `baselines_from_pitching_totals`: explicitly constructed, capacity-checked reference scenario, not native baseline creation.
* `format_result` / `main`: presentation and argument parsing, with no probability-engine dependence on UI state.

Results are immutable dataclasses with tuples and structured JSON export. Many-vs-one and one-vs-many comparisons can loop over these pure APIs. A future simulator should supply transition kernels after the initial selection, keep RNG external, track player/game state, and distinguish run/win values from event probabilities. Lineup optimization or multiplayer strategy cannot be certified from initial class masses alone. No simulation, lineup optimizer or network service is claimed in this phase.

## 7. Validation and limits

The new tests include independent hand-calculated example expectations (including a collapsed-rounding counterexample), original fixture raw/adapter equivalence, comparison of **250 disk pitcher records × 3 positions** against the saved threshold builder, IP notation/policy errors, BCD capacities and overflow, normalization capacity, extra-factor nonuse, configurable tail cutpoints, wrapped threshold enumeration, CLI JSON and missing-input behavior.

The first full run of `python -B -m unittest discover -s reverse_engineering -p 'test_*.py' -v` passed **53 tests**, including 12 new matchup tests and the prior 41, before the requested research-index update. The existing arithmetic tests include exhaustive 8×8 multiplication, bounded-byte scaling, BCD operations, signed fixed-point division comparisons and static byte fixtures. They test returned mathematics, not NMOS cycle timing or every CPU flag. Original applications and game binaries are not executed by these tests.

The user explicitly requested updating `RESEARCH_INDEX.md`. An older bunt preservation test freezes the old hash of that index in `BUNT_RESEARCH_MANIFEST.json`; the authorized update necessarily makes **that historical snapshot assertion fail**. Its manifest/test are left unchanged as historical evidence. Final validation reports this exception separately, rather than rewriting evidence or claiming an unchanged index. All other previously existing files are preserved byte-for-byte.

Remaining research:

1. Locate the original roster compiler/editor or its source/evidence to establish baseline generation and fractional-IP import convention. The 1984 game image alone has not established either.
2. Validate roster-editor field labels against a controlled read-only debugger capture, especially raw field 33 and baseline X. Their unknown meaning does not affect current ordinary initial masses when X is unused.
3. Check live tail bytes and source header/record selection for desired game mode before treating disk-default scenarios as live game state.
4. Trace full single/error/groundball/extra-base and continuation closures before computing final PA, runs or lineup value.
5. Validate POKEY sampling/configuration if live-machine probabilities, rather than uniform-byte mathematical distributions, are required.

No guessed historical baselines, fractional-IP rules, final outcomes, relief penalties or modern statistical adjustments were inserted into the exact engine.

Final validation: 12/12 new calculator tests pass, including 750 original-pitcher/position comparisons and the overlay IP-display byte anchor. Namespace import and the corners-in example also pass. Full discovery after the authorized index update gives 52 passes and exactly one historical index-hash failure described above. SHA-256 comparison of 45 pre-existing files confirms RESEARCH_INDEX.md is the only changed existing file; the ATR, applications and all other research files match their pre-task fingerprints. The only added files are MATCHUP_CALCULATOR.py, MATCHUP_ANALYSIS.md and test_matchup_calculator.py.