Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8.9 KiB
5. Rating Boost Mechanics
< Back to Index | Next: Database Schema >
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_idfield 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_idfor 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:
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.