# 5. Rating Boost Mechanics

[< Back to Index](README.md) | [Next: Database Schema >](06-database.md)

---

## 5.1 Rating Model Overview

The card rating system is built on the `battingcardratings` and `pitchingcardratings` models.
Each model defines outcome columns whose values represent chances out of a **108-chance total**
(derived from the D20 probability system: 2d6 × 3 columns × 6 rows = 108 total chances).

**Batter ratings** have **22 outcome columns** summing to 108:

| Category | Columns |
|---|---|
| Hits | `homerun`, `bp_homerun`, `triple`, `double_three`, `double_two`, `double_pull`, `single_two`, `single_one`, `single_center`, `bp_single` |
| On-base | `hbp`, `walk` |
| Outs | `strikeout`, `lineout`, `popout`, `flyout_a`, `flyout_bq`, `flyout_lf_b`, `flyout_rf_b`, `groundout_a`, `groundout_b`, `groundout_c` |

**Pitcher ratings** have **18 outcome columns + 9 x-check fields** summing to 108:

| Category | Columns |
|---|---|
| Hits allowed | `homerun`, `bp_homerun`, `triple`, `double_three`, `double_two`, `double_cf`, `single_two`, `single_one`, `single_center`, `bp_single` |
| On-base | `hbp`, `walk` |
| Outs | `strikeout`, `flyout_lf_b`, `flyout_cf_b`, `flyout_rf_b`, `groundout_a`, `groundout_b` |
| X-checks | `xcheck_p` (1), `xcheck_c` (3), `xcheck_1b` (2), `xcheck_2b` (6), `xcheck_3b` (3), `xcheck_ss` (7), `xcheck_lf` (2), `xcheck_cf` (3), `xcheck_rf` (2) — always sum to 29 |

**Key differences:** Batters have `double_pull`, pitchers have `double_cf`. Batters have
`lineout`, `popout`, `flyout_a`, `flyout_bq`, `groundout_c` — pitchers do not. Pitchers have
`flyout_cf_b` and x-check fields — batters do not.

Evolution boosts apply **flat deltas to individual result columns** within these models. The
108-sum constraint must be maintained: any increase to a positive outcome column requires an
equal decrease to a negative outcome column.

### Rating Cap Enforcement

All boosts are subject to the existing hard caps on individual stat columns. If applying a delta
would push a value past its cap, the delta is **truncated** to the cap value.

**Key caps (from existing card creation system):**

| Stat | Cap | Direction | Example |
|---|---|---|---|
| Hold rating (pitcher) | -5 | Lower is better | A pitcher at -4 hold can only receive -1 more |
| Result columns | 0 floor | Cannot go negative | A 0.1 strikeout column can only lose 0.1 |

**Truncated points are lost, not redistributed.** If a boost would push a stat past its cap, the
delta is truncated and the excess is simply discarded. This is an intentional soft penalty for
cards that are already near their ceiling — they're being penalized because they're already that
good. Lower-rated cards have more headroom and benefit more from the same flat delta.

## 5.2 Boost Budgets Per Tier

Rating boosts are defined as **flat deltas to specific result columns** within the 108-sum model.
The budget per tier is the total number of chances that can be shifted from negative outcomes
(outs) to positive outcomes (hits, on-base).

| Tier | Budget (chances/108) | Approx Impact |
|------|---------------------|---------------|
| T1 | 1.0 | A full chance moved from outs to positive outcomes |
| T2 | 1.0 | Same — consistent per-tier reward |
| T3 | 1.0 | Same — consistent per-tier reward |
| T4 | 1.0 | Same — plus rarity upgrade and name change |
| **Total** | **4.0** | **~3.7% of total chances shifted from outs to positive outcomes** |

Every tier provides the same 1.0-chance budget. This keeps the system simple and predictable —
each tier completion feels equally rewarding in raw stats. T4 is distinguished not by a larger
delta but by the rarity upgrade and evolved card name, which are the real capstone rewards.

**Flat delta design rationale:** All cards receive the same absolute budget regardless of rarity.
A 4.0-chance shift on a Replacement card (where `homerun` might be 0.3) is a huge relative
improvement. The same shift on a Hall of Fame card (where `homerun` might be 5.0) is marginal.
This intentionally incentivizes using lower-rated cards and prevents elite cards from becoming
god-tier. Cards already near column caps receive even less due to truncation.

