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:
team3
2026-07-23 10:30:56 +02:00
parent 28fb667005
commit f7e569e519
7 changed files with 249 additions and 3 deletions

View File

@@ -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": [],

View 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()}

View File

@@ -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", {})

View File

@@ -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

View File

@@ -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: