Unit-Stärken präzisiert: Stat-Proxy-Multiplikatoren + gelernte Unit-Gewichte
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,3 +50,15 @@ def test_empty_rows():
|
||||
assert learned["traits"] == {}
|
||||
assert learned["pairs"] == {}
|
||||
assert learned["n_boards"] == 0
|
||||
|
||||
|
||||
def test_unit_multipliers_learned():
|
||||
def row(placement, unit):
|
||||
units = [{"character_id": unit, "tier": 2, "itemNames": []}]
|
||||
return (placement, json.dumps(units), json.dumps([]), json.dumps([]))
|
||||
|
||||
rows = [row(1, "TFT17_Strong") for _ in range(150)]
|
||||
rows += [row(8, "TFT17_Weak") for _ in range(150)]
|
||||
learned = learn(rows)
|
||||
assert learned["units"]["TFT17_Strong"] > 1.05
|
||||
assert learned["units"]["TFT17_Weak"] < 0.95
|
||||
|
||||
@@ -86,3 +86,33 @@ def test_craftable_item_pool():
|
||||
"TFT_Item_Radiant": {"composition": None},
|
||||
}
|
||||
assert craftable_items(items) == ["TFT_Item_InfinityEdge"]
|
||||
|
||||
|
||||
def test_stat_mults_rank_within_cost():
|
||||
from tft.model.baseline import compute_stat_mults
|
||||
|
||||
def unit(hp, armor, ad, aspd):
|
||||
return {"cost": 2, "traits": [], "role": "Tank",
|
||||
"stats": {"hp": hp, "armor": armor, "magicResist": armor,
|
||||
"damage": ad, "attackSpeed": aspd, "mana": 60,
|
||||
"initialMana": 0, "range": 1}}
|
||||
|
||||
units = {"tanky": unit(900, 60, 45, 0.6), "avg": unit(700, 40, 55, 0.7),
|
||||
"weak": unit(550, 25, 45, 0.6)}
|
||||
mults = compute_stat_mults(units)
|
||||
assert mults["tanky"] > mults["avg"] > mults["weak"]
|
||||
assert all(0.9 <= m <= 1.1 for m in mults.values())
|
||||
|
||||
|
||||
def test_stat_mults_identical_units_are_neutral():
|
||||
from tft.model.baseline import compute_stat_mults
|
||||
from tests.test_sim import make_static
|
||||
|
||||
mults = compute_stat_mults(make_static()["units"])
|
||||
assert all(m == 1.0 for m in mults.values())
|
||||
|
||||
|
||||
def test_stat_mult_raises_score():
|
||||
art = {**ARTIFACT, "stat_mults": {"TFT17_B": 1.1}}
|
||||
board = [{"api_name": "TFT17_B", "stars": 1, "items": []}]
|
||||
assert score_board(board, [], art) > score_board(board, [], ARTIFACT)
|
||||
|
||||
@@ -45,6 +45,7 @@ def build(static: dict, learned: dict | None = None, extra_meta: dict | None = N
|
||||
"augments": static["augments"],
|
||||
},
|
||||
"roles": roles,
|
||||
"stat_mults": baseline.compute_stat_mults(static["units"]),
|
||||
"item_pool": craftable_items(static["items"]),
|
||||
"learned": learned or {},
|
||||
}
|
||||
|
||||
@@ -6,11 +6,49 @@ def unit_value(cost: int, stars: int) -> float:
|
||||
|
||||
|
||||
def stat_proxies(stats: dict) -> dict:
|
||||
ehp = stats["hp"] * (1 + (stats["armor"] + stats["magicResist"]) / 2 / 100)
|
||||
auto_dps = stats["damage"] * stats["attackSpeed"]
|
||||
mana_gap = max(stats["mana"] - stats["initialMana"], 1)
|
||||
cast_rate = stats["attackSpeed"] * 10 / mana_gap
|
||||
return {"ehp": ehp, "auto_dps": auto_dps, "cast_rate": cast_rate}
|
||||
# cdragon liefert für manche Units null-Werte — als 0 behandeln.
|
||||
hp = stats.get("hp") or 0
|
||||
resists = ((stats.get("armor") or 0) + (stats.get("magicResist") or 0)) / 2
|
||||
ad = stats.get("damage") or 0
|
||||
aspd = stats.get("attackSpeed") or 0
|
||||
mana_gap = max((stats.get("mana") or 0) - (stats.get("initialMana") or 0), 1)
|
||||
return {
|
||||
"ehp": hp * (1 + resists / 100),
|
||||
"auto_dps": ad * aspd,
|
||||
"cast_rate": aspd * 10 / mana_gap,
|
||||
}
|
||||
|
||||
|
||||
STAT_MULT_RANGE = (0.9, 1.1)
|
||||
|
||||
|
||||
def compute_stat_mults(static_units: dict) -> dict:
|
||||
"""Stat-Stärke relativ zur eigenen Kostenstufe: (eHP + Offense) / 2, gekappt.
|
||||
|
||||
Offense = Maximum aus Auto-DPS und Cast-Rate (je normalisiert), damit
|
||||
Caster nicht gegen Auto-Attacker abfallen.
|
||||
"""
|
||||
proxies = {api: stat_proxies(u["stats"]) for api, u in static_units.items()}
|
||||
by_cost: dict[int, list[str]] = {}
|
||||
for api, u in static_units.items():
|
||||
by_cost.setdefault(u["cost"], []).append(api)
|
||||
|
||||
mults = {}
|
||||
for apis in by_cost.values():
|
||||
n = len(apis)
|
||||
avg = {
|
||||
key: sum(proxies[a][key] for a in apis) / n or 1.0
|
||||
for key in ("ehp", "auto_dps", "cast_rate")
|
||||
}
|
||||
for a in apis:
|
||||
ehp_norm = proxies[a]["ehp"] / avg["ehp"]
|
||||
offense = max(
|
||||
proxies[a]["auto_dps"] / avg["auto_dps"],
|
||||
proxies[a]["cast_rate"] / avg["cast_rate"],
|
||||
)
|
||||
raw = (ehp_norm + offense) / 2
|
||||
mults[a] = round(min(max(raw, STAT_MULT_RANGE[0]), STAT_MULT_RANGE[1]), 4)
|
||||
return mults
|
||||
|
||||
|
||||
def classify_role(unit: dict) -> str:
|
||||
|
||||
@@ -28,6 +28,7 @@ def learn(rows: list[tuple]) -> dict:
|
||||
trait_placements = defaultdict(list)
|
||||
item_placements = defaultdict(list)
|
||||
augment_placements = defaultdict(list)
|
||||
unit_placements = defaultdict(list)
|
||||
unit_top4 = defaultdict(int)
|
||||
pair_top4 = defaultdict(int)
|
||||
n_boards = 0
|
||||
@@ -40,6 +41,8 @@ def learn(rows: list[tuple]) -> dict:
|
||||
|
||||
units = json.loads(units_json)
|
||||
names = sorted({u["character_id"] for u in units})
|
||||
for name in names:
|
||||
unit_placements[name].append(placement)
|
||||
for u in units:
|
||||
for item in u.get("itemNames", []):
|
||||
item_placements[item].append(placement)
|
||||
@@ -69,6 +72,7 @@ def learn(rows: list[tuple]) -> dict:
|
||||
pairs[f"{a}|{b}"] = round(lift, 4)
|
||||
|
||||
return {
|
||||
"units": {k: round(_multiplier(v), 4) for k, v in unit_placements.items()},
|
||||
"traits": {k: round(_multiplier(v), 4) for k, v in trait_placements.items()},
|
||||
"items": {k: round(_multiplier(v), 4) for k, v in item_placements.items()},
|
||||
"augments": {k: round(_multiplier(v), 4) for k, v in augment_placements.items()},
|
||||
|
||||
@@ -32,18 +32,24 @@ def score_board(board_units: list[dict], augments: list[str], artifact: dict) ->
|
||||
static_units = artifact["static"]["units"]
|
||||
static_traits = artifact["static"]["traits"]
|
||||
learned = artifact.get("learned", {})
|
||||
unit_mults = learned.get("units", {})
|
||||
item_mults = learned.get("items", {})
|
||||
trait_mults = learned.get("traits", {})
|
||||
augment_mults = learned.get("augments", {})
|
||||
pair_lifts = learned.get("pairs", {})
|
||||
roles = artifact.get("roles", {})
|
||||
stat_mults = artifact.get("stat_mults", {})
|
||||
|
||||
total = 0.0
|
||||
for u in board_units:
|
||||
unit = static_units.get(u["api_name"])
|
||||
if not unit:
|
||||
continue
|
||||
value = baseline.unit_value(unit["cost"], u["stars"])
|
||||
value = (
|
||||
baseline.unit_value(unit["cost"], u["stars"])
|
||||
* stat_mults.get(u["api_name"], 1.0)
|
||||
* unit_mults.get(u["api_name"], 1.0)
|
||||
)
|
||||
role = roles.get(u["api_name"]) or baseline.classify_role(unit)
|
||||
for item in u.get("items", []):
|
||||
mult = item_mults.get(item, baseline.DEFAULT_ITEM_MULT)
|
||||
|
||||
Reference in New Issue
Block a user