**Example — T1 power batter boost (1.0 budget):**
```
homerun:    +0.50  (from 2.0 → 2.50)
double_pull: +0.50  (from 3.5 → 4.00)
strikeout:  -0.70  (from 15.0 → 14.30)
groundout_a: -0.30  (from 8.0 → 7.70)
                     Net: +1.0 / -1.0 = 0, sum stays at 108
```

## 5.3 Default Boost Distribution Rules

By default, the boost is distributed to result columns based on the player's characteristic
style, auto-detected from their existing ratings:

**Power hitter profile** (high homerun relative to singles): budget added to `homerun`,
`double_three`, `double_two`, `double_pull`; subtracted from `strikeout`, `groundout_a`.

**Contact hitter profile** (high singles relative to HR): budget added to `single_one`,
`single_center`, `single_two`; subtracted from `strikeout`, `lineout`.

**Patient hitter profile** (high walk): budget added to `walk`, `hbp`; subtracted from
`strikeout`, `popout`.

**Starting pitcher** (reduce hits allowed): budget subtracted from `homerun`, `walk` (positive
for the pitcher); added to `strikeout`, `groundout_a`.

**Relief pitcher**: Same as SP but with larger strikeout bias and tighter HR reduction.

The `apply_evolution_boosts(card_ratings, boost_tier, player_profile)` function (card-creation
repo) handles this distribution deterministically. It ensures the 108-sum invariant is maintained
after each boost application. Columns are modified in the `battingcardratings` /
`pitchingcardratings` rows directly — the same model objects used by the game engine.

## 5.4 Rarity Upgrade at T4

When a card completes T4, the card's rarity is upgraded by one tier (if below HoF):

- The `player.rarity_id` field is incremented by one step (e.g., Sta -> All)
- The card's base rating recalculation is skipped; only the T4 boost deltas are applied on top of the
  accumulated evolved ratings
- The card cost field is NOT automatically recalculated (rarity upgrade is a gameplay reward, not
  a market event; admin can manually adjust if needed)
- The rarity change is recorded in `evolution_card_state.final_rarity_id` for audit purposes
- **HoF cards cannot upgrade further** — they receive the T4 boost deltas but no rarity change

**Live series interaction:** If a card's rarity changes due to a live series update (e.g.,
Reserve → All-Star after a hot streak), the evolution rarity upgrade stacks on top of the
*current* rarity at the time T4 completes. The evolution system does not track or care about
historical rarity — it simply increments whatever the current rarity is by one step.

## 5.5 Variant System Usage (Hash-Based)

The existing `battingcard.variant` and `pitchingcard.variant` fields (integer, UNIQUE with player)
are currently always 0. The evolution system uses variant to store evolved versions, with the
variant number derived from a **deterministic hash** of all inputs that affect the card:

```python
import hashlib

def compute_variant_hash(player_id: int, evolution_tier: int,
                         cosmetics: list[str] | None) -> int:
    """Compute a stable variant number from evolution + cosmetic state."""
    inputs = {
        "player_id": player_id,
        "evolution_tier": evolution_tier,
        "cosmetics": sorted(cosmetics or []),
    }
    raw = hashlib.sha256(str(inputs).encode()).hexdigest()
    return int(raw[:8], 16)  # 32-bit unsigned integer from first 8 hex chars
```

- `variant = 0`: Base card (standard, shared across all teams)
- `variant = <hash>`: Evolution/cosmetic-specific card with boosted ratings and custom image

**Key property: two teams with the same player_id, same evolution tier, and same cosmetics
produce the same variant hash.** This means they share the same ratings rows and the same
rendered S3 image — no duplication. If either team changes any input (buys a cosmetic), the
hash changes, creating a new variant.

Each tier completion or cosmetic change computes the new variant hash, checks if a `battingcard`
row with that variant exists (reuse if so), and creates one if not. The `card` table instance
points to its current variant via `card.variant`.

Evolved rating rows coexist with the base card in the same `battingcardratings`/`pitchingcardratings`
tables, keyed by `(battingcard_id, vs_hand)` where `battingcard_id` points to the variant row.
No new columns needed on the ratings table itself.

**Image storage:** Each variant's rendered card image URL is stored on `battingcard.image_url`
and `pitchingcard.image_url` (new nullable columns). The bot's display logic checks `card.variant`:
if set, look up the variant's `battingcard.image_url`; if null, fall back to `player.image`.
Images are rendered once via the existing Playwright pipeline (with cosmetic CSS applied) and
uploaded to S3 at a predictable path: `cards/cardset-{id}/player-{player_id}/v{variant}/battingcard.png`.
The 5-6 second render cost is paid once per variant creation, not on every display.
