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:
@@ -243,14 +243,49 @@ def test_loot_drops_components(art, cfg):
|
|||||||
|
|
||||||
|
|
||||||
def test_bot_leveling_follows_targets(art, cfg):
|
def test_bot_leveling_follows_targets(art, cfg):
|
||||||
|
from tft.sim import policy as policy_mod
|
||||||
|
|
||||||
game = Game(art, cfg, seed=12)
|
game = Game(art, cfg, seed=12)
|
||||||
levels_at = {}
|
levels_at = {}
|
||||||
while not game.over and game.round["label"] != "4-3":
|
while not game.over and game.round["label"] != "4-3":
|
||||||
label = game.round["label"]
|
label = game.round["label"]
|
||||||
if label in ("2-6", "3-3", "4-2"):
|
if label in ("2-6", "3-3", "4-2"):
|
||||||
levels_at[label] = [b.level for b in game.bots if b.alive]
|
levels_at[label] = [b.level for b in game.bots if b.alive]
|
||||||
|
policy_mod.act(game.player, game.pool, game.artifact, game.cfg, game.rng,
|
||||||
|
game.round, policy_mod.ARCHETYPES["fast8"])
|
||||||
game.step()
|
game.step()
|
||||||
assert all(lvl >= 5 for lvl in levels_at["2-6"]) # L5 ab 2-5
|
assert all(lvl >= 5 for lvl in levels_at["2-6"]) # L5 ab 2-5
|
||||||
# L6 ab 3-1/3-2 — goldarme Bots dürfen 1-2 Runden nachhinken
|
# L6 ab 3-1/3-2 — goldarme Bots dürfen 1-2 Runden nachhinken
|
||||||
assert sum(1 for lvl in levels_at["3-3"] if lvl >= 6) >= len(levels_at["3-3"]) - 2
|
assert sum(1 for lvl in levels_at["3-3"] if lvl >= 6) >= len(levels_at["3-3"]) - 2
|
||||||
assert any(lvl >= 7 for lvl in levels_at["4-2"]) # fast8/streaker auf 7
|
assert any(lvl >= 7 for lvl in levels_at["4-2"]) # fast8/streaker auf 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_augment_effects_apply(art, cfg):
|
||||||
|
game = Game(art, cfg, seed=3)
|
||||||
|
game.artifact = {**art, "augment_specs": {
|
||||||
|
"AUG_GOLD": {"atoms": [{"kind": "gold_now", "amount": 7},
|
||||||
|
{"kind": "gold_per_stage", "amount": 7}], "offerable": True},
|
||||||
|
"AUG_UNIT": {"atoms": [{"kind": "unit_grant", "cost": 2, "count": 2}], "offerable": True},
|
||||||
|
}}
|
||||||
|
gold_before = game.player.gold
|
||||||
|
game.player.augment_offer = ["AUG_GOLD"]
|
||||||
|
game.pick_augment(0)
|
||||||
|
assert game.player.gold == gold_before + 7
|
||||||
|
assert game.player.gold_per_stage == 7
|
||||||
|
|
||||||
|
bench_before = len(game.player.bench)
|
||||||
|
pool_before = sum(game.pool.available(a) for a in game.pool.copies)
|
||||||
|
game.player.augment_offer = ["AUG_UNIT"]
|
||||||
|
game.pick_augment(0)
|
||||||
|
assert len(game.player.bench) == bench_before + 2
|
||||||
|
assert sum(game.pool.available(a) for a in game.pool.copies) == pool_before - 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_team_buff_augment_raises_score(art, cfg):
|
||||||
|
from tft.model.score import score_mechanical
|
||||||
|
buffed_art = {**art, "augment_specs": {
|
||||||
|
"AUG_BUFF": {"atoms": [{"kind": "team_buff", "stat": "hp_pct", "value": 0.3}],
|
||||||
|
"offerable": True}}}
|
||||||
|
board = [{"api_name": "U2_0", "stars": 2, "items": []}]
|
||||||
|
assert score_mechanical(board, ["AUG_BUFF"], buffed_art) > \
|
||||||
|
score_mechanical(board, [], buffed_art)
|
||||||
|
|||||||
@@ -62,3 +62,27 @@ def test_unit_multipliers_learned():
|
|||||||
learned = learn(rows)
|
learned = learn(rows)
|
||||||
assert learned["units"]["TFT17_Strong"] > 1.05
|
assert learned["units"]["TFT17_Strong"] > 1.05
|
||||||
assert learned["units"]["TFT17_Weak"] < 0.95
|
assert learned["units"]["TFT17_Weak"] < 0.95
|
||||||
|
|
||||||
|
|
||||||
|
def test_augment_spec_builder():
|
||||||
|
from tft.model.augments import build_spec
|
||||||
|
|
||||||
|
units = {"TFT17_Briar": {"name": "Briar", "cost": 1}}
|
||||||
|
gold = build_spec({"desc": "Gain 6 gold now.", "effects": {"Gold": 6.0}}, units)
|
||||||
|
assert gold["atoms"] == [{"kind": "gold_now", "amount": 6}]
|
||||||
|
|
||||||
|
buff = build_spec({"desc": "Your team gains 35 Health and 10% Attack Speed.",
|
||||||
|
"effects": {"Health": 35.0, "AS": 0.10}}, units)
|
||||||
|
kinds = {(a["kind"], a.get("stat")) for a in buff["atoms"]}
|
||||||
|
assert ("team_buff", "hp_flat") in kinds
|
||||||
|
assert ("team_buff", "as_pct") in kinds
|
||||||
|
|
||||||
|
cond = build_spec({"desc": "Your team gains 10 Health for each item.",
|
||||||
|
"effects": {"Health": 10.0}}, units)
|
||||||
|
assert not any(a["kind"] == "team_buff" for a in cond["atoms"])
|
||||||
|
|
||||||
|
unit = build_spec({"desc": "Gain a Briar and 2 gold.", "effects": {"Gold": 2.0}}, units)
|
||||||
|
assert {"kind": "unit_grant", "units": ["TFT17_Briar"]} in unit["atoms"]
|
||||||
|
|
||||||
|
junk = build_spec({"desc": "", "effects": {}}, units)
|
||||||
|
assert junk["offerable"] is False
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ def spell_dps_cap(static: dict, roles: dict) -> float | None:
|
|||||||
return values[int(len(values) * 0.9)]
|
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:
|
def build(static: dict, learned: dict | None = None, extra_meta: dict | None = None) -> dict:
|
||||||
roles = {
|
roles = {
|
||||||
api: baseline.classify_role(unit) for api, unit in static["units"].items()
|
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"]),
|
"item_pool": craftable_items(static["items"]),
|
||||||
"component_pool": component_pool(static["items"]),
|
"component_pool": component_pool(static["items"]),
|
||||||
"recipes": recipes(static["items"]),
|
"recipes": recipes(static["items"]),
|
||||||
|
"augment_specs": _augment_specs(static),
|
||||||
# Riot-Match-API liefert keine Augments (Feld entfernt) — leer heißt:
|
# Riot-Match-API liefert keine Augments (Feld entfernt) — leer heißt:
|
||||||
# der Sim nutzt alle geparsten Augments als Pool.
|
# der Sim nutzt alle geparsten Augments als Pool.
|
||||||
"augment_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)
|
tiers = active_trait_tiers(board_units, static_traits, static_units)
|
||||||
team_buffs, recognized = statsheet.trait_buffs(tiers, static_traits)
|
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")
|
spell_cap = artifact.get("spell_dps_cap")
|
||||||
tier_profiles = artifact.get("tier_profiles", {})
|
tier_profiles = artifact.get("tier_profiles", {})
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,9 @@ class Game:
|
|||||||
if not self.round.get("augment"):
|
if not self.round.get("augment"):
|
||||||
return
|
return
|
||||||
augments = self.artifact["static"]["augments"]
|
augments = self.artifact["static"]["augments"]
|
||||||
|
specs = self.artifact.get("augment_specs", {})
|
||||||
pool = [a for a in (self.artifact.get("augment_pool") or list(augments))
|
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.
|
# Pro Runde eine Stufe rollen; alle Angebote (Spieler + Bots) kommen daraus.
|
||||||
weights = self.cfg.get("augments", {}).get("tier_weights", {}).get(
|
weights = self.cfg.get("augments", {}).get("tier_weights", {}).get(
|
||||||
self.round["label"], [35, 50, 15]
|
self.round["label"], [35, 50, 15]
|
||||||
@@ -101,7 +102,9 @@ class Game:
|
|||||||
continue
|
continue
|
||||||
choices = [a for a in tier_pool if a not in b.augments]
|
choices = [a for a in tier_pool if a not in b.augments]
|
||||||
if choices:
|
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) ----
|
# ---- human actions (delegation) ----
|
||||||
|
|
||||||
@@ -127,7 +130,51 @@ class Game:
|
|||||||
self.player.shop_locked = not self.player.shop_locked
|
self.player.shop_locked = not self.player.shop_locked
|
||||||
|
|
||||||
def pick_augment(self, choice: int) -> None:
|
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)
|
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 ----
|
# ---- 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.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 = 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()
|
self._advance()
|
||||||
@@ -228,6 +276,7 @@ class Game:
|
|||||||
|
|
||||||
def _advance(self) -> None:
|
def _advance(self) -> None:
|
||||||
"""Zur nächsten Planungsrunde; Carousels laufen dabei automatisch für alle ab."""
|
"""Zur nächsten Planungsrunde; Carousels laufen dabei automatisch für alle ab."""
|
||||||
|
prev_stage = self.round["stage"]
|
||||||
while True:
|
while True:
|
||||||
if self.idx + 1 >= len(self.rounds):
|
if self.idx + 1 >= len(self.rounds):
|
||||||
self._finish_by_hp()
|
self._finish_by_hp()
|
||||||
@@ -238,6 +287,9 @@ class Game:
|
|||||||
# Carousel gibt weder Einkommen noch XP — nur Loot und Unit.
|
# Carousel gibt weder Einkommen noch XP — nur Loot und Unit.
|
||||||
for p in self._alive():
|
for p in self._alive():
|
||||||
self._carousel(p)
|
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():
|
for p in self._alive():
|
||||||
if p is self.player and p.shop_locked:
|
if p is self.player and p.shop_locked:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class PlayerState:
|
|||||||
augment_offer: list = field(default_factory=list)
|
augment_offer: list = field(default_factory=list)
|
||||||
placement: int | None = None
|
placement: int | None = None
|
||||||
last_opponent: str | None = None
|
last_opponent: str | None = None
|
||||||
|
gold_per_stage: int = 0
|
||||||
|
xp_per_round: int = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alive(self) -> bool:
|
def alive(self) -> bool:
|
||||||
|
|||||||
Reference in New Issue
Block a user