mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
feat(web): Core Refactor Phase A — extract sampling and cache modules; add adaptive TTL + eviction heuristics, Redis PoC, and metrics wiring. Tests added for TTL, eviction, exports, splash-adaptive, card index, and service worker. Docs+roadmap updated.
This commit is contained in:
parent
c4a7fc48ea
commit
a029d430c5
49 changed files with 3889 additions and 701 deletions
123
code/web/services/sampling_config.py
Normal file
123
code/web/services/sampling_config.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"""Scoring & sampling configuration constants (Phase 2 extraction).
|
||||
|
||||
Centralizes knobs used by the sampling pipeline so future tuning (or
|
||||
experimentation via environment variables) can occur without editing the
|
||||
core algorithm code.
|
||||
|
||||
Public constants (import into sampling.py and tests):
|
||||
COMMANDER_COLOR_FILTER_STRICT
|
||||
COMMANDER_OVERLAP_BONUS
|
||||
COMMANDER_THEME_MATCH_BONUS
|
||||
SPLASH_OFF_COLOR_PENALTY
|
||||
ROLE_BASE_WEIGHTS
|
||||
ROLE_SATURATION_PENALTY
|
||||
|
||||
Helper functions:
|
||||
rarity_weight_base() -> dict[str, float]
|
||||
Returns per-rarity base weights (reads env each call to preserve
|
||||
existing test expectations that patch env before invoking sampling).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Dict, Tuple, Optional
|
||||
|
||||
# Commander related bonuses (identical defaults to previous inline values)
|
||||
COMMANDER_COLOR_FILTER_STRICT = True
|
||||
COMMANDER_OVERLAP_BONUS = 1.8
|
||||
COMMANDER_THEME_MATCH_BONUS = 0.9
|
||||
|
||||
# Penalties / bonuses
|
||||
SPLASH_OFF_COLOR_PENALTY = -0.3
|
||||
# Adaptive splash penalty feature flag & scaling factors.
|
||||
# When SPLASH_ADAPTIVE=1 the effective penalty becomes:
|
||||
# base_penalty * splash_adaptive_scale(color_count)
|
||||
# Where color_count is the number of distinct commander colors (1-5).
|
||||
# Default scale keeps existing behavior at 1-3 colors, softens at 4, much lighter at 5.
|
||||
SPLASH_ADAPTIVE_ENABLED = os.getenv("SPLASH_ADAPTIVE", "0") == "1"
|
||||
_DEFAULT_SPLASH_SCALE = "1:1.0,2:1.0,3:1.0,4:0.6,5:0.35"
|
||||
def parse_splash_adaptive_scale() -> Dict[int, float]: # dynamic to allow test env changes
|
||||
spec = os.getenv("SPLASH_ADAPTIVE_SCALE", _DEFAULT_SPLASH_SCALE)
|
||||
mapping: Dict[int, float] = {}
|
||||
for part in spec.split(','):
|
||||
part = part.strip()
|
||||
if not part or ':' not in part:
|
||||
continue
|
||||
k_s, v_s = part.split(':', 1)
|
||||
try:
|
||||
k = int(k_s)
|
||||
v = float(v_s)
|
||||
if 1 <= k <= 5 and v > 0:
|
||||
mapping[k] = v
|
||||
except Exception:
|
||||
continue
|
||||
# Ensure all 1-5 present; fallback to 1.0 if unspecified
|
||||
for i in range(1, 6):
|
||||
mapping.setdefault(i, 1.0)
|
||||
return mapping
|
||||
ROLE_SATURATION_PENALTY = -0.4
|
||||
|
||||
# Base role weights applied inside score calculation
|
||||
ROLE_BASE_WEIGHTS: Dict[str, float] = {
|
||||
"payoff": 2.5,
|
||||
"enabler": 2.0,
|
||||
"support": 1.5,
|
||||
"wildcard": 0.9,
|
||||
}
|
||||
|
||||
# Rarity base weights (diminishing duplicate influence applied in sampling pipeline)
|
||||
# Read from env at call time to allow tests to modify.
|
||||
|
||||
def rarity_weight_base() -> Dict[str, float]: # dynamic to allow env override per test
|
||||
return {
|
||||
"mythic": float(os.getenv("RARITY_W_MYTHIC", "1.2")),
|
||||
"rare": float(os.getenv("RARITY_W_RARE", "0.9")),
|
||||
"uncommon": float(os.getenv("RARITY_W_UNCOMMON", "0.65")),
|
||||
"common": float(os.getenv("RARITY_W_COMMON", "0.4")),
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"COMMANDER_COLOR_FILTER_STRICT",
|
||||
"COMMANDER_OVERLAP_BONUS",
|
||||
"COMMANDER_THEME_MATCH_BONUS",
|
||||
"SPLASH_OFF_COLOR_PENALTY",
|
||||
"SPLASH_ADAPTIVE_ENABLED",
|
||||
"parse_splash_adaptive_scale",
|
||||
"ROLE_BASE_WEIGHTS",
|
||||
"ROLE_SATURATION_PENALTY",
|
||||
"rarity_weight_base",
|
||||
"parse_rarity_diversity_targets",
|
||||
"RARITY_DIVERSITY_OVER_PENALTY",
|
||||
]
|
||||
|
||||
|
||||
# Extended rarity diversity (optional) ---------------------------------------
|
||||
# Env var RARITY_DIVERSITY_TARGETS pattern e.g. "mythic:0-1,rare:0-2,uncommon:0-4,common:0-6"
|
||||
# Parsed into mapping rarity -> (min,max). Only max is enforced currently (penalty applied
|
||||
# when overflow occurs); min reserved for potential future boosting logic.
|
||||
|
||||
RARITY_DIVERSITY_OVER_PENALTY = float(os.getenv("RARITY_DIVERSITY_OVER_PENALTY", "-0.5"))
|
||||
|
||||
def parse_rarity_diversity_targets() -> Optional[Dict[str, Tuple[int, int]]]:
|
||||
spec = os.getenv("RARITY_DIVERSITY_TARGETS")
|
||||
if not spec:
|
||||
return None
|
||||
targets: Dict[str, Tuple[int, int]] = {}
|
||||
for part in spec.split(','):
|
||||
part = part.strip()
|
||||
if not part or ':' not in part:
|
||||
continue
|
||||
name, rng = part.split(':', 1)
|
||||
name = name.strip().lower()
|
||||
if '-' not in rng:
|
||||
continue
|
||||
lo_s, hi_s = rng.split('-', 1)
|
||||
try:
|
||||
lo = int(lo_s)
|
||||
hi = int(hi_s)
|
||||
if lo < 0 or hi < lo:
|
||||
continue
|
||||
targets[name] = (lo, hi)
|
||||
except Exception:
|
||||
continue
|
||||
return targets or None
|
||||
Loading…
Add table
Add a link
Reference in a new issue