Augment-Effekte wirken: Gold/XP/Komponenten/Units/Spieler-HP bei Auswahl, Team-Buffs im Score, Junk gefiltert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,12 @@ def spell_dps_cap(static: dict, roles: dict) -> float | None:
|
||||
return values[int(len(values) * 0.9)]
|
||||
|
||||
|
||||
def _augment_specs(static: dict) -> dict:
|
||||
from tft.model.augments import build_specs
|
||||
|
||||
return build_specs(static["augments"], static["units"])
|
||||
|
||||
|
||||
def build(static: dict, learned: dict | None = None, extra_meta: dict | None = None) -> dict:
|
||||
roles = {
|
||||
api: baseline.classify_role(unit) for api, unit in static["units"].items()
|
||||
@@ -107,6 +113,7 @@ def build(static: dict, learned: dict | None = None, extra_meta: dict | None = N
|
||||
"item_pool": craftable_items(static["items"]),
|
||||
"component_pool": component_pool(static["items"]),
|
||||
"recipes": recipes(static["items"]),
|
||||
"augment_specs": _augment_specs(static),
|
||||
# Riot-Match-API liefert keine Augments (Feld entfernt) — leer heißt:
|
||||
# der Sim nutzt alle geparsten Augments als Pool.
|
||||
"augment_pool": [],
|
||||
|
||||
121
backend/tft/model/augments.py
Normal file
121
backend/tft/model/augments.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Augment-Atome: mechanisch anwendbare Effekte aus effects-Keys und Beschreibung.
|
||||
|
||||
Deterministische Atome (~200/432): Gold, XP, Komponenten, Units, Team-Buffs,
|
||||
Spieler-HP. Bedingte und Kampf-Spezialeffekte bleiben beim gelernten Fallback.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from tft.model.statsheet import _fraction
|
||||
|
||||
GOLD_KEYS = ("Gold", "InstantGold", "GoldNow", "GoldAmount")
|
||||
XP_KEYS = ("XP", "InstantXP", "Experience", "UpfrontXP")
|
||||
COMPONENT_KEYS = ("NumComponents", "ComponentsToGive")
|
||||
ANVIL_KEYS = ("CompletedAnvils",)
|
||||
PLAYER_HP_KEYS = ("PlayerHealth", "Heal", "Health")
|
||||
|
||||
# effects-Key -> (Accumulator-Stat, Normalisierung)
|
||||
STAT_KEYS = {
|
||||
"AD": ("ad_pct", "frac"), "BonusAD": ("ad_pct", "frac"), "ADBonus": ("ad_pct", "frac"),
|
||||
"AP": ("ap_flat", "flat"), "AbilityPower": ("ap_flat", "flat"), "BonusAP": ("ap_flat", "flat"),
|
||||
"AS": ("as_pct", "frac"), "AttackSpeed": ("as_pct", "frac"),
|
||||
"BonusAS": ("as_pct", "frac"), "TeamAttackSpeed": ("as_pct", "frac"),
|
||||
"Health": ("hp_flat", "flat"), "BaseHP": ("hp_flat", "flat"), "BonusHealth": ("hp_flat", "flat"),
|
||||
"Armor": ("armor_flat", "flat"), "BonusMR": ("mr_flat", "flat"),
|
||||
"DamageAmp": ("damage_amp", "frac"), "Durability": ("dr", "frac"),
|
||||
"Omnivamp": ("dr", "frac_half"), "CritChance": ("crit_chance", "frac"),
|
||||
}
|
||||
|
||||
TEAM_RE = re.compile(r"(your team|allies|units|champions|army) gain", re.I)
|
||||
CONDITIONAL_RE = re.compile(
|
||||
r"per |for each|each level|per level|combat start|when|whenever|casting"
|
||||
r"|(?:on|after) attack|takedown|dies|each round|after each", re.I)
|
||||
GOLD_RE = re.compile(r"[Gg]ain (\d+) gold(?! each| per)")
|
||||
GOLD_PER_STAGE_RE = re.compile(r"(\d+) gold (?:at the start of|each) (?:every )?stage", re.I)
|
||||
XP_RE = re.compile(r"[Gg]ain (\d+) XP")
|
||||
COMP_RE = re.compile(r"(\d+) random(?:\w| )*components?", re.I)
|
||||
COST_UNITS_RE = re.compile(r"[Gg]ain (\d+) (\d)-cost champions?")
|
||||
TACTICIAN_HP_RE = re.compile(r"[Gg]ain (\d+) Tactician Health")
|
||||
|
||||
|
||||
def _num(effects: dict, keys: tuple) -> float | None:
|
||||
for key in keys:
|
||||
val = effects.get(key)
|
||||
if isinstance(val, (int, float)) and val > 0:
|
||||
return val
|
||||
return None
|
||||
|
||||
|
||||
def build_spec(aug: dict, static_units: dict) -> dict:
|
||||
desc = aug.get("desc") or ""
|
||||
effects = aug.get("effects") or {}
|
||||
atoms = []
|
||||
|
||||
offerable = len(desc) >= 10 and "_Desc" not in desc and "@" not in desc
|
||||
if not offerable:
|
||||
return {"atoms": [], "offerable": False}
|
||||
|
||||
if TACTICIAN_HP_RE.search(desc):
|
||||
m = TACTICIAN_HP_RE.search(desc)
|
||||
atoms.append({"kind": "player_hp",
|
||||
"amount": int(_num(effects, PLAYER_HP_KEYS) or m.group(1))})
|
||||
|
||||
gold = _num(effects, GOLD_KEYS)
|
||||
m = GOLD_RE.search(desc)
|
||||
if gold is None and m:
|
||||
gold = int(m.group(1))
|
||||
if gold:
|
||||
atoms.append({"kind": "gold_now", "amount": int(gold)})
|
||||
|
||||
m = GOLD_PER_STAGE_RE.search(desc)
|
||||
if m:
|
||||
atoms.append({"kind": "gold_per_stage", "amount": int(m.group(1))})
|
||||
|
||||
xp = _num(effects, XP_KEYS)
|
||||
m = XP_RE.search(desc)
|
||||
if xp is None and m:
|
||||
xp = int(m.group(1))
|
||||
if xp:
|
||||
kind = "xp_per_round" if "each round" in desc.lower() else "xp_now"
|
||||
atoms.append({"kind": kind, "amount": int(xp)})
|
||||
|
||||
comps = _num(effects, COMPONENT_KEYS)
|
||||
m = COMP_RE.search(desc)
|
||||
if comps is None and m:
|
||||
comps = int(m.group(1))
|
||||
if comps:
|
||||
atoms.append({"kind": "components", "count": int(comps)})
|
||||
|
||||
anvils = _num(effects, ANVIL_KEYS)
|
||||
if anvils:
|
||||
atoms.append({"kind": "completed_items", "count": int(anvils)})
|
||||
|
||||
# Benannte Unit-Geschenke: "Gain a Briar, a Jinx, ..."
|
||||
if re.search(r"[Gg]ain (a|an) [A-Z]", desc):
|
||||
granted = [api for api, u in static_units.items()
|
||||
if u.get("name") and re.search(rf"[Gg]ain (?:a|an) {re.escape(u['name'])}\b", desc)]
|
||||
if granted:
|
||||
atoms.append({"kind": "unit_grant", "units": sorted(granted)})
|
||||
m = COST_UNITS_RE.search(desc)
|
||||
if m:
|
||||
atoms.append({"kind": "unit_grant", "cost": int(m.group(2)), "count": int(m.group(1))})
|
||||
|
||||
# Team-Stat-Buffs nur bei flachem "your team gains ..." ohne Bedingung.
|
||||
if TEAM_RE.search(desc) and not CONDITIONAL_RE.search(desc):
|
||||
for key, (stat, norm) in STAT_KEYS.items():
|
||||
val = effects.get(key)
|
||||
if not isinstance(val, (int, float)) or val == 0:
|
||||
continue
|
||||
if norm == "frac":
|
||||
value = _fraction(val)
|
||||
elif norm == "frac_half":
|
||||
value = _fraction(val) * 0.5
|
||||
else:
|
||||
value = val
|
||||
atoms.append({"kind": "team_buff", "stat": stat, "value": value})
|
||||
|
||||
return {"atoms": atoms, "offerable": True}
|
||||
|
||||
|
||||
def build_specs(static_augments: dict, static_units: dict) -> dict:
|
||||
return {api: build_spec(aug, static_units) for api, aug in static_augments.items()}
|
||||
@@ -94,6 +94,11 @@ def score_mechanical(board_units: list[dict], augments: list[str], artifact: dic
|
||||
|
||||
tiers = active_trait_tiers(board_units, static_traits, static_units)
|
||||
team_buffs, recognized = statsheet.trait_buffs(tiers, static_traits)
|
||||
# Augment-Team-Buffs (z.B. "+35 Health für dein Team") in die Stats mischen.
|
||||
for a in augments:
|
||||
for atom in artifact.get("augment_specs", {}).get(a, {}).get("atoms", []):
|
||||
if atom["kind"] == "team_buff":
|
||||
team_buffs[atom["stat"]] += atom["value"]
|
||||
spell_cap = artifact.get("spell_dps_cap")
|
||||
tier_profiles = artifact.get("tier_profiles", {})
|
||||
|
||||
|
||||
@@ -84,8 +84,9 @@ class Game:
|
||||
if not self.round.get("augment"):
|
||||
return
|
||||
augments = self.artifact["static"]["augments"]
|
||||
specs = self.artifact.get("augment_specs", {})
|
||||
pool = [a for a in (self.artifact.get("augment_pool") or list(augments))
|
||||
if a in augments]
|
||||
if a in augments and specs.get(a, {}).get("offerable", True)]
|
||||
# Pro Runde eine Stufe rollen; alle Angebote (Spieler + Bots) kommen daraus.
|
||||
weights = self.cfg.get("augments", {}).get("tier_weights", {}).get(
|
||||
self.round["label"], [35, 50, 15]
|
||||
@@ -101,7 +102,9 @@ class Game:
|
||||
continue
|
||||
choices = [a for a in tier_pool if a not in b.augments]
|
||||
if choices:
|
||||
b.augments.append(self.rng.choice(choices))
|
||||
chosen = self.rng.choice(choices)
|
||||
b.augments.append(chosen)
|
||||
self._apply_augment(b, chosen)
|
||||
|
||||
# ---- human actions (delegation) ----
|
||||
|
||||
@@ -127,7 +130,51 @@ class Game:
|
||||
self.player.shop_locked = not self.player.shop_locked
|
||||
|
||||
def pick_augment(self, choice: int) -> None:
|
||||
api = self.player.augment_offer[choice] \
|
||||
if 0 <= choice < len(self.player.augment_offer) else None
|
||||
player.pick_augment(self.player, choice)
|
||||
if api:
|
||||
self._apply_augment(self.player, api)
|
||||
|
||||
def _apply_augment(self, p: PlayerState, api: str) -> None:
|
||||
"""Deterministische Augment-Atome sofort bzw. als Dauereffekt anwenden."""
|
||||
spec = self.artifact.get("augment_specs", {}).get(api, {})
|
||||
for atom in spec.get("atoms", []):
|
||||
kind = atom["kind"]
|
||||
if kind == "gold_now":
|
||||
p.gold += atom["amount"]
|
||||
elif kind == "gold_per_stage":
|
||||
p.gold_per_stage += atom["amount"]
|
||||
elif kind == "xp_now":
|
||||
p.level, p.xp = economy.apply_xp(p.level, p.xp, atom["amount"], self.cfg)
|
||||
elif kind == "xp_per_round":
|
||||
p.xp_per_round += atom["amount"]
|
||||
elif kind == "player_hp":
|
||||
p.hp += atom["amount"]
|
||||
elif kind == "components":
|
||||
player.grant_loot(p, atom["count"], 0, self.artifact, self.rng)
|
||||
elif kind == "completed_items":
|
||||
pool = self.artifact.get("item_pool") or []
|
||||
for _ in range(atom["count"]):
|
||||
if pool:
|
||||
p.items.append(self.rng.choice(pool))
|
||||
elif kind == "unit_grant":
|
||||
self._grant_units(p, atom)
|
||||
# team_buff wirkt im Score, nicht hier.
|
||||
|
||||
def _grant_units(self, p: PlayerState, atom: dict) -> None:
|
||||
apis = list(atom.get("units", []))
|
||||
if "cost" in atom:
|
||||
for _ in range(atom.get("count", 1)):
|
||||
candidates = self.pool.units_of_cost(atom["cost"])
|
||||
if candidates:
|
||||
apis.append(self.rng.choice(candidates))
|
||||
for api in apis:
|
||||
if len(p.bench) >= player.BENCH_SIZE or self.pool.available(api) < 1:
|
||||
continue
|
||||
self.pool.take(api)
|
||||
p.bench.append({"api_name": api, "stars": 1, "items": []})
|
||||
player.merge(p, api, 1)
|
||||
|
||||
# ---- round resolution ----
|
||||
|
||||
@@ -161,7 +208,8 @@ class Game:
|
||||
p.gold, p.streak if is_pvp else 0, is_pvp and won, rnd["label"], self.cfg
|
||||
)
|
||||
p.level, p.xp = economy.apply_xp(
|
||||
p.level, p.xp, self.cfg["xp"]["passive_per_round"], self.cfg
|
||||
p.level, p.xp,
|
||||
self.cfg["xp"]["passive_per_round"] + p.xp_per_round, self.cfg
|
||||
)
|
||||
|
||||
self._advance()
|
||||
@@ -228,6 +276,7 @@ class Game:
|
||||
|
||||
def _advance(self) -> None:
|
||||
"""Zur nächsten Planungsrunde; Carousels laufen dabei automatisch für alle ab."""
|
||||
prev_stage = self.round["stage"]
|
||||
while True:
|
||||
if self.idx + 1 >= len(self.rounds):
|
||||
self._finish_by_hp()
|
||||
@@ -238,6 +287,9 @@ class Game:
|
||||
# Carousel gibt weder Einkommen noch XP — nur Loot und Unit.
|
||||
for p in self._alive():
|
||||
self._carousel(p)
|
||||
if self.round["stage"] != prev_stage:
|
||||
for p in self._alive():
|
||||
p.gold += p.gold_per_stage # Augment: Gold zum Stage-Start
|
||||
for p in self._alive():
|
||||
if p is self.player and p.shop_locked:
|
||||
continue
|
||||
|
||||
@@ -34,6 +34,8 @@ class PlayerState:
|
||||
augment_offer: list = field(default_factory=list)
|
||||
placement: int | None = None
|
||||
last_opponent: str | None = None
|
||||
gold_per_stage: int = 0
|
||||
xp_per_round: int = 0
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user