Mechanisches Boardstärke-Modell: Statsheet aus cdragon-Exaktdaten, sqrt(eHP×DPS), Tier-Anker, Tau-Fit

A/B auf 38 Holdout-Matches: mechanical 0.724 vs legacy 0.695. Promotion durchgeführt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
team3
2026-07-23 08:52:56 +02:00
parent 7c72761190
commit c450841822
10 changed files with 605 additions and 31 deletions

View File

@@ -3,8 +3,67 @@
Fails loudly on missing keys — never guess through structure drift.
"""
import re
CDRAGON_GAME = "https://raw.communitydragon.org/latest/game"
DAMAGE_TAG_RE = re.compile(
r"<(magicDamage|physicalDamage|trueDamage)>(.*?)</\1>", re.S
)
VAR_RE = re.compile(r"@([A-Za-z0-9_]+?)(?:\*[\d.]+)?@")
# Reihenfolge = Priorität; ADDamage/APDamage werden gesondert addiert.
DAMAGE_FALLBACKS = (
"Damage", "DamageAP", "DamageAD", "SpellDamage",
"DamagePerSecond", "TrueDamagePerSecond", "BonusDamageOnAttack",
)
TAG_TO_TYPE = {"magicDamage": "magic", "physicalDamage": "physical", "trueDamage": "true"}
def resolve_spell(ability: dict) -> dict:
"""Spell-Schaden pro Stern aus Desc-Markup + Variablen auflösen.
Stern-Konvention der Arrays: value[1..3] = 1-3 Sterne (verifiziert).
"""
desc = ability.get("desc") or ""
variables = {v["name"]: v["value"] for v in ability.get("variables", [])
if v.get("value")}
def get(name: str):
val = variables.get(name)
return val if val and any(val) else None
dmg_type = None
array = None
for m in DAMAGE_TAG_RE.finditer(desc):
for vm in VAR_RE.finditer(m.group(2)):
name = vm.group(1)
array = get(name) or get(name.removeprefix("Modified"))
if array:
dmg_type = TAG_TO_TYPE[m.group(1)]
break
if array:
break
if array is None:
ad, ap = get("ADDamage"), get("APDamage")
if ad and ap:
array = [a + b for a, b in zip(ad, ap)]
else:
for name in DAMAGE_FALLBACKS:
array = get(name)
if array:
break
has_ap = "%i:scaleAP%" in desc
has_ad = "%i:scaleAD%" in desc
scaling = "both" if has_ap and has_ad else "ap" if has_ap else "ad" if has_ad else None
return {
"spell_damage": array,
"spell_damage_type": dmg_type or ("magic" if array else None),
"spell_scaling": scaling,
}
def icon_url(asset_path: str) -> str:
p = asset_path.lower()
@@ -39,10 +98,8 @@ def parse(raw: dict, set_override: int | None = None) -> dict:
"role": c.get("role"),
"stats": c["stats"],
"ability_name": c["ability"]["name"],
"ability_variables": {
v["name"]: v["value"] for v in c["ability"]["variables"]
},
"icon": icon_url(c["squareIcon"]),
**resolve_spell(c["ability"]),
}
traits = {}
@@ -52,7 +109,8 @@ def parse(raw: dict, set_override: int | None = None) -> dict:
"api_name": t["apiName"],
"name": t["name"],
"breakpoints": [
{"min_units": e["minUnits"], "style": e["style"]}
{"min_units": e["minUnits"], "style": e["style"],
"variables": e.get("variables") or {}}
for e in t["effects"]
],
"icon": icon_url(t["icon"]),
@@ -74,6 +132,7 @@ def parse(raw: dict, set_override: int | None = None) -> dict:
"api_name": api,
"name": i["name"],
"composition": i["composition"],
"effects": i.get("effects") or {},
"icon": icon_url(i["icon"]) if i["icon"] else None,
}
if "Augment" in api: