claude-home/paper-dynasty/card-evolution-prd/05-rating-boosts.md
Cal Corum aafe527d51
All checks were successful
Reindex Knowledge Base / reindex (push) Successful in 5s
docs: add Major Domo and Paper Dynasty release notes and card evolution PRD
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:29:18 -05:00

8.9 KiB
Raw Blame History

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_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:

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.