docs: add Major Domo and Paper Dynasty release notes and card evolution PRD
All checks were successful
Reindex Knowledge Base / reindex (push) Successful in 5s
All checks were successful
Reindex Knowledge Base / reindex (push) Successful in 5s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2258dfade
commit
aafe527d51
55
major-domo/database-release-2026.3.17.md
Normal file
55
major-domo/database-release-2026.3.17.md
Normal file
@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Major Domo Database Release — 2026.3.17
|
||||
type: reference
|
||||
domain: server-configs
|
||||
tags: [major-domo, deployment, release-notes, database]
|
||||
---
|
||||
|
||||
# Major Domo Database Release — 2026.3.17
|
||||
|
||||
**Date:** 2026-03-17
|
||||
**Branch:** next-release → main (PR #64, 11 commits)
|
||||
**Repo:** cal/major-domo-database
|
||||
**Server:** akamai (`sba_db_api` container, port 801)
|
||||
|
||||
## Release Summary
|
||||
|
||||
11 commits from `next-release` rebased onto `main`. Mix of query performance improvements, bug fixes, and code quality cleanup. No schema migrations — safe drop-in deploy.
|
||||
|
||||
## Performance
|
||||
|
||||
- **Push limit/offset to DB in `PlayerService.get_players`** (#37): Previously fetched all rows then sliced in Python. Now passes `LIMIT`/`OFFSET` directly to the Peewee query, eliminating full-table reads on paginated requests.
|
||||
- **Eliminate N+1 queries in batch POST endpoints** (#25): Replaced per-row `get_or_none()` loops in `battingstats`, `results`, `schedules`, and `transactions` batch insert endpoints with a single bulk set-lookup before the write loop.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Fix `lob_2outs` and `rbipercent` calculation in `SeasonPitchingStats`** (#28): Both fields were returning incorrect values due to wrong formula inputs. Now computed correctly from the underlying stat columns.
|
||||
- **Invalidate cache after `PlayerService` write operations** (#32): Cache was being populated on reads but never invalidated on writes, causing stale player data to persist until the next restart.
|
||||
- **Validate `sort_by` parameter with `Literal` type in `views.py`** (#36): Unvalidated `sort_by` could cause silent no-ops or unexpected ordering. Now enforced with a `Literal` type annotation, raising a 422 on invalid values.
|
||||
|
||||
## Code Quality
|
||||
|
||||
- **Replace bare `except:` with `except Exception:`** (#29): Silencing `KeyboardInterrupt`, `SystemExit`, and `GeneratorExit` is an antipattern. All 6 instances updated.
|
||||
- **Remove unused imports in `standings.py` and `pitchingstats.py`** (#30): Dead imports removed (`pydantic` in `standings.py`).
|
||||
- **Replace `print(req.scope)` with `logger.debug` in `/api/docs`** (#21): Debug print statement that logged full request scope on every docs page load replaced with a conditional logger call.
|
||||
- **Remove empty `WEEK_NUMS` dict from `db_engine.py`** (#34): Unused stub dict removed.
|
||||
- **Remove unimplementable skipped caching tests** (#33): Tests marked `@pytest.mark.skip` for a caching interface that was never built. Removed rather than leaving permanently skipped.
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
Branch was rebased (not squash-merged) onto main. Six files had textual conflicts from two main-branch fixes that landed after `next-release` diverged:
|
||||
|
||||
**Trailing slash regression (6 router files)**
|
||||
- PR #61 on main fixed a production bug: `aiohttp` converts POST→GET on 307 redirects, silently dropping request bodies. The fix changed all `@router.post("")` → `@router.post("/")`.
|
||||
- next-release's batch N+1 and bare-except PRs all touched the same files with the old `""` signature.
|
||||
- Resolution: kept `@router.post("/")` from main, applied next-release's functional changes on top.
|
||||
|
||||
**Case-insensitive `team_abbrev` filter (`transactions.py`)**
|
||||
- PR #60 on main fixed affiliate roster transactions being silently dropped due to case mismatch (input uppercased, DB column mixed-case). Fix used `fn.UPPER(Team.abbrev)`.
|
||||
- next-release's version of `transactions.py` predated this fix.
|
||||
- Resolution: preserved `fn.UPPER(Team.abbrev)` from main alongside next-release's N+1 fix.
|
||||
|
||||
**`requirements.txt`**
|
||||
- Main has fully pinned deps including `starlette==0.52.1` (from 2026-03-09 outage) and a new `requirements-dev.txt`.
|
||||
- next-release had loose pins and no `requirements-dev.txt`.
|
||||
- Resolution: took main's fully-pinned version entirely.
|
||||
105
major-domo/release-2026.3.17.md
Normal file
105
major-domo/release-2026.3.17.md
Normal file
@ -0,0 +1,105 @@
|
||||
---
|
||||
title: Major Domo v2 Release — 2026.3.17
|
||||
type: reference
|
||||
domain: server-configs
|
||||
tags: [discord, major-domo, deployment, release-notes]
|
||||
---
|
||||
|
||||
# Major Domo v2 Release — 2026.3.17
|
||||
|
||||
**Date:** 2026-03-17
|
||||
**Branch:** next-release → main (PR #81, 33 commits)
|
||||
**Image:** `manticorum67/major-domo-discordapp:latest`
|
||||
**Server:** akamai (`/root/container-data/major-domo`)
|
||||
**Deploy script:** `/mnt/NV2/Development/major-domo/discord-app-v2/.scripts/deploy.sh`
|
||||
|
||||
## Release Summary
|
||||
|
||||
Merged 33 commits from `next-release` into `main`. This was the first release cut since the next-release branch workflow was adopted. A merge conflict in `views/trade_embed.py` required manual resolution.
|
||||
|
||||
## Merge Conflict Resolution
|
||||
|
||||
**File:** `views/trade_embed.py`
|
||||
**Cause:** Two divergent changes since the branch point:
|
||||
- **main hotfix** (`e98a658`): Stripped all emoji from trade embed UI, added inline validation error details to Quick Status
|
||||
- **next-release refactor** (`858663c`): Moved 42 lazy imports to top-level, which also touched this file and had emoji additions
|
||||
|
||||
**Resolution:** Kept main's hotfix (no emoji, detailed validation errors) as the authoritative version, then applied only the import refactor from next-release (hoisted lazy imports to top-level). Merged main into next-release first to resolve, then PR #81 merged cleanly.
|
||||
|
||||
## Key Changes in This Release
|
||||
|
||||
### Trades & Transactions
|
||||
- Trade validation now checks projected rosters (next week + pending transactions)
|
||||
- Player roster auto-detection in `/trade add-player` and `/trade supplementary` (was hardcoded to Major League, caused cascading validation errors)
|
||||
- Team-prefixed validation errors: `[BSG] Too many ML players`
|
||||
- Stale transaction cache fixed — TransactionBuilder re-fetches on each validation
|
||||
- Stale roster cache fixed — refreshed before validation, invalidated after submission
|
||||
|
||||
### Scorecard Submission
|
||||
- All spreadsheet data read before any DB writes (prevents partial state on formula errors)
|
||||
- Detailed error messages shown to users instead of generic failure
|
||||
|
||||
### Commands & Permissions
|
||||
- `/injury set-new` and `/injury clear` verify team ownership (admins bypass)
|
||||
- `/admin-maintenance` maintenance mode flag implemented (but see Post-Deploy Issues below)
|
||||
- Roster labels updated: Active Roster / Minor League / Injured List sections
|
||||
|
||||
### Bug Fixes
|
||||
- Key plays score text: tied scores now show correctly
|
||||
- `is_admin()` helper prevents AttributeError in DM contexts
|
||||
|
||||
### Performance & Code Quality
|
||||
- Persistent aiohttp session for Giphy (eliminates per-call TCP/DNS overhead)
|
||||
- 42 lazy imports moved to top-level across codebase
|
||||
- Deduplicated command hash logic
|
||||
|
||||
### CI/CD
|
||||
- `next-release` branch now triggers Docker builds via shared `docker-tags` composite action
|
||||
|
||||
## Post-Deploy Issues
|
||||
|
||||
### 1. Maintenance mode interaction_check was broken (FIXED)
|
||||
|
||||
**Discovered immediately after deploy.** RuntimeWarning in logs:
|
||||
```
|
||||
RuntimeWarning: coroutine 'CommandTree.interaction_check' was never awaited
|
||||
```
|
||||
|
||||
**Root cause:** `@self.tree.interaction_check` is not a decorator — it's an async method meant to be overridden via subclassing. The check was never registered; maintenance mode was a complete no-op.
|
||||
|
||||
**Fix:** PR #83 — Created `MaintenanceAwareTree(CommandTree)` subclass, passed via `tree_cls=` to Bot constructor. Merged and redeployed same session.
|
||||
|
||||
**Issue:** #82 (closed)
|
||||
|
||||
### 2. Stale scorecards causing repeated channel updates (MITIGATED)
|
||||
|
||||
**Discovered after deploy.** The `#live-sba-scores` channel kept being cleared/hidden every 3 minutes despite no active games.
|
||||
|
||||
**Root cause:** `scorecards.json` on akamai contained 16 stale entries from March 9-17. The `data/` volume is mounted `:ro` in docker-compose (to protect Google Sheets credentials), which silently prevents `ScorecardTracker.save_data()` from persisting scorecard removals. Completed games accumulate indefinitely.
|
||||
|
||||
**Immediate fix:** Cleared the file on akamai:
|
||||
```bash
|
||||
ssh akamai
|
||||
echo '{"scorecards": {}}' > /root/container-data/major-domo/storage/scorecards.json
|
||||
```
|
||||
|
||||
**Permanent fix needed:** Split volume mount — credentials read-only, state files writable. Also add logging to the bare `except` in `save_data()`.
|
||||
|
||||
**Issue:** #85 (open)
|
||||
|
||||
## Rollback
|
||||
|
||||
Previous image digest (pre-release):
|
||||
```
|
||||
manticorum67/major-domo-discordapp@sha256:8e01dd6ee78442c1bbf06f3c291c7e8694e472683defb403b70f6d1e1a5facf1
|
||||
```
|
||||
|
||||
Rollback command:
|
||||
```bash
|
||||
ssh akamai "cd /root/container-data/major-domo && \
|
||||
docker pull manticorum67/major-domo-discordapp@sha256:8e01dd6ee78442c1bbf06f3c291c7e8694e472683defb403b70f6d1e1a5facf1 && \
|
||||
docker tag manticorum67/major-domo-discordapp@sha256:8e01dd6ee78442c1bbf06f3c291c7e8694e472683defb403b70f6d1e1a5facf1 manticorum67/major-domo-discordapp:latest && \
|
||||
docker compose up -d discord-app"
|
||||
```
|
||||
|
||||
**Note:** Rolling back would reintroduce the broken `@self.tree.interaction_check` (no-op, not harmful) but would also lose all the trade validation fixes which are actively used by league GMs.
|
||||
26
paper-dynasty/card-evolution-prd/01-objectives.md
Normal file
26
paper-dynasty/card-evolution-prd/01-objectives.md
Normal file
@ -0,0 +1,26 @@
|
||||
# 1. Business Objectives and Success Metrics
|
||||
|
||||
[< Back to Index](README.md) | [Next: Architecture >](02-architecture.md)
|
||||
|
||||
---
|
||||
|
||||
## Business Objectives
|
||||
|
||||
| Objective | Description |
|
||||
|-----------|-------------|
|
||||
| Increase session depth | Give players a persistent reason to play games beyond pack acquisition |
|
||||
| Create card attachment | Players build emotional investment in specific cards through accumulated play |
|
||||
| Reward active play | Evolution milestones reward game activity, not currency spending |
|
||||
| Differentiate the meta | Evolved cards create unique roster compositions that are hard to replicate |
|
||||
| Cosmetic revenue | Premium cosmetics create a voluntary, desirable currency sink |
|
||||
| Introduce healthy grind | Controlled progression loops keep engaged players busy between major events |
|
||||
|
||||
## Success Metrics
|
||||
|
||||
| Metric | Target | Measurement |
|
||||
|--------|--------|-------------|
|
||||
| Games played per active user per week | +25% within 60 days of launch | stratgame records |
|
||||
| Average evolution tier completions per active user per month | >= 1 full track | evolution_card_state table |
|
||||
| Player retention at 30 days | +15% vs pre-evolution cohort | team active flag / login frequency |
|
||||
| Currency spent on cosmetics | >= 15% of total currency activity | wallet transaction logs |
|
||||
| Cards with at least one milestone completed | >= 75% of all owned cards with 10+ games played | evolution_card_state |
|
||||
319
paper-dynasty/card-evolution-prd/02-architecture.md
Normal file
319
paper-dynasty/card-evolution-prd/02-architecture.md
Normal file
@ -0,0 +1,319 @@
|
||||
# 2. System Architecture Overview
|
||||
|
||||
[< Back to Index](README.md) | [Next: Tracks >](03-tracks.md)
|
||||
|
||||
---
|
||||
|
||||
## Stats Tracking Approach: Lazy Evaluation Against Existing Data
|
||||
|
||||
Evolution progress is computed on-demand by querying the existing `stratplay`, `decision`, and
|
||||
`stratgame` tables at the `player_id` level. There are no new event tables or pipeline changes
|
||||
required.
|
||||
|
||||
The `stratplay` table already records `player_id` for every plate appearance, with full counting
|
||||
stats (hits, HR, BB, SO, etc.). The `decision` table already records pitcher wins, saves, holds.
|
||||
The `stratgame` table records game outcomes by team. All milestone types can be satisfied by
|
||||
querying this existing data.
|
||||
|
||||
**Progress is team-scoped.** Milestones count stats accumulated by `player_id` in games played
|
||||
by the *owning team*, starting from when the team acquired the card (`progress_since`). This means
|
||||
evolution rewards your team's history with a player. If a card is traded (when trading ships),
|
||||
progress resets for the new team.
|
||||
|
||||
A card that has been on a roster for months before this system launches gets credit for all past
|
||||
games played by that team. This is intentional — it gives the feature immediate retroactive depth
|
||||
rather than a cold start.
|
||||
|
||||
**What this means in practice:**
|
||||
- "Hit 10 HRs" → `SELECT SUM(homerun) FROM stratplay sp JOIN stratgame sg ON sp.game_id = sg.id WHERE sp.batter_id = {player_id} AND (sg.away_team = {team_id} OR sg.home_team = {team_id}) AND sg.created_at >= {progress_since}`
|
||||
- "Win 5 games" → `SELECT COUNT(*) FROM stratgame WHERE (away_team = {team_id} AND away_score > home_score OR home_team = {team_id} AND home_score > away_score) AND created_at >= {progress_since}`
|
||||
- "Record 8 saves" → `SELECT COUNT(*) FROM decision d JOIN stratgame sg ON d.game_id = sg.id WHERE d.pitcher = {player_id} AND d.is_save = true AND (sg.away_team = {team_id} OR sg.home_team = {team_id})`
|
||||
|
||||
### Materialized Aggregate Stats Table
|
||||
|
||||
Evolution milestone evaluation requires counting stats like hits, HRs, saves, etc. for a
|
||||
`(player_id, team_id)` pair. Currently, all stat aggregation is done on-demand via
|
||||
`GROUP BY + SUM()` against raw `stratplay` rows (one row per plate appearance). This gets
|
||||
progressively slower as the table grows.
|
||||
|
||||
A **`player_season_stats`** table should be created as an early deliverable to provide
|
||||
pre-computed season totals. This table is updated incrementally in the post-game callback
|
||||
(add the game's deltas to running totals) rather than recomputed from scratch. Milestone
|
||||
evaluation then becomes a single row lookup instead of a table scan.
|
||||
|
||||
**This pattern is already implemented in the Major Domo project** (SBA fantasy baseball) for
|
||||
the same purpose — quick lookup of large sample size stats. Reference that implementation for
|
||||
schema design and the incremental update logic.
|
||||
|
||||
The aggregate table benefits the entire system beyond evolution: leaderboards, scouting,
|
||||
player comparison, and any future feature that needs season totals.
|
||||
|
||||
See [06-database.md](06-database.md) for the `player_season_stats` schema.
|
||||
|
||||
**The `evolution_card_state` table** is a cached materialization of the current tier per
|
||||
`(player_id, team_id)` pair. It is the source of display truth for Discord embeds and the card
|
||||
API. The source of truth for progress is always the raw game data tables. The cached state is
|
||||
refreshed after each game completes (post-game callback) and can be invalidated on-demand via
|
||||
the evaluate endpoint.
|
||||
|
||||
**Duplicate cards of the same player_id** on the same team are identical for evolution purposes.
|
||||
They share the same `evolution_card_state`, the same tier, the same boosts, the same cosmetics,
|
||||
and the same variant hash. The evolution system sees `(player_id, team_id)` as its fundamental
|
||||
unit — individual `card_id` instances are not differentiated.
|
||||
|
||||
**Different-year versions of the same real-world player** (e.g., 2019 Mike Trout vs 2024 Mike
|
||||
Trout) have different `player_id` values and evolve independently. They are completely separate
|
||||
players in the database with separate stats, ratings, and evolution tracks.
|
||||
|
||||
## Component Map
|
||||
|
||||
```
|
||||
Discord Bot (game engine)
|
||||
|
|
||||
|-- [existing] stratplay pipeline (player_id level, UNCHANGED)
|
||||
|
|
||||
|-- [NEW] post-game evolution evaluator
|
||||
|
|
||||
+-- queries stratplay/decision/stratgame for milestone progress
|
||||
+-- updates evolution_card_state table (cached tier per card)
|
||||
+-- sends Discord notifications on milestone/tier completion
|
||||
+-- applies auto-detected boost profile and cosmetics
|
||||
|
||||
Paper Dynasty API (database service)
|
||||
|
|
||||
+-- [NEW] /v2/evolution/* endpoints
|
||||
+-- [NEW] /v2/evolution/evaluate/{card_id} -- recalculate tier from source tables
|
||||
+-- [MODIFIED] /v2/players/{id}/battingcard -- aware of evolution variant
|
||||
+-- [MODIFIED] /v2/players/{id}/pitchingcard -- aware of evolution variant
|
||||
|
||||
Card Creation System (this repo)
|
||||
|
|
||||
+-- [MODIFIED] variant=0 -> variant=1+ for evolved card ratings
|
||||
+-- [NEW] apply_evolution_boosts() utility
|
||||
+-- [NEW] APNG render pipeline for animated cosmetics
|
||||
+-- [MODIFIED] concurrent upload pipeline with semaphore-bounded asyncio.gather
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Card Render Pipeline Optimization
|
||||
|
||||
### Current State (Pre-Optimization)
|
||||
|
||||
The existing card image render pipeline in `database/app/routers_v2/players.py` launches a
|
||||
**new Chromium process for every single card render**:
|
||||
|
||||
```python
|
||||
# Current: ~3 seconds per card
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch() # ~1.0-1.5s: spawn Chromium
|
||||
page = await browser.new_page()
|
||||
await page.set_content(html_string) # ~0.3-0.5s: parse HTML + fetch Google Fonts from CDN
|
||||
await page.screenshot(path=..., clip=...) # ~0.5-0.8s: layout + paint + PNG encode
|
||||
await browser.close() # teardown
|
||||
```
|
||||
|
||||
The upload pipeline in `pd_cards/core/upload.py` is fully sequential — one card at a time in a
|
||||
`for` loop, no concurrency. A typical 800-card run takes **~40 minutes**.
|
||||
|
||||
**Breakdown of the ~3 seconds per card:**
|
||||
|
||||
| Step | Time | Notes |
|
||||
|---|---|---|
|
||||
| Chromium launch | ~1.0-1.5s | New OS process per card |
|
||||
| Google Fonts CDN fetch | ~0.3-0.5s | Network round-trip during `set_content()` |
|
||||
| HTML parse + layout + paint | ~0.5-0.8s | Skia software rasterizer, 1200x600 |
|
||||
| PNG screenshot encode | ~0.3-0.5s | PNG compression of framebuffer |
|
||||
| DB rating write-back + disk write | ~0.2-0.3s | Two UPDATE statements + file I/O |
|
||||
|
||||
### Optimization 1: Persistent Browser (Priority: Immediate)
|
||||
|
||||
Launch Chromium **once** at API startup and reuse it across all render requests. This eliminates
|
||||
the ~1.0-1.5 second launch/teardown overhead per card.
|
||||
|
||||
**Implementation in `database/app/routers_v2/players.py`:**
|
||||
|
||||
```python
|
||||
# Module-level: persistent browser instance
|
||||
_browser: Browser | None = None
|
||||
_playwright: Playwright | None = None
|
||||
|
||||
async def get_browser() -> Browser:
|
||||
"""Get or create persistent Chromium browser instance."""
|
||||
global _browser, _playwright
|
||||
if _browser is None or not _browser.is_connected():
|
||||
_playwright = await async_playwright().start()
|
||||
_browser = await _playwright.chromium.launch(
|
||||
args=["--no-sandbox", "--disable-dev-shm-usage"]
|
||||
)
|
||||
return _browser
|
||||
|
||||
async def shutdown_browser():
|
||||
"""Clean shutdown on API exit. Register with FastAPI lifespan."""
|
||||
global _browser, _playwright
|
||||
if _browser:
|
||||
await _browser.close()
|
||||
if _playwright:
|
||||
await _playwright.stop()
|
||||
```
|
||||
|
||||
**Per-render request (replaces current `async with async_playwright()` block):**
|
||||
|
||||
```python
|
||||
browser = await get_browser()
|
||||
page = await browser.new_page(viewport={"width": 1280, "height": 720})
|
||||
try:
|
||||
await page.set_content(html_response.body.decode("UTF-8"))
|
||||
await page.screenshot(
|
||||
path=file_path,
|
||||
type="png",
|
||||
clip={"x": 0, "y": 0, "width": 1200, "height": 600},
|
||||
)
|
||||
finally:
|
||||
await page.close() # ~2ms vs ~1500ms for full browser close+relaunch
|
||||
```
|
||||
|
||||
**FastAPI lifespan integration:**
|
||||
|
||||
```python
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup: warm up browser
|
||||
await get_browser()
|
||||
yield
|
||||
# Shutdown: clean up
|
||||
await shutdown_browser()
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
```
|
||||
|
||||
**Expected result:** ~3.0s → ~1.0-1.5s per card (browser overhead eliminated).
|
||||
|
||||
**Safety:** If the browser crashes or disconnects, `get_browser()` detects via
|
||||
`_browser.is_connected()` and relaunches automatically. Page-level errors are isolated — a
|
||||
crashed page does not affect other pages in the same browser.
|
||||
|
||||
### Optimization 2: Self-Hosted Fonts (Priority: Immediate)
|
||||
|
||||
The card HTML template loads Google Fonts via CDN during every `set_content()` call:
|
||||
|
||||
```css
|
||||
/* Current: network round-trip on every render */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;700&display=swap');
|
||||
```
|
||||
|
||||
Self-hosting eliminates the ~0.3-0.5s network dependency. Two approaches:
|
||||
|
||||
**Option A: Base64-embed in template (simplest, no infrastructure change):**
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'Source Sans 3';
|
||||
font-weight: 400;
|
||||
src: url(data:font/woff2;base64,<base64_data>) format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans 3';
|
||||
font-weight: 700;
|
||||
src: url(data:font/woff2;base64,<base64_data>) format('woff2');
|
||||
}
|
||||
```
|
||||
|
||||
**Option B: Local filesystem served via Playwright's `route` API:**
|
||||
|
||||
```python
|
||||
await page.route("**/fonts.googleapis.com/**", lambda route: route.fulfill(
|
||||
path="/app/storage/fonts/source-sans-3.css"
|
||||
))
|
||||
await page.route("**/fonts.gstatic.com/**", lambda route: route.fulfill(
|
||||
path=f"/app/storage/fonts/{route.request.url.split('/')[-1]}"
|
||||
))
|
||||
```
|
||||
|
||||
Option A is preferred — it makes the template fully self-contained with zero external
|
||||
dependencies, which also makes it work in airgapped/offline environments.
|
||||
|
||||
**Expected result:** Additional ~0.3-0.5s saved. Combined with persistent browser: **~0.6-1.0s per card**.
|
||||
|
||||
### Optimization 3: Concurrent Upload Pipeline (Priority: High)
|
||||
|
||||
The upload loop in `pd_cards/core/upload.py` processes cards sequentially. Adding
|
||||
semaphore-bounded concurrency allows multiple cards to render and upload in parallel:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def upload_cardset(all_players, session, concurrency=8):
|
||||
"""Upload cards with bounded concurrency."""
|
||||
sem = asyncio.Semaphore(concurrency)
|
||||
results = []
|
||||
|
||||
async def process_card(player):
|
||||
async with sem:
|
||||
image_bytes = await fetch_card_image(session, card_url, timeout=10)
|
||||
if image_bytes:
|
||||
s3_url = await upload_card_to_s3(image_bytes, ...)
|
||||
await update_player_url(session, player_id, s3_url)
|
||||
return player_id
|
||||
return None
|
||||
|
||||
results = await asyncio.gather(
|
||||
*[process_card(p) for p in all_players],
|
||||
return_exceptions=True
|
||||
)
|
||||
|
||||
successes = [r for r in results if r is not None and not isinstance(r, Exception)]
|
||||
failures = [r for r in results if isinstance(r, Exception)]
|
||||
return successes, failures
|
||||
```
|
||||
|
||||
**Concurrency tuning:** The API server's Chromium instance handles multiple pages concurrently
|
||||
within a single browser process. Each page consumes ~50-100 MB RAM. With 16 GB server RAM:
|
||||
|
||||
| Concurrency | RAM Usage | Cards/Minute | 800 Cards |
|
||||
|---|---|---|---|
|
||||
| 1 (current) | ~200 MB | ~60-100 | ~8-13 min |
|
||||
| 4 | ~600 MB | ~240 | ~3-4 min |
|
||||
| 8 | ~1 GB | ~480 | ~2 min |
|
||||
| 16 | ~2 GB | ~800+ | ~1 min |
|
||||
|
||||
Start with `concurrency=8` and tune based on server load. The `fetch_card_image` timeout should
|
||||
increase from 6s to 10s to account for render queue contention.
|
||||
|
||||
**Expected result:** Combined with persistent browser + local fonts, an 800-card run goes from
|
||||
**~40 minutes to ~2-4 minutes**.
|
||||
|
||||
### Optimization 4: GPU Acceleration (Priority: Skip)
|
||||
|
||||
Chromium supports `--enable-gpu` and `--use-gl=egl` for hardware-accelerated rendering. However:
|
||||
|
||||
- Card images are flat 2D layout — Skia's software rasterizer is already fast for this
|
||||
- GPU passthrough in Docker requires NVIDIA container toolkit + EGL libraries
|
||||
- Expected gain for 2D card images: **~5-10%** — not worth the infrastructure complexity
|
||||
- GPU becomes relevant if we add WebGL effects or complex CSS 3D transforms in the future
|
||||
|
||||
**Recommendation:** Skip GPU acceleration. The persistent browser + concurrency changes deliver
|
||||
a **20-40x total speedup** without touching infrastructure.
|
||||
|
||||
### Summary: Expected Performance After Optimization
|
||||
|
||||
| Metric | Before | After | Improvement |
|
||||
|---|---|---|---|
|
||||
| Per-card render time | ~3.0s | ~0.6-1.0s | ~3-5x |
|
||||
| 800-card sequential run | ~40 min | ~8-13 min | ~3-5x |
|
||||
| 800-card concurrent run (8x) | N/A | ~2-4 min | ~10-20x vs current |
|
||||
| External dependencies | Google Fonts CDN | None | Offline-capable |
|
||||
| Chromium processes spawned | 800 | 1 | 800x fewer |
|
||||
|
||||
### Implementation Files
|
||||
|
||||
| Change | File | Scope |
|
||||
|---|---|---|
|
||||
| Persistent browser | `database/app/routers_v2/players.py` | Replace `async with async_playwright()` block |
|
||||
| FastAPI lifespan | `database/app/main.py` (or equivalent) | Add startup/shutdown hooks |
|
||||
| Self-hosted fonts | `database/storage/templates/style.html` | Replace `@import` with `@font-face` |
|
||||
| Font files | `database/storage/fonts/` | Download Source Sans 3 + Open Sans WOFF2 files |
|
||||
| Concurrent upload | `pd_cards/core/upload.py` | Replace sequential loop with `asyncio.gather` |
|
||||
| Timeout increase | `pd_cards/core/upload.py` | `fetch_card_image` timeout: 6s → 10s |
|
||||
69
paper-dynasty/card-evolution-prd/03-tracks.md
Normal file
69
paper-dynasty/card-evolution-prd/03-tracks.md
Normal file
@ -0,0 +1,69 @@
|
||||
# 3. Evolution Track Design
|
||||
|
||||
[< Back to Index](README.md) | [Next: Milestones >](04-milestones.md)
|
||||
|
||||
---
|
||||
|
||||
## 3.1 Track Structure
|
||||
|
||||
Each evolution track has **four tiers** (T1 through T4). Each tier contains **two to three
|
||||
milestones**. Completing all milestones in a tier unlocks the tier's rating boost and advances
|
||||
the card to the next tier automatically. Completing all four tiers achieves **Full Evolution**.
|
||||
|
||||
```
|
||||
[Card Acquired]
|
||||
|
|
||||
[T1: Initiate] 2 milestones --> +small rating boost + visual badge
|
||||
|
|
||||
[T2: Rising] 2 milestones --> +medium rating boost + embed accent
|
||||
|
|
||||
[T3: Ascendant] 3 milestones --> +large rating boost + rarity border glow
|
||||
|
|
||||
[T4: Evolved] 2 milestones --> +capstone boost + full visual rework
|
||||
--> rarity tier upgrade (Standard track, Sta and below)
|
||||
--> card name becomes "[Team]'s Evolved [Player]"
|
||||
```
|
||||
|
||||
## 3.2 Track Parameterization
|
||||
|
||||
Track definitions are stored in the database, not hardcoded. Each track record specifies:
|
||||
|
||||
- Which card type it applies to (batter, starting pitcher, relief pitcher)
|
||||
- Rating boost amounts per tier (flat probability point deltas, same regardless of rarity)
|
||||
- Milestone thresholds per tier (admin-tunable)
|
||||
|
||||
**Tracks are universal — not rarity-gated.** A Replacement-level card and a Hall of Fame card
|
||||
follow the same track with the same flat rating deltas. This is intentional:
|
||||
|
||||
- **Rarity fluctuates during live series.** Cards update every 2 weeks during the MLB season.
|
||||
A Reserve card in April could become All-Star by June. Rarity-gated tracks would cause
|
||||
mid-season track jumps, resetting or invalidating progress.
|
||||
- **Flat deltas intentionally favor lower-rarity cards.** A +0.5 chances boost to a Replacement
|
||||
card's homerun probability is a larger relative improvement than the same boost to a HoF card.
|
||||
This gives lower-tier cards a reason to exist beyond crafting fodder and avoids making elite
|
||||
cards overpowered.
|
||||
- **Existing rating caps prevent runaway stats.** The D20 system has hard caps on individual
|
||||
stat columns (e.g., hold rating caps at -5). Evolution boosts that would exceed a cap are
|
||||
truncated, providing a natural ceiling that prevents creating artificial super-cards.
|
||||
|
||||
**Three tracks exist at launch (one per card type):**
|
||||
|
||||
| Track | Card Type | T4 Outcome |
|
||||
|---|---|---|
|
||||
| Batter | All batters | Rarity upgrade (if below HoF) + full visual rework |
|
||||
| Starting Pitcher | SP | Rarity upgrade (if below HoF) + full visual rework |
|
||||
| Relief Pitcher | RP/CP | Rarity upgrade (if below HoF) + full visual rework |
|
||||
|
||||
**Subtracks are on the table.** The schema supports multiple tracks per card type. Before launch,
|
||||
we may add subtracks within each category — e.g., power batter vs contact batter tracks with
|
||||
different milestone compositions, or a closer-specific RP track. The `evolution_track` table
|
||||
is designed to accommodate this without schema changes; adding a subtrack is just inserting new
|
||||
rows with different milestone sets. The decision on how many subtracks to ship with is deferred
|
||||
until milestone design is finalized.
|
||||
|
||||
## 3.3 Evolution Eligibility
|
||||
|
||||
Every card is always eligible. There is no enrollment step, no activation cost, and no slot limit.
|
||||
A card that has not yet reached Full Evolution is always progressing. A fully evolved card
|
||||
(`current_tier = 4`, all milestones complete) cannot progress further — it has reached its peak
|
||||
state. It can still be crafted (#49) or traded.
|
||||
78
paper-dynasty/card-evolution-prd/04-milestones.md
Normal file
78
paper-dynasty/card-evolution-prd/04-milestones.md
Normal file
@ -0,0 +1,78 @@
|
||||
# 4. Milestone Challenge System
|
||||
|
||||
[< Back to Index](README.md) | [Next: Rating Boosts >](05-rating-boosts.md)
|
||||
|
||||
---
|
||||
|
||||
## 4.1 Milestone Design Philosophy
|
||||
|
||||
Milestones are evaluated by querying existing `stratplay`, `decision`, and `stratgame` tables at
|
||||
the `player_id` level, filtered to games played after the card's `created_at` date. No new event
|
||||
tables or pipeline changes needed.
|
||||
|
||||
**Performance Milestones** (looked up from `player_season_stats`):
|
||||
- Record N hits
|
||||
- Hit N home runs
|
||||
- Reach base N times (H + BB + HBP)
|
||||
- Strike out N batters (pitcher)
|
||||
- Record N scoreless appearances (pitcher)
|
||||
|
||||
**Game Outcome Milestones** (looked up from `player_season_stats`):
|
||||
- Win N games (any mode, with card in the rostered team)
|
||||
- Win N games in a specific mode (campaign, gauntlet, unlimited)
|
||||
- Record N saves or holds (pitcher)
|
||||
- Record N quality starts (pitcher, from `decision` where is_start = true + derived ERA check)
|
||||
|
||||
Most milestone types resolve to a single column lookup against `player_season_stats` rather than
|
||||
a full `stratplay` table scan. See [06-database.md](06-database.md) for the aggregate table
|
||||
schema and [02-architecture.md](02-architecture.md) for the incremental update design.
|
||||
|
||||
**Event Milestones** (tracked via post-game callbacks):
|
||||
- Complete a gauntlet event (any placement)
|
||||
- Place top 3 in a gauntlet event
|
||||
- Win a gauntlet event
|
||||
- Complete a campaign stage with this card in the active roster
|
||||
|
||||
## 4.2 Milestone Quantity Targets
|
||||
|
||||
Milestones are calibrated so an average active player (4-6 games per week) completes a full track
|
||||
in 4-8 weeks. This avoids both instant gratification and grind fatigue.
|
||||
|
||||
**Batter example milestones by tier:**
|
||||
|
||||
| Tier | Milestone A | Milestone B | Milestone C |
|
||||
|------|-------------|-------------|-------------|
|
||||
| T1 | Win 3 games with card rostered | Record 15 hits | — |
|
||||
| T2 | Win 8 games with card in active lineup | Hit 5 HR | Reach base 30 times |
|
||||
| T3 | Win 15 games with card in active lineup | Hit 12 HR | Record 60 hits |
|
||||
| T4 | Complete a gauntlet with card rostered | Hit 20 HR | — |
|
||||
|
||||
**Starting pitcher example milestones by tier:**
|
||||
|
||||
| Tier | Milestone A | Milestone B | Milestone C |
|
||||
|------|-------------|-------------|-------------|
|
||||
| T1 | Win 3 games with card rostered | Record 10 strikeouts | — |
|
||||
| T2 | Win 8 games as starter | Record 3 quality starts (6+ innings, 3 or fewer ER equiv) | Record 25 strikeouts |
|
||||
| T3 | Win 15 games as starter | Record 8 quality starts | Record 60 strikeouts |
|
||||
| T4 | Complete a gauntlet with card rostered | Record 12 quality starts | — |
|
||||
|
||||
**Relief pitcher example milestones by tier:**
|
||||
|
||||
| Tier | Milestone A | Milestone B | Milestone C |
|
||||
|------|-------------|-------------|-------------|
|
||||
| T1 | Win 3 games with card rostered | Record 5 holds or saves | — |
|
||||
| T2 | Win 8 games with card active | Record 12 holds or saves | Record 25 strikeouts |
|
||||
| T3 | Win 15 games with card active | Record 20 holds or saves | Record 3 scoreless appearances |
|
||||
| T4 | Complete a gauntlet with card rostered | Record 30 holds or saves | — |
|
||||
|
||||
## 4.3 Milestone Completion Logic
|
||||
|
||||
Milestones are evaluated after each game completes. The post-game callback queries
|
||||
`stratplay`/`decision`/`stratgame` for all cards on the team that participated, computes
|
||||
accumulated progress against each milestone threshold, and updates `evolution_card_state.current_tier`
|
||||
and `evolution_progress.current_count` as needed. Tier completion is evaluated after all milestones
|
||||
in a tier are marked complete. The bot sends a Discord notification to the team channel when a
|
||||
tier is completed and a rating boost is applied.
|
||||
|
||||
Progress can also be recomputed on-demand via the evaluate endpoint (useful for admin correction
|
||||
or cache invalidation after a data fix).
|
||||
171
paper-dynasty/card-evolution-prd/05-rating-boosts.md
Normal file
171
paper-dynasty/card-evolution-prd/05-rating-boosts.md
Normal file
@ -0,0 +1,171 @@
|
||||
# 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.
|
||||
272
paper-dynasty/card-evolution-prd/06-database.md
Normal file
272
paper-dynasty/card-evolution-prd/06-database.md
Normal file
@ -0,0 +1,272 @@
|
||||
# 6. Database Schema
|
||||
|
||||
[< Back to Index](README.md) | [Next: Variant System >](07-variant-system.md)
|
||||
|
||||
---
|
||||
|
||||
## 6.1 New Tables
|
||||
|
||||
### `player_season_stats` (Early Deliverable — Phase 1)
|
||||
|
||||
Pre-computed season totals per `(player_id, team_id, season)`. Updated incrementally in the
|
||||
post-game callback. Used by the evolution milestone evaluator for fast lookups instead of
|
||||
scanning `stratplay`. Also benefits leaderboards, scouting, and any future feature needing
|
||||
season totals.
|
||||
|
||||
**Reference implementation exists in the Major Domo project** (SBA fantasy baseball) — use
|
||||
that as the basis for schema design and incremental update logic.
|
||||
|
||||
```sql
|
||||
CREATE TABLE player_season_stats (
|
||||
id SERIAL PRIMARY KEY,
|
||||
player_id INTEGER NOT NULL REFERENCES player(id),
|
||||
team_id INTEGER NOT NULL REFERENCES team(id),
|
||||
season INTEGER NOT NULL,
|
||||
-- batting totals
|
||||
games_batting INTEGER NOT NULL DEFAULT 0,
|
||||
pa INTEGER NOT NULL DEFAULT 0,
|
||||
ab INTEGER NOT NULL DEFAULT 0,
|
||||
hits INTEGER NOT NULL DEFAULT 0,
|
||||
hr INTEGER NOT NULL DEFAULT 0,
|
||||
doubles INTEGER NOT NULL DEFAULT 0,
|
||||
triples INTEGER NOT NULL DEFAULT 0,
|
||||
bb INTEGER NOT NULL DEFAULT 0,
|
||||
hbp INTEGER NOT NULL DEFAULT 0,
|
||||
so INTEGER NOT NULL DEFAULT 0,
|
||||
rbi INTEGER NOT NULL DEFAULT 0,
|
||||
runs INTEGER NOT NULL DEFAULT 0,
|
||||
sb INTEGER NOT NULL DEFAULT 0,
|
||||
cs INTEGER NOT NULL DEFAULT 0,
|
||||
-- pitching totals
|
||||
games_pitching INTEGER NOT NULL DEFAULT 0,
|
||||
innings NUMERIC(5,1) NOT NULL DEFAULT 0,
|
||||
k INTEGER NOT NULL DEFAULT 0,
|
||||
bb_allowed INTEGER NOT NULL DEFAULT 0,
|
||||
hits_allowed INTEGER NOT NULL DEFAULT 0,
|
||||
hr_allowed INTEGER NOT NULL DEFAULT 0,
|
||||
wins INTEGER NOT NULL DEFAULT 0,
|
||||
losses INTEGER NOT NULL DEFAULT 0,
|
||||
saves INTEGER NOT NULL DEFAULT 0,
|
||||
holds INTEGER NOT NULL DEFAULT 0,
|
||||
blown_saves INTEGER NOT NULL DEFAULT 0,
|
||||
-- metadata
|
||||
last_game_id INTEGER REFERENCES stratgame(id), -- last game included in totals
|
||||
last_updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_player_season UNIQUE (player_id, team_id, season)
|
||||
);
|
||||
CREATE INDEX idx_player_season_stats_team ON player_season_stats(team_id, season);
|
||||
CREATE INDEX idx_player_season_stats_player ON player_season_stats(player_id, season);
|
||||
```
|
||||
|
||||
**Update pattern:** After each game completes, the post-game callback computes deltas from the
|
||||
game's `stratplay`/`decision` rows and adds them to the running totals via
|
||||
`UPDATE ... SET hits = hits + delta_hits, ...`. The `last_game_id` field provides an audit trail
|
||||
and prevents double-counting if the callback retries.
|
||||
|
||||
**Backfill:** A one-time backfill script aggregates all existing `stratplay`/`decision` rows
|
||||
into this table, grouped by `(player_id, team_id, season)`.
|
||||
|
||||
---
|
||||
|
||||
### `evolution_track`
|
||||
|
||||
Defines available evolution tracks. Populated by admin at launch and expandable over time.
|
||||
|
||||
```sql
|
||||
CREATE TABLE evolution_track (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL, -- e.g. "Batter", "Starting Pitcher", "Relief Pitcher"
|
||||
card_type VARCHAR(20) NOT NULL, -- 'batter', 'sp', 'rp'
|
||||
description TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
Tracks are universal per card type — not rarity-gated. All batters follow the same track
|
||||
regardless of whether they are Replacement or Hall of Fame. Rarity upgrade at T4 is universal
|
||||
(capped at HoF). See [03-tracks.md](03-tracks.md) for design rationale.
|
||||
|
||||
### `evolution_milestone`
|
||||
|
||||
Defines individual milestones within a track. Each track has 9-10 milestones across 4 tiers.
|
||||
|
||||
```sql
|
||||
CREATE TABLE evolution_milestone (
|
||||
id SERIAL PRIMARY KEY,
|
||||
track_id INTEGER NOT NULL REFERENCES evolution_track(id),
|
||||
tier SMALLINT NOT NULL CHECK (tier BETWEEN 1 AND 4),
|
||||
name VARCHAR(200) NOT NULL, -- e.g. "First Steps: Win 3 games"
|
||||
description TEXT NOT NULL, -- Player-facing description
|
||||
challenge_type VARCHAR(50) NOT NULL, -- 'win_games', 'record_hits', 'record_hr',
|
||||
-- 'record_k', 'record_sv_hd', 'record_qs',
|
||||
-- 'complete_gauntlet', 'win_gauntlet',
|
||||
-- 'complete_campaign_stage'
|
||||
threshold INTEGER NOT NULL, -- Quantity required (e.g. 15 for 15 hits)
|
||||
game_mode VARCHAR(30), -- NULL = any mode, or 'campaign', 'gauntlet', etc.
|
||||
sort_order SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
### `evolution_card_state`
|
||||
|
||||
Cached evolution tier and metadata per `(player_id, team_id)` pair. This is the fundamental
|
||||
evolution unit — all card instances sharing the same `player_id` on the same team share this
|
||||
single record. Duplicate cards are not differentiated.
|
||||
|
||||
This is a cache — authoritative data lives in the game tables (stratplay, decision, stratgame).
|
||||
Recomputable via evaluate endpoint.
|
||||
|
||||
```sql
|
||||
CREATE TABLE evolution_card_state (
|
||||
id SERIAL PRIMARY KEY,
|
||||
player_id INTEGER NOT NULL REFERENCES player(id),
|
||||
team_id INTEGER NOT NULL REFERENCES team(id),
|
||||
track_id INTEGER NOT NULL REFERENCES evolution_track(id),
|
||||
current_tier SMALLINT NOT NULL DEFAULT 0, -- 0 = no tier complete yet
|
||||
variant INTEGER, -- current variant hash (shared by all card instances)
|
||||
progress_since TIMESTAMP NOT NULL, -- earliest card.created_at for this player+team
|
||||
last_evaluated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
fully_evolved BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
initial_rarity_id INTEGER REFERENCES rarity(id),
|
||||
final_rarity_id INTEGER REFERENCES rarity(id), -- set on T4 completion
|
||||
CONSTRAINT uq_evolution_player_team UNIQUE (player_id, team_id)
|
||||
);
|
||||
CREATE INDEX idx_evolution_card_state_team ON evolution_card_state(team_id);
|
||||
CREATE INDEX idx_evolution_card_state_player ON evolution_card_state(player_id);
|
||||
```
|
||||
|
||||
**Key design points:**
|
||||
- `card_id` is intentionally absent — the state belongs to `(player_id, team_id)`, not to a
|
||||
card instance. All card instances with the same player_id on the same team read from this record.
|
||||
- `variant` is stored here (the single source of truth) and propagated to all matching
|
||||
`card.variant` fields when it changes.
|
||||
- `progress_since` uses the earliest `card.created_at` among all cards of this player on this team.
|
||||
|
||||
### `evolution_progress`
|
||||
|
||||
Tracks per-milestone completion state for each `(player_id, team_id)`. One row per (card_state, milestone).
|
||||
|
||||
```sql
|
||||
CREATE TABLE evolution_progress (
|
||||
id SERIAL PRIMARY KEY,
|
||||
card_state_id INTEGER NOT NULL REFERENCES evolution_card_state(id),
|
||||
milestone_id INTEGER NOT NULL REFERENCES evolution_milestone(id),
|
||||
current_count INTEGER NOT NULL DEFAULT 0,
|
||||
completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
completed_at TIMESTAMP,
|
||||
CONSTRAINT uq_card_milestone UNIQUE (card_state_id, milestone_id)
|
||||
);
|
||||
CREATE INDEX idx_evolution_progress_card_state ON evolution_progress(card_state_id);
|
||||
```
|
||||
|
||||
### `evolution_tier_boost`
|
||||
|
||||
Audit record of rating changes applied when a tier is completed. Allows rollback and inspection.
|
||||
|
||||
```sql
|
||||
CREATE TABLE evolution_tier_boost (
|
||||
id SERIAL PRIMARY KEY,
|
||||
card_state_id INTEGER NOT NULL REFERENCES evolution_card_state(id),
|
||||
tier SMALLINT NOT NULL,
|
||||
battingcard_id INTEGER REFERENCES battingcard(id), -- NULL if pitcher
|
||||
pitchingcard_id INTEGER REFERENCES pitchingcard(id), -- NULL if batter
|
||||
variant_created INTEGER NOT NULL, -- the variant hash written
|
||||
boost_delta_json JSONB NOT NULL, -- column deltas applied, keyed by vs_hand
|
||||
profile_used VARCHAR(50) NOT NULL, -- 'auto_power', 'auto_contact', 'override_contact', etc.
|
||||
applied_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### `evolution_cosmetic`
|
||||
|
||||
Tracks cosmetic purchases per `(player_id, team_id)` via the evolution_card_state FK. All
|
||||
duplicate card instances of the same player on the same team share cosmetics. This is the
|
||||
primary revenue table for the evolution system.
|
||||
|
||||
```sql
|
||||
CREATE TABLE evolution_cosmetic (
|
||||
id SERIAL PRIMARY KEY,
|
||||
card_state_id INTEGER NOT NULL REFERENCES evolution_card_state(id),
|
||||
cosmetic_type VARCHAR(50) NOT NULL, -- 'frame_gold', 'frame_animated', 'border_glow',
|
||||
-- 'shimmer_effect', 'tier_badge_premium', etc.
|
||||
cosmetic_key VARCHAR(100) NOT NULL, -- specific variant/skin identifier
|
||||
cost_paid INTEGER NOT NULL, -- manticoins spent
|
||||
purchased_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
CREATE INDEX idx_evolution_cosmetic_card ON evolution_cosmetic(card_state_id);
|
||||
```
|
||||
|
||||
**Note:** `team_id` is not stored directly — it is available via `card_state_id → evolution_card_state.team_id`.
|
||||
This avoids redundancy and ensures cosmetics are always consistent with the owning team.
|
||||
|
||||
## 6.2 Modified Tables
|
||||
|
||||
### `battingcard` / `pitchingcard` — Add `image_url` and `image_format` columns
|
||||
|
||||
```sql
|
||||
ALTER TABLE battingcard ADD COLUMN image_url VARCHAR(500) NULL;
|
||||
ALTER TABLE battingcard ADD COLUMN image_format VARCHAR(10) NULL DEFAULT 'png';
|
||||
ALTER TABLE pitchingcard ADD COLUMN image_url VARCHAR(500) NULL;
|
||||
ALTER TABLE pitchingcard ADD COLUMN image_format VARCHAR(10) NULL DEFAULT 'png';
|
||||
```
|
||||
|
||||
For `variant = 0` rows, `image_url` remains NULL (use `player.image` / `player.image2` as before).
|
||||
For evolution/cosmetic variant rows, `image_url` stores the S3 URL for the rendered card with
|
||||
boosts and cosmetics baked in. `image_format` indicates whether the image is `'png'` (static) or
|
||||
`'apng'` (animated). This keeps the image co-located with the ratings data it was rendered from.
|
||||
The `battingcard`/`pitchingcard` tables are not auth-gated (unlike the ratings tables), so the
|
||||
bot can always fetch the image URL.
|
||||
|
||||
The existing `UNIQUE(player_id, variant)` constraint enforces one set of ratings and one image
|
||||
per player per variant hash.
|
||||
|
||||
### `card` — Add `variant` column
|
||||
|
||||
```sql
|
||||
ALTER TABLE card ADD COLUMN variant INTEGER NULL DEFAULT NULL;
|
||||
```
|
||||
|
||||
`variant` defaults to NULL, meaning the card uses `variant = 0` (base card, `player.image`).
|
||||
When evolution state changes (tier completion or cosmetic purchase), the new
|
||||
variant hash is computed in `evolution_card_state.variant` and propagated to `card.variant` on
|
||||
**all** card instances matching that `(player_id, team_id)` pair. The bot's image lookup:
|
||||
|
||||
```python
|
||||
if card.variant is not None:
|
||||
batting_card = BattingCard.get(player_id=card.player_id, variant=card.variant)
|
||||
image_url = batting_card.image_url # S3 URL with cosmetics/boosts baked in
|
||||
else:
|
||||
image_url = card.player.image # standard base card image
|
||||
```
|
||||
|
||||
**Variant propagation:** When `evolution_card_state.variant` changes, all card instances with
|
||||
the matching `player_id` owned by the matching `team_id` are updated:
|
||||
|
||||
```python
|
||||
Card.update(variant=new_variant).where(
|
||||
Card.player_id == player_id,
|
||||
Card.team_id == team_id,
|
||||
).execute()
|
||||
```
|
||||
|
||||
Thousands of existing cards will have `variant = NULL` — no backfill needed for the column itself.
|
||||
The `evolution_card_state` backfill job handles computing variant hashes for cards with evolution
|
||||
progress.
|
||||
|
||||
## 6.3 Schema Relationships Diagram (text)
|
||||
|
||||
```
|
||||
team ──< evolution_card_state(player_id, team_id) ──> player
|
||||
| |
|
||||
| (variant propagated to all matching ├──< battingcard(variant=0..<hash>)
|
||||
| card instances) │ ├── battingcardratings
|
||||
| │ ├── image_url (S3, nullable)
|
||||
| card.variant ← evo_state.variant │ └── image_format ('png' | 'apng')
|
||||
| │
|
||||
├──< evolution_progress >── evolution_milestone >── evolution_track
|
||||
|
|
||||
├──< evolution_tier_boost
|
||||
|
|
||||
└──< evolution_cosmetic
|
||||
```
|
||||
69
paper-dynasty/card-evolution-prd/07-variant-system.md
Normal file
69
paper-dynasty/card-evolution-prd/07-variant-system.md
Normal file
@ -0,0 +1,69 @@
|
||||
# 7. Card Model Changes and Variant System
|
||||
|
||||
[< Back to Index](README.md) | [Next: Display and Visual Identity >](08-display.md)
|
||||
|
||||
---
|
||||
|
||||
## 7.1 How the Game Engine Resolves Ratings and Images for a Card
|
||||
|
||||
When the bot builds a game lineup or displays a card, it checks `card.variant`:
|
||||
|
||||
**For ratings (game engine):**
|
||||
1. Read `card.variant` from the card instance
|
||||
2. Call `GET battingcardratings/player/{player_id}?variant={card.variant}` — the API filters by
|
||||
`(player_id, variant)` and returns the matching `battingcard` row with its nested vL/vR ratings
|
||||
3. Each variant has its own distinct `battingcard` row (with `battingcard.variant` matching
|
||||
`card.variant`). The ratings rows themselves don't carry a variant — they belong to a specific
|
||||
`battingcard`, and the variant distinguishes which `battingcard` to use
|
||||
4. `variant = 0` is the base card (unchanged behavior); evolved/cosmetic variants use higher values
|
||||
|
||||
> **Current implementation:** `card.variant` does not exist, yet, but `battingcard.variant` column does already exist
|
||||
> (default 0). The API endpoint and game engine query path (`gameplay_queries.py →
|
||||
> get_batter_scouting_or_none`) already pass variant through. The local Postgres cache
|
||||
> (`batterscouting`) keys on `battingcard_id`, which is inherently variant-specific since each
|
||||
> variant produces a separate `battingcard` row. Same pattern for `pitchingcard`/`pitcherscouting`.
|
||||
|
||||
**For images (card display):**
|
||||
1. Read `card.variant` from the card instance
|
||||
2. If `variant` is not 0: fetch `battingcard.image_url` for that variant — the pre-rendered
|
||||
image (PNG or APNG) with cosmetics and evolution visuals baked in
|
||||
3. If `variant` is 0: use `player.image` / `player.image2` (base card, unchanged behavior)
|
||||
|
||||
> **Migration required:** `battingcard.image_url` and `pitchingcard.image_url` columns do not
|
||||
> exist yet — they must be added (nullable varchar). Current image resolution uses `player.image`
|
||||
> and `player.image2` fields exclusively (checked via `Player.batter_card_url` /
|
||||
> `Player.pitcher_card_url` properties in `gameplay_models.py`, and `helpers.player_bcard` /
|
||||
> `helpers.player_pcard` in the legacy cog system). Bot display logic must be updated to check
|
||||
> `battingcard.image_url` first when `card.variant != 0`, falling back to `player.image`.
|
||||
|
||||
The variant field on the card instance acts as a simple pointer. The bot does not need to know
|
||||
about evolution tiers, cosmetics, or boost profiles — it just reads the variant and gets the
|
||||
right ratings and image. All complexity is in the variant creation/update path, not the read path.
|
||||
|
||||
**All card instances of the same `player_id` on the same team share the same variant.** The
|
||||
variant is stored authoritatively on `evolution_card_state` and propagated to `card.variant` on
|
||||
all matching instances when it changes. Duplicate cards are not differentiated.
|
||||
|
||||
## 7.2 Evolved Card Naming
|
||||
|
||||
When `evolution_card_state.fully_evolved = true`, the card display name is modified:
|
||||
- Base name: `"Mike Trout"`
|
||||
- Evolved name: `"[TeamName]'s Evolved Mike Trout"`
|
||||
|
||||
The `player.p_name` field is NOT modified (it is a shared blueprint). The evolved name is
|
||||
computed dynamically from the card state record at display time. This preserves data integrity
|
||||
and allows future re-display if the naming convention changes.
|
||||
|
||||
## 7.3 Card Uniqueness
|
||||
|
||||
**Within a team:** All copies of the same `player_id` are identical — same evolution state, same
|
||||
variant, same boosts, same cosmetics. Duplicates are not differentiated in any way.
|
||||
|
||||
**Across teams:** Two teams that evolve the same `player_id` with the same boost profile and
|
||||
same cosmetics will share the same variant hash — and therefore the same ratings rows and S3
|
||||
image. This is by design: shared variants avoid redundant storage and rendering.
|
||||
|
||||
Uniqueness diverges when teams purchase different cosmetics — each distinct combination produces
|
||||
a different variant hash, creating a genuinely different card with its own rendered image.
|
||||
Premium cosmetic customization is the path to a truly unique card. Rating boosts are determined
|
||||
by auto-detected card profile and are not customizable.
|
||||
402
paper-dynasty/card-evolution-prd/08-display.md
Normal file
402
paper-dynasty/card-evolution-prd/08-display.md
Normal file
@ -0,0 +1,402 @@
|
||||
# 8. Discord Display, Visual Identity, and Animated Cards
|
||||
|
||||
[< Back to Index](README.md) | [Next: Integrations >](09-integrations.md)
|
||||
|
||||
---
|
||||
|
||||
## 8.1 Tier Visual Indicators
|
||||
|
||||
Evolved cards display visual indicators in Discord embeds that signal evolution status without
|
||||
requiring image regeneration (which is expensive and slow):
|
||||
|
||||
| Evolution State | Embed Change |
|
||||
|---|---|
|
||||
| Not evolved (T0) | Standard embed, no change |
|
||||
| T1 complete | Small badge emoji appended to card name field: `Mike Trout [T1]` |
|
||||
| T2 complete | Badge updates: `Mike Trout [T2]` + embed accent color changes to gold |
|
||||
| T3 complete | Badge: `Mike Trout [T3]` + embed color changes to purple |
|
||||
| T4 full evolved | Name changes to `[Team]'s Evolved Mike Trout` + embed color changes to teal |
|
||||
|
||||
> **TBD:** Tier badge icons are placeholders and likely to change. Current candidates:
|
||||
> T1 = `:seedling:`, T2 = `:star:`, T3 = `:gem:`, T4 = `:crown:`. Final icons will be
|
||||
> chosen during implementation based on Discord rendering quality and visual distinctiveness.
|
||||
|
||||
## 8.2 Evolution Status Embed
|
||||
|
||||
A dedicated `!evo status` command (or equivalent slash command) displays evolution status for
|
||||
all cards on the roster (or a specific card):
|
||||
|
||||
```
|
||||
[Team Name]'s Evolution Progress
|
||||
================================
|
||||
Mike Trout (All-Star) — T2 in progress
|
||||
Standard Batter Track
|
||||
Tier 2 milestones:
|
||||
[x] Win 8 games with card in lineup (8/8)
|
||||
[x] Hit 5 HR (5/5)
|
||||
[ ] Reach base 30 times (22/30) <-- in progress
|
||||
Tier 2 unlocks: +1.0 budget (contact profile)
|
||||
Boost profile: auto-contact
|
||||
|
||||
Babe Ruth (HoF) — T1 complete, T2 in progress
|
||||
Legend Batter Track
|
||||
...
|
||||
|
||||
Type !evo card <card_id> for detailed milestone breakdown.
|
||||
Type !evo cosmetics <card_id> to browse premium visual upgrades.
|
||||
```
|
||||
|
||||
## 8.3 Milestone Completion Notification
|
||||
|
||||
When a milestone is completed, the bot sends a notification in the team's designated channel:
|
||||
|
||||
```
|
||||
[Seedling] Evolution milestone completed!
|
||||
Mike Trout — T1, Milestone 2: "Record 15 Hits" COMPLETE
|
||||
Tier 1 is now fully complete! Rating boost applied: +1.0 budget (contact profile)
|
||||
[Mike Trout's card is now Tier 1 Evolved]
|
||||
```
|
||||
|
||||
When a full tier is completed and the rarity upgrade fires:
|
||||
```
|
||||
[Crown] FULL EVOLUTION COMPLETE!
|
||||
"[TeamName]'s Evolved Mike Trout" — Starter -> All-Star Upgrade Applied!
|
||||
Rating boost: +1.0 budget capstone applied (4.0 total across all tiers).
|
||||
This card is now permanently upgraded on your roster.
|
||||
```
|
||||
|
||||
## 8.4 Premium Cosmetics Display
|
||||
|
||||
Premium cosmetics purchased via the currency store apply visual upgrades to the card's Discord
|
||||
embed beyond the standard tier badge system:
|
||||
|
||||
For the full cosmetics catalog with pricing, see [10-economy.md § 10.3](10-economy.md#103-cosmetics-pricing).
|
||||
|
||||
| Cosmetic | Effect | Image Format |
|
||||
|---|---|---|
|
||||
| Gold Frame | Card embed styled with gold border accent | Static PNG |
|
||||
| Diamond Frame | Diamond shimmer gradient border | Static PNG |
|
||||
| Team Color Frame | Solid team-color border | Static PNG |
|
||||
| Holographic Frame | Animated rainbow gradient border shimmer | **Animated APNG** |
|
||||
| Dark Mode Theme | Charcoal header + dark column backgrounds | Static PNG |
|
||||
| Midnight Theme | Deep navy/black column backgrounds | Static PNG |
|
||||
| Prestige Badge | T4 badge upgraded from `:crown:` to `:crown::sparkles:` with custom label | Static PNG |
|
||||
| Rarity Glow (Subtle) | Subtle pulsing glow matching rarity color | **Animated APNG** |
|
||||
| Rarity Glow (Strong) | Intense pulsing glow matching rarity color | **Animated APNG** |
|
||||
| Full Art Reveal | Custom card art background | Static PNG |
|
||||
|
||||
Cosmetics are purchased per `player_id` for a specific team. Since `player_id` maps 1:many with
|
||||
`card_id`, purchasing a cosmetic for a player applies to all duplicate cards of that player on
|
||||
the team — the variant hash is recomputed for all of them. Cosmetics are attached to the
|
||||
`evolution_card_state` record (keyed by `player_id + team_id`), so duplicates always share the
|
||||
same visual treatment.
|
||||
|
||||
When a card is traded, the new owner does **not** inherit purchased cosmetics — those remain
|
||||
with the selling team's evolution state. The traded card's cosmetic display reverts to the
|
||||
standard tier badge for the new owner.
|
||||
|
||||
This creates a flex incentive: players invest in cosmetics for cards they intend to keep and show
|
||||
off. It also means a card with premium cosmetics is visually distinctive in gauntlet brackets
|
||||
and trading discussions, providing social proof of investment.
|
||||
|
||||
## 8.5 Card Image Generation for Variants (Static)
|
||||
|
||||
When a new variant is created (tier completion or cosmetic purchase), the system:
|
||||
|
||||
1. Computes the variant hash from `(player_id, evolution_tier, cosmetics)`
|
||||
2. Checks if a `battingcard` row with that hash already exists — if so, reuses it
|
||||
3. If not, creates the variant's rating rows (base + evolution boosts applied)
|
||||
4. Determines whether the variant requires animation (see § 8.6) or is static PNG
|
||||
5. For **static variants**: renders the card via the existing Playwright pipeline, with cosmetic
|
||||
CSS injected into the HTML template (frame overlays, background gradients, evolution badge, etc.)
|
||||
6. Uploads the PNG to S3 at `cards/cardset-{id}/player-{player_id}/v{variant}/battingcard.png`
|
||||
7. Stores the S3 URL in `battingcard.image_url`
|
||||
8. Updates `card.variant` on all card instances that should use this variant
|
||||
|
||||
The image format (PNG vs APNG) is determined by the file extension in the S3 URL stored in
|
||||
`battingcard.image_url` — no separate format column is needed. The bot and Discord both handle
|
||||
`.png` and `.apng` URLs identically.
|
||||
|
||||
The ~5 second render + upload cost is paid **once per variant creation**, not on every display.
|
||||
Subsequent displays are instant — the bot reads `battingcard.image_url` and puts it in the embed
|
||||
just like it reads `player.image` today.
|
||||
|
||||
**Cosmetic CSS injection** is handled by passing cosmetic parameters to the card rendering
|
||||
endpoint. The HTML template conditionally applies CSS classes based on cosmetic flags:
|
||||
- Frame overlays → CSS border/outline/box-shadow on the card container
|
||||
- Background themes → replace hardcoded `#ACE6FF`/`#EAA49C` with themed colors
|
||||
- Evolution badge → additional `<img>` element alongside the rarity badge
|
||||
- Header gradient → custom gradient colors replacing the standard blue/red
|
||||
|
||||
See [14-risks.md § Open Question 9](14-risks.md) for CSS implementation approach discussion.
|
||||
|
||||
---
|
||||
|
||||
## 8.6 Animated Card Pipeline
|
||||
|
||||
### 8.6.1 Why APNG
|
||||
|
||||
Several cosmetic effects benefit from animation: holographic frame shimmer, rarity glow pulses,
|
||||
evolution badge sparkle effects. The format choice matters:
|
||||
|
||||
| Format | Discord Embed | Browser `<img>` | Color Depth | File Size |
|
||||
|---|---|---|---|---|
|
||||
| **APNG** | Auto-plays, full support | Native, all browsers | Full 24-bit + alpha | ~1-4 MB for 8-12 frames |
|
||||
| GIF | Auto-plays | Native | **256 colors** — visible banding on gradients | ~0.5-2 MB |
|
||||
| Animated WebP | Works in most clients | Native, modern browsers | Full color | ~0.8-3 MB |
|
||||
| MP4/WebM | **Not supported** in `set_image` | Requires `<video>` tag | Full color | Smallest |
|
||||
|
||||
**APNG wins** because it preserves the full PNG color depth our cards rely on (subtle gradients
|
||||
in the blue/red columns, rarity badge details) while working natively in Discord embeds and
|
||||
all modern browsers. GIF's 256-color limit would visibly degrade card quality. MP4/WebM cannot
|
||||
be used in Discord embed images at all.
|
||||
|
||||
### 8.6.2 Which Cosmetics Trigger Animation
|
||||
|
||||
Not all cosmetics need animation. Only cosmetics that involve visible motion produce APNG output.
|
||||
All others remain static PNG (smaller file, faster to render).
|
||||
|
||||
| Cosmetic | Animated? | Effect Description |
|
||||
|---|---|---|
|
||||
| Holographic Frame | Yes | Rainbow gradient shifts position across frames |
|
||||
| Rarity Glow (Subtle) | Yes | Border glow opacity pulses 0.3 → 0.6 → 0.3 over ~2 seconds |
|
||||
| Rarity Glow (Strong) | Yes | Border glow opacity pulses 0.5 → 1.0 → 0.5, larger spread |
|
||||
| T4 Crown Badge sparkle | Yes | Small sparkle particles rotate around the crown icon |
|
||||
| Gold Frame | No | Static gold border + subtle shadow |
|
||||
| Diamond Frame | No | Static diamond-gradient border |
|
||||
| Dark/Midnight themes | No | Static background color swap |
|
||||
| Team Color Frame | No | Static solid border |
|
||||
|
||||
When a variant includes **any** animated cosmetic, the entire card is rendered as APNG.
|
||||
When all cosmetics are static, the card stays PNG.
|
||||
|
||||
### 8.6.3 Render Pipeline
|
||||
|
||||
The animated card pipeline builds on the **persistent browser** optimization described in
|
||||
[02-architecture.md](02-architecture.md#card-render-pipeline-optimization). Rather than launching
|
||||
a new Chromium process, frames are captured using a page from the shared browser instance.
|
||||
|
||||
The pipeline uses **deterministic frame control** via a CSS custom property (`--anim-progress`)
|
||||
rather than relying on CSS animation timing. This guarantees identical output across renders and
|
||||
eliminates timing jitter.
|
||||
|
||||
```
|
||||
1. Get page from persistent browser (shared instance, ~2ms)
|
||||
2. Load HTML template with cosmetic CSS + animation custom properties
|
||||
3. For each frame i in range(N):
|
||||
a. Set --anim-progress = i / N (0.0 → 0.9)
|
||||
b. CSS calc() expressions update all animated properties from progress value
|
||||
c. page.screenshot() captures 1200x600 PNG frame buffer
|
||||
4. Close page (~2ms)
|
||||
5. Assemble frames into APNG using Python apng library
|
||||
- Frame delay: 166ms (6 FPS) for smooth loop without excessive file size
|
||||
- Loop count: 0 (infinite)
|
||||
6. Optional: pngquant-compress each frame before assembly if over file size budget
|
||||
7. Upload APNG to S3 with content-type image/apng
|
||||
8. Store URL in battingcard.image_url (format inferred from .apng extension)
|
||||
```
|
||||
|
||||
**Frame count targets by effect:**
|
||||
|
||||
| Effect | Frames | Duration | Rationale |
|
||||
|---|---|---|---|
|
||||
| Holographic shimmer | 12 | 2.0s | Full gradient cycle needs more frames for smoothness |
|
||||
| Rarity glow pulse | 8 | 1.3s | Simple opacity fade needs fewer frames |
|
||||
| Badge sparkle | 10 | 1.7s | Particle rotation at medium smoothness |
|
||||
|
||||
**Performance (with persistent browser):** Each screenshot takes ~0.4s with the persistent
|
||||
browser (no launch overhead). Assembly adds ~1s.
|
||||
|
||||
| Variant Type | Frames | Capture Time | Assembly | Total |
|
||||
|---|---|---|---|---|
|
||||
| Static PNG | 1 | ~0.4s | — | ~0.6s |
|
||||
| 8-frame APNG | 8 | ~3.2s | ~1.0s | ~4.5s |
|
||||
| 12-frame APNG | 12 | ~4.8s | ~1.0s | ~6.0s |
|
||||
|
||||
Since animated cosmetics are premium purchases (not bulk operations), the volume is low —
|
||||
estimated <50 animated variants per month at steady state.
|
||||
|
||||
### 8.6.4 Deterministic Frame Control
|
||||
|
||||
Animated card rendering uses a **custom CSS property** (`--anim-progress`) to drive all
|
||||
animation state, rather than relying on CSS `animation` timing. This ensures:
|
||||
|
||||
- **Reproducible output:** Same inputs always produce identical frames
|
||||
- **No timing jitter:** No dependency on Playwright's internal clock or screenshot timing
|
||||
- **Simpler debugging:** Each frame's visual state is fully determined by a single `0.0 → 1.0` value
|
||||
|
||||
**Python frame capture loop:**
|
||||
|
||||
```python
|
||||
async def capture_animated_frames(
|
||||
page: Page,
|
||||
html_content: str,
|
||||
frame_count: int = 10,
|
||||
) -> list[bytes]:
|
||||
"""Capture N deterministic animation frames from a card template.
|
||||
|
||||
Each frame sets --anim-progress to i/frame_count, which CSS calc()
|
||||
expressions use to compute all animated property values.
|
||||
"""
|
||||
await page.set_content(html_content)
|
||||
frames = []
|
||||
|
||||
for i in range(frame_count):
|
||||
progress = i / frame_count # 0.0, 0.1, 0.2, ... 0.9
|
||||
await page.evaluate(
|
||||
f"document.documentElement.style.setProperty('--anim-progress', '{progress}')"
|
||||
)
|
||||
frame = await page.screenshot(
|
||||
type="png",
|
||||
clip={"x": 0, "y": 0, "width": 1200, "height": 600},
|
||||
)
|
||||
frames.append(frame)
|
||||
|
||||
return frames
|
||||
```
|
||||
|
||||
**CSS using `--anim-progress` (replaces @keyframes):**
|
||||
|
||||
```css
|
||||
:root {
|
||||
--anim-progress: 0; /* set by Python per frame: 0.0 → 0.9 */
|
||||
}
|
||||
|
||||
/* Holographic frame shimmer — gradient position driven by progress */
|
||||
.frame-holographic {
|
||||
border: 4px solid transparent;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#ff0000, #ff8800, #ffff00, #00ff00,
|
||||
#0088ff, #8800ff, #ff0088, #ff0000
|
||||
) border-box;
|
||||
background-size: 400% 400%;
|
||||
background-position: calc(var(--anim-progress) * 400%) 50%;
|
||||
}
|
||||
|
||||
/* Rarity glow pulse — opacity follows a sine-like curve over progress
|
||||
Approximation: triangle wave via abs(progress - 0.5) * 2
|
||||
At progress=0: opacity=0.3, progress=0.5: opacity=0.8, progress=1: opacity=0.3 */
|
||||
.glow-subtle {
|
||||
--glow-intensity: calc(0.3 + 0.5 * (1 - abs(var(--anim-progress) - 0.5) * 2));
|
||||
box-shadow: 0 0 calc(8px + 12px * var(--glow-intensity)) calc(2px + 4px * var(--glow-intensity)) var(--rarity-color);
|
||||
opacity: var(--glow-intensity);
|
||||
}
|
||||
|
||||
.glow-strong {
|
||||
--glow-intensity: calc(0.5 + 0.5 * (1 - abs(var(--anim-progress) - 0.5) * 2));
|
||||
box-shadow: 0 0 calc(12px + 20px * var(--glow-intensity)) calc(4px + 8px * var(--glow-intensity)) var(--rarity-color);
|
||||
opacity: var(--glow-intensity);
|
||||
}
|
||||
|
||||
/* T4 Crown badge sparkle — rotation driven by progress */
|
||||
.badge-sparkle {
|
||||
transform: rotate(calc(var(--anim-progress) * 360deg));
|
||||
}
|
||||
```
|
||||
|
||||
**Why not CSS @keyframes?** CSS animation timing in headless Chromium depends on internal clock
|
||||
ticks between screenshots. Two renders of the same card could produce slightly different frames
|
||||
if the timing drifts. The `--anim-progress` approach makes each frame's visual state a pure
|
||||
function of its index — deterministic and reproducible.
|
||||
|
||||
### 8.6.5 APNG Assembly
|
||||
|
||||
Python assembly using the `apng` library:
|
||||
|
||||
```python
|
||||
from apng import APNG, PNG
|
||||
|
||||
def assemble_apng(frame_buffers: list[bytes], delay_ms: int = 166) -> bytes:
|
||||
"""Assemble PNG frame buffers into an APNG.
|
||||
|
||||
Args:
|
||||
frame_buffers: List of PNG image bytes (one per frame).
|
||||
delay_ms: Delay between frames in milliseconds.
|
||||
|
||||
Returns:
|
||||
APNG file bytes ready for S3 upload.
|
||||
"""
|
||||
apng = APNG()
|
||||
for buf in frame_buffers:
|
||||
png = PNG.from_bytes(buf)
|
||||
apng.append(png, delay=delay_ms, delay_den=1000)
|
||||
output = io.BytesIO()
|
||||
apng.save(output)
|
||||
return output.getvalue()
|
||||
```
|
||||
|
||||
**Dependencies:** `pip install apng` (pure Python, no native deps). The `apng` library handles
|
||||
APNG chunk encoding per the PNG spec extension. No need for external tools like `apngasm`.
|
||||
|
||||
### 8.6.6 S3 Storage and Content-Type
|
||||
|
||||
Animated cards use `.apng` extension and `image/apng` content type:
|
||||
|
||||
```python
|
||||
s3_key = f"cards/cardset-{cardset_id:03d}/player-{player_id}/v{variant}/battingcard.apng"
|
||||
|
||||
await s3_client.put_object(
|
||||
Bucket=bucket,
|
||||
Key=s3_key,
|
||||
Body=apng_bytes,
|
||||
ContentType="image/apng",
|
||||
CacheControl="public, max-age=31536000", # immutable variant images
|
||||
)
|
||||
```
|
||||
|
||||
Discord and browsers handle `image/apng` identically to `image/png` for display purposes — the
|
||||
APNG spec is a backward-compatible extension of PNG. A client that doesn't support APNG will
|
||||
display the first frame as a static PNG, which is an acceptable graceful degradation.
|
||||
|
||||
### 8.6.7 Bot Display Logic Update
|
||||
|
||||
The bot's image resolution needs a minor update to handle both formats:
|
||||
|
||||
```python
|
||||
if card.variant != 0:
|
||||
bc = BattingCard.get(player_id=card.player_id, variant=card.variant)
|
||||
image_url = bc.image_url # could be .png or .apng — Discord handles both
|
||||
else:
|
||||
image_url = card.player.image # always static PNG
|
||||
|
||||
embed.set_image(url=image_url) # Discord auto-plays APNG in embeds
|
||||
```
|
||||
|
||||
No format-specific logic needed in the bot — Discord's embed renderer handles APNG auto-play
|
||||
transparently. The image format is inferred from the URL extension (`.png` vs `.apng`) when
|
||||
the render pipeline needs to decide whether to re-render as animated.
|
||||
|
||||
### 8.6.8 File Size Budget
|
||||
|
||||
Target file sizes to keep Discord embeds responsive:
|
||||
|
||||
| Type | Target | Max |
|
||||
|---|---|---|
|
||||
| Static PNG | ~150-250 KB | 500 KB |
|
||||
| Animated APNG (8 frames) | ~1.0-2.0 MB | 4 MB |
|
||||
| Animated APNG (12 frames) | ~1.5-3.0 MB | 5 MB |
|
||||
|
||||
Discord's embed image limit is 25 MB, so we have significant headroom. However, large images
|
||||
slow down embed rendering on mobile clients. The targets above keep load times under 1 second
|
||||
on typical connections.
|
||||
|
||||
**Optimization options if sizes run high:**
|
||||
- Reduce frame count (8 instead of 12 — less smooth but much smaller)
|
||||
- Use `pngquant` to lossy-compress each frame before assembly (can cut 50-70% with minimal
|
||||
visual impact on card images)
|
||||
- Only animate the border/glow region and keep the card body static (composite optimization)
|
||||
|
||||
### 8.6.9 Animated Card Cosmetics Pricing
|
||||
|
||||
Animated cosmetics carry a premium over static equivalents, reflecting both the visual impact
|
||||
and the higher render cost:
|
||||
|
||||
| Cosmetic | Static Equivalent | Animated Price | Premium Over Static |
|
||||
|---|---|---|---|
|
||||
| Holographic Frame | Gold Frame (800₼) | 2,000₼ | +150% |
|
||||
| Rarity Glow (Subtle) | — (no static equiv) | 1,200₼ | — |
|
||||
| Rarity Glow (Strong) | Rarity Glow Subtle (1,200₼) | 1,800₼ | +50% |
|
||||
|
||||
The higher price point makes animated cards a genuine flex — visible rarity in gauntlet brackets
|
||||
and trading negotiations.
|
||||
102
paper-dynasty/card-evolution-prd/09-integrations.md
Normal file
102
paper-dynasty/card-evolution-prd/09-integrations.md
Normal file
@ -0,0 +1,102 @@
|
||||
# 9. System Integrations
|
||||
|
||||
[< Back to Index](README.md) | [Next: Economy >](10-economy.md)
|
||||
|
||||
---
|
||||
|
||||
## 9.1 Crafting System (#49) — Future Integration
|
||||
|
||||
The crafting system (Gitea #49) is out of scope for this PRD and has not been fully designed.
|
||||
When crafting ships, the evolution system will need to define interaction rules (e.g., what
|
||||
happens to evolution state when a card is used as a crafting ingredient, whether evolution tier
|
||||
affects crafting output). Those rules will be specified in the crafting PRD.
|
||||
|
||||
---
|
||||
|
||||
## 9.2 Campaign Mode
|
||||
|
||||
Evolution milestones of the `complete_campaign_stage` type are met when the team completes a
|
||||
campaign stage. The bot already records campaign stage completions; the evolution subsystem hooks
|
||||
into the post-game callback.
|
||||
|
||||
Evolved cards used in campaign games contribute normally to game mechanics. No special
|
||||
campaign-mode rules for evolved cards.
|
||||
|
||||
## 9.3 Gauntlet Mode
|
||||
|
||||
Gauntlet is the highest-stakes game mode. Two milestone types reference it directly:
|
||||
`complete_gauntlet` (finish any gauntlet) and `win_gauntlet` (place first). These are evaluated
|
||||
via the `gauntletreward` table's post-event callback, which triggers milestone evaluation for all
|
||||
active teams.
|
||||
|
||||
Fully evolved cards used in gauntlet display their evolved name in the gauntlet bracket display,
|
||||
providing social prestige incentive for completing evolutions before entering gauntlet events.
|
||||
|
||||
## 9.4 Unlimited / Ranked PvP
|
||||
|
||||
Evolved cards are legal in all game modes. Their higher ratings reflect the investment of gameplay
|
||||
time, not additional spending. This positions evolved cards as earned rather than bought power,
|
||||
which is important for PvP mode balance.
|
||||
|
||||
There is no separate evolved card bracket or ladder. Evolved cards compete in the same pool.
|
||||
|
||||
## 9.5 Scouting System
|
||||
|
||||
The scouting reports (`pd-cards scouting all`) aggregate stats at the `player_id` level across
|
||||
all cardsets. Evolution creates `variant`-specific rating rows, which the scouting report
|
||||
generator should ignore (it reads `variant = 0` base rows). No changes needed to scouting scripts.
|
||||
|
||||
A future enhancement could flag evolved cards in scouting output with a visual marker.
|
||||
|
||||
## 9.6 Trading (Not Yet Implemented — Roadmap)
|
||||
|
||||
Trading is not yet implemented but is on the roadmap. When it ships, the following rules apply:
|
||||
|
||||
**Evolution resets on trade.** The traded card's `card.variant` is set to `0`, and it is
|
||||
disassociated from the selling team's `evolution_card_state` record. If the new team already has
|
||||
an `evolution_card_state` for this `player_id`, the traded card inherits that state (and its
|
||||
variant). If the new team has no prior state for this player, a fresh `evolution_card_state` is
|
||||
created with `current_tier = 0` and `progress_since` set to the trade date. The new team starts
|
||||
fresh — only stats accumulated in games played by the *new* team count toward milestones.
|
||||
|
||||
**The selling team's state is unaffected.** If Oakland still has other copies of the same
|
||||
player_id, their `evolution_card_state` remains intact. The trade only affects the specific
|
||||
card instance being transferred.
|
||||
|
||||
Example: If Mark McGwire evolves to T2 on the Oakland Athletics and that card gets traded to the
|
||||
St. Louis Cardinals, the Cardinals' McGwire card starts at T0 (assuming they had no prior McGwire
|
||||
cards). Only stats from Cardinals games count toward the new evolution progress.
|
||||
|
||||
**The card's variant resets.** `card.variant` is set to `0` on the traded card instance, meaning
|
||||
it falls back to `player.image` and base ratings. The old variant's
|
||||
`battingcard`/`pitchingcard` rows and S3 images are NOT deleted — they may still be shared by
|
||||
other cards with the same hash.
|
||||
|
||||
**The evolved card name resets.** Since the card is at T0 on the new team, no evolved name is
|
||||
displayed.
|
||||
|
||||
**Purchased cosmetics do not transfer.** Cosmetics are attached to the selling team's
|
||||
`evolution_card_state`. The new team does not inherit the previous owner's premium visuals.
|
||||
|
||||
**Design rationale:** This creates a loyalty incentive — the longer you keep a card, the more
|
||||
evolved it becomes. Trading away a fully evolved card is a real sacrifice, creating meaningful
|
||||
tension when trading eventually ships.
|
||||
|
||||
## 9.7 Pack Opening and paperdex
|
||||
|
||||
When cards are acquired (pack opening, reward, etc.), the system resolves evolution state using
|
||||
a **batched eager** approach so that card flair is visible immediately during the opening:
|
||||
|
||||
1. Collect all `player_id` values from the pack's cards
|
||||
2. Single batch query: `SELECT player_id, current_tier, variant FROM evolution_card_state WHERE team_id = ? AND player_id IN (?...)`
|
||||
3. For cards with an existing state: set `card.variant` to match (inherits tier, cosmetics, flair)
|
||||
4. For cards without a state: batch `INSERT` new `evolution_card_state` rows at `current_tier = 0`, `progress_since = card.created_at`
|
||||
5. Batch `UPDATE` card variants for all cards that inherited existing state
|
||||
|
||||
This reduces evolution resolution to ~3 queries total regardless of pack size, avoiding per-card
|
||||
round trips. The eager approach ensures evolved card visuals (and any future random cosmetic
|
||||
effects like holo pulls) display immediately during the pack opening experience rather than
|
||||
loading lazily on first view.
|
||||
|
||||
paperdex tracks ownership booleans at the team+player level and does not need modification for
|
||||
evolution.
|
||||
57
paper-dynasty/card-evolution-prd/10-economy.md
Normal file
57
paper-dynasty/card-evolution-prd/10-economy.md
Normal file
@ -0,0 +1,57 @@
|
||||
# 10. Economy Design and Currency Sinks
|
||||
|
||||
[< Back to Index](README.md) | [Next: Safeguards >](11-safeguards.md)
|
||||
|
||||
---
|
||||
|
||||
## 10.1 Design Principle: Free Progression, Paid Expression
|
||||
|
||||
Evolution as a stat progression system is entirely free. No currency is required to evolve a card
|
||||
from T0 to T4. The sole currency sink in this system is:
|
||||
|
||||
1. **Premium cosmetics** — Visual upgrades that signal investment and create prestige
|
||||
|
||||
Cosmetics are not required to compete. A player who never spends currency still gets the full
|
||||
rating boost from evolution. Paying currency gets you a better-looking card.
|
||||
|
||||
Boost direction is determined automatically by the card's detected profile (see
|
||||
[05-rating-boosts.md](05-rating-boosts.md)) and cannot be overridden. This keeps the system
|
||||
simple and avoids a niche min-max feature that adds UI, economy, and state tracking complexity
|
||||
for minimal player engagement.
|
||||
|
||||
## 10.2 Cosmetics Pricing
|
||||
|
||||
| Cosmetic | Cost | Format |
|
||||
|---|---|---|
|
||||
| Gold Frame | 800₼ | Static PNG |
|
||||
| Diamond Frame | 1,000₼ | Static PNG |
|
||||
| Team Color Frame | 600₼ | Static PNG |
|
||||
| Holographic Frame | 2,000₼ | Animated APNG |
|
||||
| Dark Mode Theme | 800₼ | Static PNG |
|
||||
| Midnight Theme | 1,000₼ | Static PNG |
|
||||
| Prestige Badge | 600₼ | Static PNG |
|
||||
| Rarity Glow (Subtle) | 1,200₼ | Animated APNG |
|
||||
| Rarity Glow (Strong) | 1,800₼ | Animated APNG |
|
||||
| Full Art Reveal | 2,000₼ | Static PNG |
|
||||
|
||||
A player who fully decks out a single card (all cosmetics) spends approximately 10,200₼. This
|
||||
is the maximum per-card investment and represents a significant commitment to a single card —
|
||||
intentionally aspirational.
|
||||
|
||||
Animated cosmetics carry a premium over static equivalents (see [08-display.md § 8.6.9](08-display.md#869-animated-card-cosmetics-pricing)).
|
||||
|
||||
## 10.3 Economy Projections
|
||||
|
||||
At steady state with 20 active players:
|
||||
- Assume 20% of active players purchase at least one cosmetic per month: ~4 players x avg 1,000₼ = 4,000₼/month from cosmetics
|
||||
- Assume 10% of active players purchase an animated cosmetic: ~2 players x avg 1,700₼ = 3,400₼/month from animated
|
||||
- Total estimated monthly currency sink: ~7,400₼
|
||||
|
||||
This is a meaningful sink driven by voluntary cosmetic purchases that players make by choice,
|
||||
not tollbooths they must pass through to play.
|
||||
|
||||
## 10.4 No Paywall
|
||||
|
||||
Evolution is funded entirely by earned currency. There is no mechanic that requires purchasing
|
||||
premium currency or special items to complete milestones or receive rating boosts. Challenge
|
||||
types do not include "spend N manticoins" as a milestone.
|
||||
53
paper-dynasty/card-evolution-prd/11-safeguards.md
Normal file
53
paper-dynasty/card-evolution-prd/11-safeguards.md
Normal file
@ -0,0 +1,53 @@
|
||||
# 11. Anti-Abuse Safeguards
|
||||
|
||||
[< Back to Index](README.md) | [Next: API Endpoints >](12-api.md)
|
||||
|
||||
---
|
||||
|
||||
## 11.1 Milestone Threshold Tuning
|
||||
|
||||
The primary anti-abuse lever is milestone threshold configuration. Thresholds defined in
|
||||
`evolution_milestone.threshold` are tunable by admin without code changes. If data shows
|
||||
milestones being completed too quickly (e.g., full T4 in under a week), admin raises thresholds.
|
||||
If players stall at a specific milestone, admin can lower the threshold. Already-credited
|
||||
completions are not retroactively revoked.
|
||||
|
||||
## 11.2 Game Mode Diversity Requirements
|
||||
|
||||
"Win games" milestones do not specify a mode and can be completed in Exhibition (vs AI). This is
|
||||
intentional — exhibition games are still real games with real D20 outcomes. Gauntlet and campaign
|
||||
milestones specifically cannot be completed in exhibition, creating incentive to engage with
|
||||
competitive and narrative modes.
|
||||
|
||||
If exploitation is observed (e.g., players spinning up exhibition games specifically to complete
|
||||
milestones without meaningful play), a safeguard can restrict "win games" milestones to campaign,
|
||||
gauntlet, or unlimited modes via the `game_mode` field on `evolution_milestone`.
|
||||
|
||||
## 11.3 Evaluation Deduplication
|
||||
|
||||
The post-game evaluator must be idempotent. If the game pipeline retries a post-game callback,
|
||||
progress counts should not double. The evaluator recalculates current counts by re-querying the
|
||||
source tables (not by incrementing cached values) and compares against the stored `current_count`.
|
||||
Progress is updated only when the newly computed count exceeds the stored count.
|
||||
|
||||
## 11.4 Rarity Cap Enforcement
|
||||
|
||||
A fully evolved card receiving a rarity upgrade cannot exceed HoF (the maximum rarity). If a Sta
|
||||
card completes T4, it becomes All-Star. If a card is already HoF, it receives the T4 boost deltas
|
||||
but no rarity change. This is enforced at application layer by checking `player.rarity_id`
|
||||
against the HoF ceiling before incrementing.
|
||||
|
||||
**Rating cap enforcement:** Individual stat columns have hard caps (e.g., hold rating at -5).
|
||||
The `apply_evolution_boosts()` function truncates any delta that would exceed a cap and
|
||||
discards the excess (no redistribution — see [05-rating-boosts.md](05-rating-boosts.md)).
|
||||
This is an intentional soft penalty for cards already near their ceiling.
|
||||
|
||||
## 11.5 Evaluation Performance
|
||||
|
||||
Milestone progress is resolved via `player_season_stats` (a materialized view of aggregate
|
||||
stats), not raw `stratplay` table scans. Each post-game evaluation is a simple `SELECT` keyed
|
||||
on `(team_id, player_id)` — lightweight and independent per team. Concurrent games across
|
||||
different teams do not contend since evolution state is keyed by `(player_id, team_id)`.
|
||||
|
||||
The post-game callback should only evaluate cards that participated in the just-completed game,
|
||||
not the entire roster.
|
||||
99
paper-dynasty/card-evolution-prd/12-api.md
Normal file
99
paper-dynasty/card-evolution-prd/12-api.md
Normal file
@ -0,0 +1,99 @@
|
||||
# 12. API Endpoints
|
||||
|
||||
[< Back to Index](README.md) | [Next: Implementation >](13-implementation.md)
|
||||
|
||||
---
|
||||
|
||||
All endpoints are under `/v2/` and require bearer token authentication.
|
||||
|
||||
## 12.1 Evolution Track Endpoints
|
||||
|
||||
```
|
||||
GET /v2/evolution/tracks
|
||||
Query params: card_type, track_family
|
||||
Returns: list of evolution_track records with milestones nested
|
||||
|
||||
GET /v2/evolution/tracks/{track_id}
|
||||
Returns: single track with all milestones
|
||||
|
||||
GET /v2/evolution/tracks/{track_id}/milestones
|
||||
Returns: all milestones for track, grouped by tier
|
||||
```
|
||||
|
||||
## 12.2 Card State Endpoints
|
||||
|
||||
> **Note:** Endpoints accept `card_id` for convenience since the bot always has it available.
|
||||
> The server resolves `card_id` to `(player_id, team_id)` internally via a single join — no
|
||||
> extra round trip. Evolution state is keyed by `(player_id, team_id)` in the database.
|
||||
|
||||
```
|
||||
GET /v2/teams/{team_id}/evolutions
|
||||
Returns: all evolution_card_state records for team, with nested progress
|
||||
|
||||
GET /v2/evolution/cards/{card_id}
|
||||
Returns: evolution_card_state for a specific card, with nested progress
|
||||
|
||||
POST /v2/evolution/cards/{card_id}/evaluate
|
||||
Action: Force re-evaluation of this card's evolution state from player_season_stats
|
||||
Use cases: admin correction, cache invalidation after data fix, on-demand status refresh
|
||||
Returns: updated evolution_card_state with new current_tier and progress counts
|
||||
```
|
||||
|
||||
## 12.3 Cosmetics Endpoints
|
||||
|
||||
```
|
||||
GET /v2/evolution/cosmetics
|
||||
Returns: catalog of available cosmetic types with descriptions and costs
|
||||
|
||||
GET /v2/evolution/cards/{card_id}/cosmetics
|
||||
Returns: all cosmetics purchased for this card by the requesting team
|
||||
|
||||
POST /v2/evolution/cards/{card_id}/cosmetics
|
||||
Body: { cosmetic_type, cosmetic_key }
|
||||
Validates: card ownership, sufficient wallet balance, cosmetic not already active
|
||||
Action: Deducts currency, creates evolution_cosmetic record, triggers variant render
|
||||
(static PNG or animated APNG depending on cosmetic type)
|
||||
Returns: created cosmetic record + new image_url
|
||||
|
||||
DELETE /v2/evolution/cards/{card_id}/cosmetics/{cosmetic_id}
|
||||
Action: Deactivates cosmetic (no refund). Cosmetic record preserved for audit.
|
||||
Returns: updated cosmetic record
|
||||
```
|
||||
|
||||
## 12.4 Modified Card Endpoints
|
||||
|
||||
```
|
||||
GET /v2/players/{player_id}/battingcard
|
||||
New optional query param: variant (integer)
|
||||
If variant provided: returns ratings for that variant (used by game engine for evolved cards)
|
||||
If not provided: returns variant=0 (base card) as before
|
||||
|
||||
GET /v2/players/{player_id}/pitchingcard
|
||||
Same modification as battingcard
|
||||
|
||||
GET /v2/players/{player_id}/battingcard?variant={hash}&render=true&cosmetics=frame_gold,bg_dark
|
||||
Triggers Playwright render with cosmetic CSS applied, uploads to S3, stores URL on
|
||||
battingcard.image_url for that variant. If any cosmetic is animated, renders APNG instead of PNG.
|
||||
Used by the evolution pipeline, not by the bot at display time.
|
||||
Returns: the rendered image (PNG or APNG).
|
||||
```
|
||||
|
||||
## 12.5 Admin Endpoints
|
||||
|
||||
```
|
||||
POST /v2/admin/evolution/tracks
|
||||
Body: full evolution_track definition with milestones
|
||||
Creates new track (admin only)
|
||||
|
||||
PATCH /v2/admin/evolution/milestones/{milestone_id}
|
||||
Body: { threshold, description }
|
||||
Updates threshold or description (admin only, does not retroactively revoke progress)
|
||||
|
||||
POST /v2/admin/evolution/cards/{card_id}/apply-boost
|
||||
Action: Manually trigger tier boost application (for recovery from failed boost events)
|
||||
Admin only
|
||||
|
||||
POST /v2/admin/evolution/cards/{card_id}/initialize
|
||||
Action: Create evolution_card_state for a card that was acquired before the system launched
|
||||
Admin only; used for backfill of existing card inventory
|
||||
```
|
||||
266
paper-dynasty/card-evolution-prd/13-implementation.md
Normal file
266
paper-dynasty/card-evolution-prd/13-implementation.md
Normal file
@ -0,0 +1,266 @@
|
||||
# 13. Implementation Phases and Checklists
|
||||
|
||||
[< Back to Index](README.md) | [Next: Risks and Open Questions >](14-risks.md)
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Render Pipeline Optimization (Week 0, Pre-Requisite)
|
||||
|
||||
Focus: Persistent browser, self-hosted fonts, concurrent upload pipeline. This phase is
|
||||
**independent of evolution** and benefits all existing card generation workflows immediately.
|
||||
Should be implemented before any evolution work begins.
|
||||
|
||||
**Deliverables:**
|
||||
- Persistent Chromium browser instance in API server (replaces per-request browser launch)
|
||||
- FastAPI lifespan hooks for browser startup/shutdown with auto-reconnect
|
||||
- Self-hosted fonts: download Source Sans 3 + Open Sans WOFF2 files, base64-embed in template
|
||||
- Remove Google Fonts CDN `@import` from `style.html`
|
||||
- Concurrent upload pipeline in `pd_cards/core/upload.py` using `asyncio.Semaphore(8)` + `asyncio.gather`
|
||||
- Increase `fetch_card_image` timeout from 6s to 10s to account for render queue contention
|
||||
- Benchmark: measure per-card render time before/after, total run time for full cardset
|
||||
|
||||
**Implementation details:** See [02-architecture.md § Card Render Pipeline Optimization](02-architecture.md#card-render-pipeline-optimization)
|
||||
|
||||
**Success Criteria:** Per-card render time drops from ~3s to ~0.6-1.0s. Full 800-card upload
|
||||
run completes in under 5 minutes (down from ~40 minutes). No external font CDN dependency.
|
||||
|
||||
**Files changed:**
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `database/app/routers_v2/players.py` | Persistent browser, page-per-request pattern |
|
||||
| `database/app/main.py` | FastAPI lifespan hooks |
|
||||
| `database/storage/templates/style.html` | `@font-face` with base64 WOFF2 |
|
||||
| `database/storage/fonts/` | Font files (new directory) |
|
||||
| `pd_cards/core/upload.py` | `asyncio.gather` with semaphore |
|
||||
| `check_cards_and_upload.py` | Same concurrency pattern (legacy script) |
|
||||
|
||||
---
|
||||
|
||||
## 13.1 Phase 1 — Foundation (Weeks 1-3)
|
||||
|
||||
Focus: Database schema, lazy evaluation engine, tier badge display, basic Discord commands.
|
||||
|
||||
**Deliverables:**
|
||||
- `player_season_stats` materialized aggregate table created (reference Major Domo implementation)
|
||||
- Incremental update logic in post-game callback for `player_season_stats`
|
||||
- Backfill script to populate `player_season_stats` from existing `stratplay`/`decision` data
|
||||
- All evolution database tables created with migrations
|
||||
- `evolution_card_state` initialization on card acquisition (pack opening hook): create if no `(player_id, team_id)` state exists, otherwise inherit existing state
|
||||
- API endpoints: track catalog, card state GET, evaluate POST
|
||||
- Post-game callback integration: evaluate participating cards after each game
|
||||
- Milestone evaluation engine for all stratplay/decision/stratgame milestone types
|
||||
- Bot command: `!evo status` (roster-wide view and per-card view)
|
||||
- Basic Discord embed: tier badge on card name
|
||||
- No rating boosts yet — tier advances are tracked but boosts deferred to Phase 2
|
||||
- Three universal evolution tracks seeded in database (Batter, Starting Pitcher, Relief Pitcher)
|
||||
- Backfill job: initialize `evolution_card_state` for all unique `(player_id, team_id)` pairs with `progress_since = earliest card.created_at`
|
||||
|
||||
**Success Criteria:** All cards have evolution state. Players can see tier progress. Milestones
|
||||
complete automatically after games.
|
||||
|
||||
## 13.2 Phase 2 — Rating Boosts (Weeks 4-6)
|
||||
|
||||
Focus: Apply rating boosts on tier completion, variant card API.
|
||||
|
||||
**Deliverables:**
|
||||
- `apply_evolution_boosts()` function in card-creation repo
|
||||
- Batter and pitcher player profile detection from existing ratings
|
||||
- Tier boost application on tier completion: writes new variant rows to battingcardratings/pitchingcardratings
|
||||
- `evolution_tier_boost` audit table populated
|
||||
- Updated card API: `card_id` param for variant resolution
|
||||
- Discord notification on milestone and tier completion (with boost summary)
|
||||
- T4 rarity upgrade (universal, capped at HoF)
|
||||
- Evolved card display name (`[Team]'s Evolved [Player]`)
|
||||
|
||||
**Success Criteria:** Tier completion triggers rating boost. Evolved cards show higher ratings
|
||||
in card embed.
|
||||
|
||||
## 13.3 Phase 3 — Static Cosmetics (Weeks 7-10)
|
||||
|
||||
Focus: Premium static cosmetics, visual differentiation, economy tuning.
|
||||
|
||||
**Deliverables:**
|
||||
- Cosmetics catalog seeded in database (static cosmetics only: frames, themes, badges)
|
||||
- Cosmetics purchase and display endpoints
|
||||
- Cosmetic CSS injection in card HTML template (frame overlays, background colors, badges)
|
||||
- Variant render pipeline: Playwright renders cards with cosmetic CSS, uploads to S3
|
||||
- `battingcard.image_url` column populated for variant rows
|
||||
- Bot image resolution updated: variant → `battingcard.image_url` fallback to `player.image`
|
||||
- Verify all three tracks work correctly across all rarity levels (Rep through HoF)
|
||||
- Discord embed accent colors per tier (T2 gold, T3 purple, T4 teal)
|
||||
- Analytics query for admin: evolution completion rates, milestone drop-off, cosmetic revenue
|
||||
- Economy review: check cosmetics pricing against actual currency circulation data
|
||||
|
||||
**Success Criteria:** Static cosmetics purchasable and visually distinct. Full T0-T4 evolution
|
||||
journey works end to end including cosmetics.
|
||||
|
||||
## 13.4 Phase 4 — Animated Cosmetics (Weeks 11-13)
|
||||
|
||||
Focus: APNG render pipeline, animated cosmetic effects.
|
||||
|
||||
**Prerequisite:** Phase 0 (persistent browser) must be complete. The APNG pipeline depends on
|
||||
the persistent browser for efficient multi-frame capture.
|
||||
|
||||
**Deliverables:**
|
||||
- Deterministic multi-frame capture using `--anim-progress` CSS custom property
|
||||
- `capture_animated_frames()` function using persistent browser page pool
|
||||
- APNG assembly from frame buffers using `apng` Python library (`pip install apng`)
|
||||
- CSS custom property-driven animations in card HTML template (no `@keyframes` for animated cosmetics)
|
||||
- Animated cosmetics added to catalog: Holographic Frame, Rarity Glow (Subtle/Strong), T4 Badge Sparkle
|
||||
- S3 upload with `.apng` extension and `image/apng` content type and `CacheControl: public, max-age=31536000`
|
||||
- File size validation (target <4 MB per animated card)
|
||||
- Bot display verified: APNG auto-plays in Discord embeds (desktop + mobile)
|
||||
- Performance benchmarking: target ~4.5s for 8-frame, ~6s for 12-frame animated variant
|
||||
- pngquant integration for frame compression if file sizes exceed budget
|
||||
|
||||
**Success Criteria:** Animated cosmetics render correctly as APNG, auto-play in Discord embeds,
|
||||
file sizes within budget, render time <8 seconds per animated variant.
|
||||
|
||||
---
|
||||
|
||||
## 13.5 Render Pipeline Optimization Checklist (Phase 0)
|
||||
|
||||
- [ ] Download Source Sans 3 (400, 700) and Open Sans (300, 400, 700) WOFF2 files
|
||||
- [ ] Base64-encode font files and create `@font-face` declarations
|
||||
- [ ] Replace `@import url('fonts.googleapis.com/...')` in `style.html` with local `@font-face` — PR #96 (next-release)
|
||||
- [ ] Verify card renders identically with self-hosted fonts (visual diff)
|
||||
- [x] Create `get_browser()` / `shutdown_browser()` functions in players.py — merged 2026.3.17
|
||||
- [x] Add FastAPI lifespan hooks for browser startup/shutdown — merged 2026.3.17
|
||||
- [x] Replace `async with async_playwright()` block with persistent browser + page-per-request — merged 2026.3.17
|
||||
- [x] Add `is_connected()` check with automatic browser reconnect — implemented via `asyncio.Lock`, merged 2026.3.17
|
||||
- [ ] Test: render 10 cards sequentially, verify no browser leaks (page count stays at 0 between renders)
|
||||
- [ ] Test: concurrent renders (4 simultaneous requests) complete without errors
|
||||
- [ ] Benchmark: measure per-card render time (target: <1.0s, down from ~3.0s) — script in PR #95 (next-release)
|
||||
- [ ] Refactor `pd_cards/core/upload.py` loop to use `asyncio.Semaphore(8)` + `asyncio.gather`
|
||||
- [ ] Add error handling: individual card failures don't abort the batch
|
||||
- [ ] Add progress reporting: log every N completions (not every 20 starts)
|
||||
- [ ] Increase `fetch_card_image` timeout from 6s to 10s
|
||||
- [ ] Benchmark: full 800-card upload run (target: <5 minutes)
|
||||
- [ ] Update `check_cards_and_upload.py` legacy script with same concurrency pattern
|
||||
- [ ] Deploy to dev, run full cardset upload, verify all cards render correctly
|
||||
- [x] Deploy to production — persistent browser deployed 2026.3.17
|
||||
|
||||
## 13.6 Database Checklist
|
||||
|
||||
- [ ] Write Peewee model for `evolution_track` — pending PR #84 (card-evolution)
|
||||
- [ ] Write Peewee model for `evolution_milestone` — pending PR #84 (card-evolution)
|
||||
- [ ] Write Peewee model for `evolution_card_state` — pending PR #84 (card-evolution)
|
||||
- [ ] Write Peewee model for `evolution_progress`
|
||||
- [ ] Write Peewee model for `evolution_tier_boost` — stub schema in PR #84
|
||||
- [ ] Write Peewee model for `evolution_cosmetic` — stub schema in PR #84
|
||||
- [ ] Write migration scripts for all new tables (idempotent) — pending PR #84 (card-evolution)
|
||||
- [x] Write Peewee model for `player_season_stats` — split into `BattingSeasonStats` + `PitchingSeasonStats` in `app/models/season_stats.py`, merged 2026.3.17
|
||||
- [ ] Implement incremental update in post-game callback (delta-based, not full recompute)
|
||||
- [ ] Add `last_game_id` check to prevent double-counting on callback retries
|
||||
- [ ] Write backfill script to populate from existing `stratplay`/`decision` rows
|
||||
- [ ] Verify backfill totals match on-demand `GROUP BY` queries for a sample of players
|
||||
- [ ] Reference Major Domo implementation for patterns and edge cases
|
||||
- [x] Seed three universal evolution tracks (Batter, SP, RP) — seed data + JSON in `app/seed/evolution_tracks.py`, merged 2026.3.17; runnable once migration (PR #84) lands
|
||||
- [ ] Add `image_url` nullable column to `battingcard` and `pitchingcard` — in PR #84 migration
|
||||
- [ ] Add `variant` column to `card` table (integer, default 0) — in PR #84 migration
|
||||
- [ ] Verify `battingcard.variant` UNIQUE constraint behavior with hash-based integers
|
||||
- [ ] Index all FK columns and query-hot columns on new tables — indexes in PR #84 migration
|
||||
- [ ] Ensure `player_season_stats` has appropriate indexes for `(team_id, player_id)` lookups — indexes in PR #84 migration
|
||||
- [ ] Test schema in dev environment before production migration
|
||||
- [ ] Write and test backfill script: create `evolution_card_state` for each unique `(player_id, team_id)` pair, propagate variant to all matching card instances
|
||||
|
||||
## 13.7 Card Creation System Checklist
|
||||
|
||||
- [ ] Implement `apply_evolution_boosts(card_ratings_df, boost_tier, player_profile)` in `creation_helpers.py` or `batters/calcs_batter.py`
|
||||
- [ ] Implement batter player profile detection (power, contact, patient) from existing ratings
|
||||
- [ ] Implement pitcher player profile detection (SP power, SP control, RP) from existing ratings
|
||||
- [ ] Write unit tests for boost distribution: verify column sums remain valid post-boost
|
||||
- [ ] Write unit tests for rating cap enforcement: verify boosts truncate at caps (e.g., hold at -5)
|
||||
- [ ] Write unit tests for cap truncation: verify excess budget is discarded (not redistributed) when a stat hits its cap
|
||||
- [ ] Write unit test for rarity upgrade eligibility check (HoF ceiling)
|
||||
- [ ] Integrate `apply_evolution_boosts()` with API endpoint trigger (or standalone script callable by API service)
|
||||
- [ ] Implement `compute_variant_hash()` function for deterministic variant numbering
|
||||
- [ ] Handle variant field correctly: check for existing variant row before creating new one
|
||||
- [ ] Implement cosmetic CSS injection in card HTML template (frames, backgrounds, badges)
|
||||
- [ ] Implement CSS `--anim-progress` custom property animations for animated cosmetics (holographic, glow pulse, sparkle)
|
||||
- [ ] Implement multi-frame Playwright capture for animated variants
|
||||
- [ ] Implement APNG assembly from frame buffers
|
||||
- [ ] Add file size validation for animated output
|
||||
- [ ] Add CLI command: `pd-cards evolution apply-boost --card-id <id> --tier <n>` for admin use
|
||||
- [ ] Add CLI command: `pd-cards evolution render-variant --player-id <id> --variant <hash>` for manual re-renders
|
||||
|
||||
## 13.8 API Development Checklist
|
||||
|
||||
- [x] Implement track catalog endpoints (GET list, GET single) — `GET /api/v2/evolution/tracks` and `/tracks/{id}` in `app/routers_v2/evolution.py`, merged 2026.3.17; returns 500 until PR #84 (EvolutionTrack model) lands
|
||||
- [ ] Implement track milestones endpoint (GET milestones for track)
|
||||
- [x] Implement season stats CRUD — full REST endpoints in `app/routers_v2/season_stats.py`, merged 2026.3.17
|
||||
- [x] Formula engine — `app/services/formula_engine.py`, batter OPS + pitcher ERA/WHIP/K formulas, merged 2026.3.17
|
||||
- [ ] Implement card state endpoints (GET by team, GET by card)
|
||||
- [ ] Implement POST evaluate endpoint — in PR #98 (card-evolution); `POST /api/v2/evolution/cards/{card_id}/evaluate` with evaluator service
|
||||
- [ ] Implement cosmetics endpoints (GET catalog, GET card cosmetics, POST purchase, DELETE deactivate)
|
||||
- [ ] Modify battingcard/pitchingcard endpoints to accept optional `card_id` param
|
||||
- [ ] Implement variant resolution logic in card endpoints using `evolution_card_state`
|
||||
- [ ] Implement admin endpoints for track/milestone management and manual boost application
|
||||
- [ ] Implement idempotent milestone evaluator (recalculate from source, not increment)
|
||||
- [ ] Render endpoint: detect animated vs static cosmetics, route to correct pipeline
|
||||
- [ ] Write API integration tests for full T0-T4 evolution lifecycle
|
||||
- [ ] Write API integration tests for cosmetics purchase flow (static + animated)
|
||||
- [ ] Document all new endpoints in API reference
|
||||
|
||||
## 13.9 Bot Integration Checklist
|
||||
|
||||
- [ ] Add `!evo status` command with roster-wide formatted embed
|
||||
- [ ] Add `!evo card <card_id>` command for detailed single-card milestone breakdown
|
||||
- [ ] Add `!evo cosmetics <card_id>` command for premium cosmetics browsing
|
||||
- [ ] Integrate post-game evolution evaluator in post-game processing pipeline
|
||||
- [ ] Verify evaluator fires after game resolution in all modes (campaign, gauntlet, exhibition, unlimited)
|
||||
- [ ] Add milestone completion notification to team channel
|
||||
- [ ] Add tier completion notification with rating boost summary
|
||||
- [ ] Add Full Evolution notification with rarity upgrade announcement
|
||||
- [ ] Update card display embed to show tier badge from `evolution_card_state.current_tier`
|
||||
- [ ] Update card display embed to show evolved name for fully evolved cards
|
||||
- [ ] Update card display embed to apply active cosmetic styling
|
||||
- [ ] Verify APNG auto-plays correctly in Discord embeds across desktop and mobile
|
||||
- [ ] Update trade processing: set traded card's variant to 0, check for existing state on new team's `(player_id, team_id)`, create fresh state if none exists
|
||||
|
||||
## 13.10 Testing Checklist
|
||||
|
||||
- [ ] Unit tests: `apply_evolution_boosts()` for all player profiles and tiers
|
||||
- [ ] Unit tests: milestone evaluation logic for all challenge types
|
||||
- [ ] Unit tests: variant resolution in card endpoint
|
||||
- [ ] Unit tests: idempotent evaluator (calling twice yields same result)
|
||||
- [ ] Unit tests: APNG assembly produces valid APNG output
|
||||
- [ ] Integration tests: game -> post-game callback -> milestone completion -> tier boost full flow
|
||||
- [ ] Integration tests: T4 rarity upgrade across all rarity levels (Rep→Res, Sta→All, MVP→HoF, HoF stays HoF)
|
||||
- [ ] Integration tests: evolved card state and name update correctly after trade
|
||||
- [ ] Integration tests: cosmetics remain on selling team's `(player_id, team_id)` state after trade
|
||||
- [ ] Integration tests: animated cosmetic purchase triggers APNG render pipeline
|
||||
- [ ] Load test: milestone query latency against player_season_stats with large dataset
|
||||
- [ ] Regression test: base card ratings unaffected by evolution variant writes
|
||||
- [ ] Regression test: scouting report generation ignores variant rows
|
||||
- [ ] Visual test: animated APNG renders correctly in Discord (desktop + mobile)
|
||||
- [ ] Visual test: APNG graceful degradation (first frame displays if client doesn't support animation)
|
||||
|
||||
## 13.11 Security and Balance Checklist
|
||||
|
||||
- [ ] Verify cosmetic purchase currency deducted atomically (wallet check + deduction in single transaction)
|
||||
- [ ] Verify card ownership validated before cosmetic purchase (card.team_id == requesting team_id)
|
||||
- [ ] Verify cosmetics are non-transferable on trade (team_id check in display layer)
|
||||
- [ ] Review milestone thresholds against expected games-per-week before launch
|
||||
- [ ] Verify rarity cap enforcement (no evolution beyond HoF)
|
||||
- [ ] Verify individual stat column caps enforced during boost application (e.g., hold ≥ -5)
|
||||
- [ ] Verify evolved card stats cannot exceed designed boost budget (4.0 chances/108 total) via any combination of track + boost
|
||||
|
||||
## 13.12 Deployment Checklist
|
||||
|
||||
- [ ] Run schema migration in dev, verify all tables and indexes created correctly — blocked on PR #84
|
||||
- [ ] Seed evolution tracks and milestones in dev — seed script ready, blocked on schema
|
||||
- [ ] Run backfill job on dev copy of card inventory, verify all cards get state records
|
||||
- [ ] Run full Phase 1 test suite in dev environment
|
||||
- [ ] Deploy API changes to dev, smoke test all new endpoints — season stats + formula engine deployed to prod 2026.3.17; track catalog deployed but non-functional (missing model)
|
||||
- [ ] Deploy bot changes to dev instance, run a game to verify post-game evaluator fires
|
||||
- [ ] Demo full T0 -> T1 progression flow in dev
|
||||
- [ ] Cal approves dev demo
|
||||
- [ ] Run schema migration in production during low-traffic window
|
||||
- [ ] Run backfill job in production for existing card inventory
|
||||
- [ ] Deploy API changes to production
|
||||
- [ ] Deploy bot changes to production
|
||||
- [ ] Monitor milestone query latency for first 48 hours
|
||||
- [ ] Monitor tier completion rate and cosmetic purchase rate after first week
|
||||
- [ ] Schedule economy review at Day 30
|
||||
86
paper-dynasty/card-evolution-prd/14-risks.md
Normal file
86
paper-dynasty/card-evolution-prd/14-risks.md
Normal file
@ -0,0 +1,86 @@
|
||||
# 14. Risk Register and Open Questions
|
||||
|
||||
[< Back to Index](README.md)
|
||||
|
||||
---
|
||||
|
||||
## Risk Register
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Milestone queries become slow at scale | Low | Medium | Queries go through `player_season_stats` materialized view keyed on `(team_id, player_id)`, not raw stratplay scans. Ensure appropriate indexes on the materialized view. |
|
||||
| Milestone thresholds too easy, Full Evolution trivial | Medium | Medium | Admin-tunable thresholds; monitor completion rate and adjust; launch conservatively |
|
||||
| Milestone thresholds too hard, players don't notice evolution is happening | Medium | Medium | Lower thresholds; T1 should complete for any card played in ~2 weeks of casual play |
|
||||
| Retroactive backfill gives some cards immediate T4 on launch | Low | Medium | Evaluate whether progress_since should be launch date rather than card.created_at; see Open Question 2 |
|
||||
| Cosmetic pricing too low, not a meaningful sink | Low | Low | Monitor purchase rates; adjust cosmetic catalog pricing post-launch |
|
||||
| Cosmetic pricing too high, nobody buys them | Medium | Medium | Start at lower price points; increase for new cosmetics based on demand data |
|
||||
| Rating boosts break 108-sum column invariant | Low | High | Unit test suite for `apply_evolution_boosts()`; validation on every boost application |
|
||||
| Variant rows conflict with existing `UNIQUE(player_id, variant)` constraint | Low | High | Validate constraint behavior in dev before migration; existing data has variant=0 only |
|
||||
| API variant resolution adds latency to card embed | Low | Low | Single additional DB lookup; index on card_state by card_id |
|
||||
| Trade cosmetic-stripping creates player frustration | Low | Low | Clear in-game warning before trade completes; cosmetics are team purchases, not card upgrades |
|
||||
| Player collusion to farm milestones via exhibition | Low | Low | Future safeguard: mode restrictions via game_mode field; low priority since exhibition outcomes are real D20 games |
|
||||
| APNG file sizes too large for mobile Discord clients | Medium | Medium | pngquant compression, frame count reduction, file size budget enforcement (see [08-display.md § 8.6.8](08-display.md#868-file-size-budget)) |
|
||||
| Animated render pipeline too slow for batch operations | Low | Low | Animated variants are rare (premium purchases only); batch re-renders can run in background |
|
||||
| APNG not supported on older Discord mobile clients | Low | Low | Graceful degradation: first frame renders as static PNG; bot can check URL extension to warn user |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Progress start date — all-time vs acquisition date vs launch date:** Currently specified as
|
||||
`card.created_at`. Three options exist: (a) all-time for that player_id on that team (most
|
||||
rewarding, may cause immediate tier completions on launch), (b) card acquisition date (natural,
|
||||
but retroactive depth varies by card age), (c) system launch date (clean cold start, no
|
||||
retroactive credit). Which is preferred for the launch experience?
|
||||
|
||||
2. **Immediate Full Evolution on backfill:** Some veteran players may have cards where enough
|
||||
historical games exist to hit T4 immediately on launch day. Is this a good thing (reward
|
||||
loyal players) or a bad thing (deflates the progression experience)? One mitigation: cap
|
||||
retroactive credit at T2 on launch, let players play their way to T3-T4.
|
||||
|
||||
3. ~~**Boost allocation player choice (Phase 4):**~~ **RESOLVED — CUT.** Boost direction override
|
||||
was removed entirely. Auto-detected profile determines distribution; no player customization.
|
||||
Cosmetics are the sole currency sink.
|
||||
|
||||
4. **Maximum evolved card count visibility:** With all cards evolving simultaneously, the
|
||||
`!evo status` embed could become overwhelming for large rosters. Should it default to showing
|
||||
only cards close to a tier milestone (within 20% of threshold)? Or only T3+ cards?
|
||||
|
||||
5. **Season-specific evolution tracks:** Time-limited evolution tracks tied to current season
|
||||
events are a strong engagement feature. Should these be a separate PRD, or designed into
|
||||
the track schema from the start?
|
||||
|
||||
6. ~~**HoF cards on Legend track:**~~ **RESOLVED in v4.0.** Tracks are now universal (not
|
||||
rarity-gated). HoF cards follow the same track as all other cards, receive the same flat rating
|
||||
boosts, and get the full visual rework at T4. They do not receive a rarity upgrade (already
|
||||
at ceiling). The flat delta naturally provides diminishing returns for elite cards.
|
||||
|
||||
7. **Quality start definition:** The D20 system does not natively track innings pitched per
|
||||
appearance. "6+ innings, 3 or fewer ER" requires a proxy calculation based on the outcome
|
||||
simulation. How is this currently exposed from the game engine, if at all? If not available,
|
||||
this milestone type may need to be replaced with a simpler proxy (e.g., "pitcher records a win
|
||||
with fewer than N walks allowed").
|
||||
|
||||
8. **Cosmetic catalog scope:** The cosmetics listed in [08-display.md](08-display.md) are
|
||||
illustrative. Should cosmetics be designed in detail before Phase 3 begins, or is it
|
||||
acceptable to ship with one or two cosmetics and expand the catalog over time?
|
||||
|
||||
9. **Cosmetic CSS implementation detail:** The card HTML template currently uses hardcoded inline
|
||||
styles for backgrounds (`#ACE6FF`, `#EAA49C`) and gradients (`blue-gradient`, `red-gradient`).
|
||||
Cosmetics need to override these. Options: (a) pass cosmetic flags as Jinja2 template variables
|
||||
and use conditionals in the template, (b) inject a cosmetic CSS override block after `style.html`,
|
||||
(c) build a cosmetic theme system with named CSS classes. Which approach best balances flexibility
|
||||
with maintainability?
|
||||
|
||||
10. ~~**Variant hash collision handling:**~~ **RESOLVED.** The `UNIQUE(player_id, variant)`
|
||||
constraint catches collisions at the DB level. No application-level check needed. With 32-bit
|
||||
hashes (~4 billion values) and realistic catalog sizes, collision probability is negligible.
|
||||
|
||||
11. ~~**APNG render determinism:**~~ **RESOLVED in v4.0.** The render pipeline uses the
|
||||
`--anim-progress` CSS custom property for deterministic per-frame control. CSS `@keyframes`
|
||||
are not used for animated cosmetics. See [08-display.md § 8.6.4](08-display.md#864-deterministic-frame-control).
|
||||
|
||||
---
|
||||
|
||||
*End of PRD: Card Evolution System v4.1*
|
||||
*Status: Full review with Cal complete (2026-03-11). Next step: Confirm Phase 1 scope, create Gitea milestone and issue breakdown.*
|
||||
26
paper-dynasty/card-evolution-prd/PRD_CARD_EVOLUTION.md
Normal file
26
paper-dynasty/card-evolution-prd/PRD_CARD_EVOLUTION.md
Normal file
@ -0,0 +1,26 @@
|
||||
# PRD: Card Evolution System
|
||||
|
||||
**This document has been refactored into chapters. See [prd-evolution/README.md](prd-evolution/README.md).**
|
||||
|
||||
The monolithic PRD was split into 14 chapter files at v4.0 for maintainability.
|
||||
All content is preserved — nothing was removed, only reorganized.
|
||||
|
||||
## Quick Links
|
||||
|
||||
| Chapter | File |
|
||||
|---------|------|
|
||||
| Index + Executive Summary | [README.md](prd-evolution/README.md) |
|
||||
| Business Objectives | [01-objectives.md](prd-evolution/01-objectives.md) |
|
||||
| Architecture | [02-architecture.md](prd-evolution/02-architecture.md) |
|
||||
| Evolution Tracks | [03-tracks.md](prd-evolution/03-tracks.md) |
|
||||
| Milestones | [04-milestones.md](prd-evolution/04-milestones.md) |
|
||||
| Rating Boosts | [05-rating-boosts.md](prd-evolution/05-rating-boosts.md) |
|
||||
| Database Schema | [06-database.md](prd-evolution/06-database.md) |
|
||||
| Variant System | [07-variant-system.md](prd-evolution/07-variant-system.md) |
|
||||
| Display, Visuals, Animated Cards | [08-display.md](prd-evolution/08-display.md) |
|
||||
| Integrations | [09-integrations.md](prd-evolution/09-integrations.md) |
|
||||
| Economy | [10-economy.md](prd-evolution/10-economy.md) |
|
||||
| Safeguards | [11-safeguards.md](prd-evolution/11-safeguards.md) |
|
||||
| API Endpoints | [12-api.md](prd-evolution/12-api.md) |
|
||||
| Implementation + Checklists | [13-implementation.md](prd-evolution/13-implementation.md) |
|
||||
| Risks + Open Questions | [14-risks.md](prd-evolution/14-risks.md) |
|
||||
116
paper-dynasty/card-evolution-prd/README.md
Normal file
116
paper-dynasty/card-evolution-prd/README.md
Normal file
@ -0,0 +1,116 @@
|
||||
# PRD: Card Evolution System
|
||||
|
||||
**Document Status:** Draft v4.0 — Phase 0 complete, Phase 1 in progress
|
||||
**Author:** Atlas (Principal Software Architect)
|
||||
**Date:** 2026-03-10 (last updated 2026-03-17)
|
||||
**Issue Reference:** Gitea #46 (Evolution), #49 (Crafting)
|
||||
**Stakeholder:** Cal
|
||||
|
||||
## Implementation Status
|
||||
|
||||
| Phase | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Phase 0 — Render Pipeline | ✅ Mostly complete | Browser persistence + lifespan hooks in main (2026.3.17). Self-hosted fonts (PR #96) and benchmark script (PR #95) still in next-release. |
|
||||
| Phase 1 — Foundation | 🔄 In progress | Season stats models, formula engine, track catalog API, seed data all in main. SQL migration + Peewee models (PR #84) and evaluate endpoint (PR #98) pending on `card-evolution` branch. |
|
||||
| Phase 2 — Rating Boosts | ⬜ Not started | |
|
||||
| Phase 3 — Static Cosmetics | ⬜ Not started | |
|
||||
| Phase 4 — Animated Cosmetics | ⬜ Not started | |
|
||||
|
||||
**Active branch:** `card-evolution` (paper-dynasty-database repo)
|
||||
**Open PRs targeting card-evolution:** #84 (SQL migration + models), #98 (evaluate endpoint)
|
||||
|
||||
---
|
||||
|
||||
## Revision History
|
||||
|
||||
> **v4.0 (2026-03-11):** Added animated card support (APNG) and render pipeline optimization.
|
||||
> Persistent browser instance replaces per-request Chromium launch (~3x per-card speedup).
|
||||
> Concurrent upload pipeline with `asyncio.gather` (~10-20x total run speedup). Self-hosted
|
||||
> fonts eliminate CDN dependency. APNG pipeline uses deterministic `--anim-progress` CSS custom
|
||||
> property for reproducible frame capture. New Phase 0 (render optimization) added as pre-requisite.
|
||||
> See [02-architecture.md](02-architecture.md#card-render-pipeline-optimization) and
|
||||
> [08-display.md § 8.6](08-display.md#86-animated-card-pipeline).
|
||||
>
|
||||
> **v3.0 (2026-03-10):** Variant system redesigned to use deterministic hash-based variant
|
||||
> numbering. `card.variant` (nullable) added to card table. `image_url` added to
|
||||
> `battingcard`/`pitchingcard` for variant-specific rendered images with cosmetics baked in.
|
||||
> Trading resets evolution (team-scoped progress). Card images rendered once per variant creation.
|
||||
>
|
||||
> **v2.0 (2026-03-10):** Major design pivot from opt-in enrollment model to passive, automatic,
|
||||
> free evolution. Every card always evolves; currency sinks are cosmetics
|
||||
> choice, not activation fees. Slot limits and abandonment mechanics removed.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
| Chapter | Section | File |
|
||||
|---------|---------|------|
|
||||
| — | Executive Summary | [README.md](#executive-summary) (below) |
|
||||
| 1 | Business Objectives and Success Metrics | [01-objectives.md](01-objectives.md) |
|
||||
| 2 | System Architecture + Render Pipeline Optimization | [02-architecture.md](02-architecture.md) |
|
||||
| 3 | Evolution Track Design | [03-tracks.md](03-tracks.md) |
|
||||
| 4 | Milestone Challenge System | [04-milestones.md](04-milestones.md) |
|
||||
| 5 | Rating Boost Mechanics | [05-rating-boosts.md](05-rating-boosts.md) |
|
||||
| 6 | Database Schema | [06-database.md](06-database.md) |
|
||||
| 7 | Card Model Changes and Variant System | [07-variant-system.md](07-variant-system.md) |
|
||||
| 8 | Discord Display, Visual Identity, and Animated Cards | [08-display.md](08-display.md) |
|
||||
| 9 | System Integrations (Crafting, Existing Systems) | [09-integrations.md](09-integrations.md) |
|
||||
| 10 | Economy Design and Currency Sinks | [10-economy.md](10-economy.md) |
|
||||
| 11 | Anti-Abuse Safeguards | [11-safeguards.md](11-safeguards.md) |
|
||||
| 12 | API Endpoints | [12-api.md](12-api.md) |
|
||||
| 13 | Implementation Phases and Checklists | [13-implementation.md](13-implementation.md) |
|
||||
| 14 | Risk Register and Open Questions | [14-risks.md](14-risks.md) |
|
||||
|
||||
**Related files:**
|
||||
- [Card Cosmetics Playground](../card-cosmetics-playground.html) — Interactive visual explorer for cosmetic options
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
### Project Overview
|
||||
|
||||
Card Evolution is a passive, automatic progression system that permanently upgrades cards as
|
||||
players use them in games. Every card a team owns is always evolving — there is no opt-in,
|
||||
no activation cost, and no slot limit. Players build attachment to specific cards by simply
|
||||
playing games with them. When enough milestones accumulate, the card tiers up automatically
|
||||
and its ratings improve.
|
||||
|
||||
The system takes direct inspiration from EA FC's Evolutions mechanic and adapts it to Paper
|
||||
Dynasty's D20 probability engine, Discord-native UX, and PostgreSQL/Peewee architecture. The
|
||||
v2.0 pivot strips away the management overhead (slots, activation fees, abandonment) that would
|
||||
have made evolution feel like a second job and instead makes it feel like a natural reward for
|
||||
playing the game.
|
||||
|
||||
### Core Design Philosophy
|
||||
|
||||
- **Passive and automatic**: Evolution happens in the background. Players do not enroll cards or
|
||||
manage slots. They play games; cards evolve.
|
||||
- **Free to evolve**: Rating boosts cost nothing. Currency is spent on customization and cosmetics,
|
||||
not on unlocking basic progression.
|
||||
- **Visible progress**: Players can always check any card's current evolution tier and milestone
|
||||
progress. The system rewards curiosity.
|
||||
- **Premium expression layer**: Players who want to customize their card's visuals (frames,
|
||||
themes, animated effects) spend currency to do so. Flex culture is the monetization hook,
|
||||
not gatekeeping.
|
||||
|
||||
### Scope
|
||||
|
||||
This PRD covers:
|
||||
- Passive evolution track structure and milestone types
|
||||
- Lazy evaluation architecture against existing game data tables
|
||||
- D20-safe rating boost calculations
|
||||
- Premium cosmetic system
|
||||
- Animated card rendering pipeline (APNG) for premium cosmetics
|
||||
- All new database tables and changes to existing tables
|
||||
- Discord embed design for evolved cards
|
||||
- Interplay with the planned Crafting (#49) system
|
||||
- Economy balance (cosmetics-based, not activation-based)
|
||||
- A phased implementation plan
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- The Crafting / Duplicate Merging system itself (covered in its own PRD, #49)
|
||||
- Ranked mode (separate project)
|
||||
- Mobile or web frontend
|
||||
74
paper-dynasty/database-release-2026.3.17.md
Normal file
74
paper-dynasty/database-release-2026.3.17.md
Normal file
@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Paper Dynasty Database Release — 2026.3.17
|
||||
type: reference
|
||||
domain: paper-dynasty
|
||||
tags: [paper-dynasty, deployment, release-notes, database]
|
||||
---
|
||||
|
||||
# Paper Dynasty Database Release — 2026.3.17
|
||||
|
||||
**Date:** 2026-03-17
|
||||
**Branch:** next-release → main (PR #100, 27 commits)
|
||||
**Repo:** cal/paper-dynasty-database
|
||||
**Server:** pddev.manticorum.com:816
|
||||
|
||||
## Release Summary
|
||||
|
||||
Major release adding the evolution system foundation, render pipeline improvements, query performance fixes, and infrastructure hardening. This is the first release cut from the next-release branch for this repo.
|
||||
|
||||
## Evolution System (Card Evolution Phase 1)
|
||||
|
||||
The core backend for the card evolution feature. Cards can now progress through tiers based on accumulated player stats across seasons.
|
||||
|
||||
- **Season stats model** (`app/models/season_stats.py`): `BattingSeasonStats` and `PitchingSeasonStats` Peewee models for per-player, per-team, per-season stat accumulators. Originally a single `PlayerSeasonStats` model, refactored mid-cycle to split batting/pitching and rename `so`→`so_batter`, `k`→`so_pitcher` for clarity.
|
||||
- **Season stats CRUD** (`app/routers_v2/season_stats.py`): Full REST endpoints for reading/writing season stats.
|
||||
- **Track catalog API** (`app/routers_v2/evolution.py`, WP-06): `GET /api/v2/evolution/tracks` and `GET /api/v2/evolution/tracks/{id}` endpoints serving the three universal tracks (Batter, SP, RP) with tier thresholds.
|
||||
- **Formula engine** (`app/services/formula_engine.py`, WP-09): Computes evolution value from career stats. Handles batter OPS-based formula and pitcher ERA/WHIP/K-based formula with split weighting.
|
||||
- **Evolution seed data** (`app/seed/evolution_tracks.py`, WP-03): Seed script and JSON for the three base tracks with t1-t4 thresholds.
|
||||
|
||||
### Tests Added
|
||||
- `tests/test_formula_engine.py` — 188 lines covering batter/pitcher value computation
|
||||
- `tests/test_evolution_seed.py` — 119 lines validating seed data structure and idempotency
|
||||
- `tests/test_evolution_track_api.py` — 132 lines for track catalog endpoints
|
||||
- `tests/test_season_stats_model.py` — 451 lines for the stats model CRUD
|
||||
|
||||
## Render Pipeline
|
||||
|
||||
- **Persistent browser instance** (WP-02, `#89`): Playwright Chromium browser is now launched once at startup via FastAPI lifespan hooks and reused across all card render requests. Eliminates ~1-1.5s per-request launch/teardown overhead. Uses `asyncio.Lock` to prevent concurrent launch races and error-tolerant shutdown.
|
||||
- **Dockerfile optimization**: Multi-stage build, switched to `python:3.11-slim-bookworm` for Playwright compatibility, pinned base image.
|
||||
|
||||
## Query Performance
|
||||
|
||||
- **Batch Paperdex lookups** (`#17`): Replaced N+1 per-player Paperdex queries with a single batch `WHERE player_id IN (...)` query in both `get_players` and `get_random_player` endpoints.
|
||||
- **Materialize queryset** (`get_random_player`): Added `list()` call before double-iteration to prevent queryset exhaustion.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Pitcher OPS split weighting** (`#6`): Use `max()` instead of incorrect weighting for pitcher OPS formula
|
||||
- **Remove stub endpoint** (`#11`): Deleted unimplemented `live_update_pitching` endpoint that returned 501
|
||||
- **Docker build fixes**: Removed Docker Hub registry cache (blob limits), pinned base image for Playwright compatibility
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- `pyproject.toml` added for ruff configuration
|
||||
- `requirements.txt` pinned all dependency versions (`#64`)
|
||||
|
||||
## Merge Conflict Resolution
|
||||
|
||||
**Files:** `app/main.py`, `app/routers_v2/players.py`
|
||||
**Cause:** Main had the Phase 0 render pipeline optimization (PR #94 — asyncio.Lock, `--no-sandbox` args, error-tolerant shutdown). Next-release had an earlier version of the same persistent browser feature (`#89` — simpler, no lock, no error handling).
|
||||
**Resolution:** Kept main's version (the more robust implementation) for all browser-related code. Added the evolution router import/inclusion from next-release.
|
||||
|
||||
## Open PRs Still Targeting next-release
|
||||
|
||||
After this release, 5 PRs remain open against next-release:
|
||||
|
||||
| PR | Title | Status |
|
||||
|----|-------|--------|
|
||||
| #84 | SQL migration for evolution tables (WP-00) | Conflict |
|
||||
| #95 | Phase 0 benchmark script | Mergeable |
|
||||
| #96 | Self-hosted WOFF2 fonts | Mergeable |
|
||||
| #98 | WP-08 evaluate endpoint | Conflict |
|
||||
| #99 | Lifespan hooks for browser pre-warm | Conflict |
|
||||
|
||||
PRs #98 and #99 conflict because they contain earlier versions of code already in main (evaluate endpoint and browser lifespan). PR #84 conflicts on shared test infrastructure files. These should be rebased or have conflicts resolved before merging into next-release.
|
||||
49
paper-dynasty/scout-token-wallet-fix.md
Normal file
49
paper-dynasty/scout-token-wallet-fix.md
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
title: "Fix: Scout Token Purchase Not Deducting Currency"
|
||||
description: "Changelog for PR #90 — scout token buy flow was silently failing to deduct 200₼ due to using db_patch instead of the dedicated money endpoint."
|
||||
domain: development
|
||||
type: troubleshooting
|
||||
tags:
|
||||
- paper-dynasty
|
||||
- discord
|
||||
- troubleshooting
|
||||
- python
|
||||
---
|
||||
|
||||
# Fix: Scout Token Purchase Not Deducting Currency
|
||||
|
||||
**PR**: #90 — `fix/scout-token-deduction`
|
||||
**Merged**: 2026-03-16
|
||||
**File**: `discord_ui/scout_view.py`
|
||||
|
||||
## Bug
|
||||
|
||||
When a player exhausted their 2 free daily scout tokens and purchased an extra one for 200₼, the token was granted and the card was scouted, but the wallet balance was never reduced. Players could buy unlimited scout tokens for free.
|
||||
|
||||
## Root Cause
|
||||
|
||||
`BuyScoutTokenView.buy_button` used `db_patch("teams", params=[("wallet", new_wallet)])` to set the wallet directly. The Paper Dynasty API does not support mutating the `wallet` field via generic PATCH query params — the request returned 200 but silently ignored the parameter.
|
||||
|
||||
## Fix
|
||||
|
||||
Replaced with the dedicated money endpoint used by all other wallet mutations:
|
||||
|
||||
```python
|
||||
# Before (broken — silently ignored by API)
|
||||
await db_patch("teams", object_id=team["id"], params=[("wallet", new_wallet)])
|
||||
|
||||
# After (correct)
|
||||
await db_post(f'teams/{team["id"]}/money/-{SCOUT_TOKEN_COST}')
|
||||
```
|
||||
|
||||
Removed unused `db_patch` import from the file.
|
||||
|
||||
## Key Pattern: Wallet Mutations
|
||||
|
||||
The Paper Dynasty API requires dedicated endpoints for wallet changes. Never use `db_patch` with a `wallet` param.
|
||||
|
||||
| Operation | Pattern | Example |
|
||||
|-----------|---------|---------|
|
||||
| Add money | `db_post(f'teams/{id}/money/{amount}')` | Daily check-in (`packs.py:218`) |
|
||||
| Deduct money | `db_post(f'teams/{id}/money/-{amount}')` | Card purchases (`selectors.py:372`) |
|
||||
| Atomic buy | `db_get(f'teams/{id}/buy/players', params=[...])` | Marketplace (`marketplace.py:82`) |
|
||||
Loading…
Reference in New Issue
Block a user