XP-Modell nach VOD-Daten: 1->2 kostet 2 XP, Carousels geben keine XP

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
team3
2026-07-23 07:29:32 +02:00
parent 11dc8994ff
commit a78aa5f1e9
5 changed files with 17 additions and 25 deletions

View File

@@ -195,14 +195,16 @@ def test_pve_freezes_streak_and_pays_no_win_bonus(art, cfg):
def test_natural_level_progression(art, cfg):
"""Ohne XP-Kauf: Level 1 bei 1-2, Level 4 bei 2-1, Level 5 mit 6/20 bei 3-2."""
"""Ohne XP-Kauf, laut VOD: 1-3 Lvl 2, 1-4 Lvl 3, 2-1 Lvl 3 (2/6), 2-5 Lvl 4 (2/10)."""
game = Game(art, cfg, seed=6)
assert game.player.level == 1 and game.player.xp == 2 # 1-1-XP gebankt
assert game.player.level == 1 and game.player.xp == 0 # 1-1 gibt keine XP
checks = {}
while not game.over and game.round["label"] != "3-3":
checks[game.round["label"]] = (game.player.level, game.player.xp)
game.step()
assert checks["1-2"][0] == 1
assert checks["1-3"][0] == 3 # gratis Level 2 + kaskadierte XP
assert checks["2-1"] == (4, 0)
assert checks["3-2"] == (5, 6)
assert checks["1-2"] == (1, 0)
assert checks["1-3"] == (2, 0)
assert checks["1-4"] == (3, 0)
assert checks["2-1"] == (3, 2)
assert checks["2-5"] == (4, 2)
assert checks["3-2"] == (5, 0)

View File

@@ -26,10 +26,9 @@ def validate(c: dict) -> None:
if len(c["pool"]["sizes"]) != 5:
raise ValueError("pool sizes must have 5 cost tiers")
xp = c["xp"]
# Level 1 -> 2 ist gratis (erste Welle); to_next deckt 2->3 bis (max-1)->max.
if len(xp["to_next"]) != xp["max_level"] - 2:
if len(xp["to_next"]) != xp["max_level"] - 1:
raise ValueError(
f"xp.to_next needs {xp['max_level'] - 2} entries, has {len(xp['to_next'])}"
f"xp.to_next needs {xp['max_level'] - 1} entries, has {len(xp['to_next'])}"
)
if len(c["shop"]["odds"]) != xp["max_level"]:
raise ValueError("shop odds must have one row per level")

View File

@@ -29,10 +29,11 @@ copies_for_2star = 3
copies_for_3star = 9
[xp]
# Start auf Level 1. Level 2 kommt GRATIS nach der ersten PvE-Welle (1-2),
# XP banken solange. to_next: Index 0 = Level 2 -> 3.
# Start auf Level 1. Kampfrunden (PvE/PvP) geben 2 XP, Carousels KEINE.
# Verlauf laut VOD: 1-3 Lvl 2, 1-4 Lvl 3, 2-1 Lvl 3 (2/6), 2-5 Lvl 4 (2/10).
# to_next: Index 0 = Level 1 -> 2.
start_level = 1
to_next = [2, 6, 10, 20, 36, 60, 68, 68]
to_next = [2, 2, 6, 10, 20, 36, 60, 68, 68]
passive_per_round = 2
buy_amount = 4
buy_cost = 4

View File

@@ -27,9 +27,7 @@ def xp_to_next(level: int, cfg: dict) -> int | None:
xp = cfg["xp"]
if level >= xp["max_level"]:
return None
if level < 2:
return None # Level 2 kommt gratis nach der ersten PvE-Welle
return xp["to_next"][level - 2]
return xp["to_next"][level - 1]
def apply_xp(level: int, xp: int, gained: int, cfg: dict) -> tuple[int, int]:

View File

@@ -44,11 +44,10 @@ class Game:
]
# 1-1-Carousel läuft automatisch für alle ab, dann startet 1-2.
# Die 2 Passiv-XP aus 1-1 banken (Level 2 kommt erst nach 1-2 gratis).
# Carousels geben keine XP.
if self.round["kind"] == "carousel":
for p in self.players:
self._carousel(p)
p.xp += cfg["xp"]["passive_per_round"]
self.idx += 1
# Bei 1-2 gibt es noch keinen Shop — niemand hat Gold.
for p in self.players:
@@ -156,10 +155,6 @@ class Game:
p.level, p.xp = economy.apply_xp(
p.level, p.xp, self.cfg["xp"]["passive_per_round"], self.cfg
)
# Nach der ersten PvE-Welle gibt es Level 2 gratis; gebankte XP kaskadieren.
if rnd["label"] == "1-2" and p.level == 1:
p.level = 2
p.level, p.xp = economy.apply_xp(p.level, p.xp, 0, self.cfg)
self._advance()
@@ -232,12 +227,9 @@ class Game:
self.idx += 1
if self.round["kind"] != "carousel":
break
# Carousel ist keine eigene Einkommensrunde — nur Loot, Unit und XP.
# Carousel gibt weder Einkommen noch XP — nur Loot und Unit.
for p in self._alive():
self._carousel(p)
p.level, p.xp = economy.apply_xp(
p.level, p.xp, self.cfg["xp"]["passive_per_round"], self.cfg
)
for p in self._alive():
if p is self.player and p.shop_locked:
continue