Regeln nach Recherche korrigiert: Schadensformel, Odds L8/L9, Start-Level 2, Streak ab 2, Verkaufsregel
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,8 @@ def test_shop_odds_shape(c):
|
|||||||
|
|
||||||
|
|
||||||
def test_xp_reaches_max_level(c):
|
def test_xp_reaches_max_level(c):
|
||||||
assert len(c["xp"]["to_next"]) == c["xp"]["max_level"] - 1
|
start = c["xp"].get("start_level", 1)
|
||||||
|
assert len(c["xp"]["to_next"]) == c["xp"]["max_level"] - start
|
||||||
assert all(x > 0 for x in c["xp"]["to_next"])
|
assert all(x > 0 for x in c["xp"]["to_next"])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ def test_streak_gold(cfg):
|
|||||||
|
|
||||||
|
|
||||||
def test_apply_xp_levels_up(cfg):
|
def test_apply_xp_levels_up(cfg):
|
||||||
level, xp = economy.apply_xp(1, 0, 4, cfg)
|
level, xp = economy.apply_xp(2, 0, 8, cfg)
|
||||||
assert level == 3 # 1->2 costs 2, 2->3 costs 2
|
assert level == 4 # 2->3 kostet 2, 3->4 kostet 6
|
||||||
assert xp == 0
|
assert xp == 0
|
||||||
level, _ = economy.apply_xp(9, 0, 1000, cfg)
|
level, _ = economy.apply_xp(9, 0, 1000, cfg)
|
||||||
assert level == cfg["xp"]["max_level"]
|
assert level == cfg["xp"]["max_level"]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from tft import paths
|
from tft import paths
|
||||||
from tft.model.score import active_trait_tiers
|
from tft.model.score import active_trait_tiers
|
||||||
|
from tft.sim import economy
|
||||||
from tft.sim.game import Game, InvalidAction
|
from tft.sim.game import Game, InvalidAction
|
||||||
|
|
||||||
app = FastAPI(title="tft-sim")
|
app = FastAPI(title="tft-sim")
|
||||||
@@ -150,8 +151,7 @@ def serialize(game: Game) -> dict:
|
|||||||
],
|
],
|
||||||
"player": {
|
"player": {
|
||||||
"hp": game.hp, "gold": game.gold, "level": game.level, "xp": game.xp,
|
"hp": game.hp, "gold": game.gold, "level": game.level, "xp": game.xp,
|
||||||
"xp_needed": game.cfg["xp"]["to_next"][game.level - 1]
|
"xp_needed": economy.xp_to_next(game.level, game.cfg),
|
||||||
if game.level < game.cfg["xp"]["max_level"] else None,
|
|
||||||
"streak": game.streak,
|
"streak": game.streak,
|
||||||
"score": round(game.player_score(), 1),
|
"score": round(game.player_score(), 1),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ def validate(c: dict) -> None:
|
|||||||
if len(c["pool"]["sizes"]) != 5:
|
if len(c["pool"]["sizes"]) != 5:
|
||||||
raise ValueError("pool sizes must have 5 cost tiers")
|
raise ValueError("pool sizes must have 5 cost tiers")
|
||||||
xp = c["xp"]
|
xp = c["xp"]
|
||||||
if len(xp["to_next"]) != xp["max_level"] - 1:
|
start = xp.get("start_level", 1)
|
||||||
|
if len(xp["to_next"]) != xp["max_level"] - start:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"xp.to_next needs {xp['max_level'] - 1} entries, has {len(xp['to_next'])}"
|
f"xp.to_next needs {xp['max_level'] - start} entries, has {len(xp['to_next'])}"
|
||||||
)
|
)
|
||||||
if len(c["shop"]["odds"]) != xp["max_level"]:
|
if len(c["shop"]["odds"]) != xp["max_level"]:
|
||||||
raise ValueError("shop odds must have one row per level")
|
raise ValueError("shop odds must have one row per level")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ set = 17
|
|||||||
|
|
||||||
[shop]
|
[shop]
|
||||||
# Odds pro Level: [1-cost, 2-cost, 3-cost, 4-cost, 5-cost], Summe = 100.
|
# Odds pro Level: [1-cost, 2-cost, 3-cost, 4-cost, 5-cost], Summe = 100.
|
||||||
# TODO: Quellen widersprechen sich für Level 5-9 (Patch 17.1b änderte Odds).
|
# Level 1-7 mehrquellig bestätigt; L8/L9 = Zwei-Quellen-Mehrheit (metabot, redeemertft).
|
||||||
odds = [
|
odds = [
|
||||||
[100, 0, 0, 0, 0], # Level 1
|
[100, 0, 0, 0, 0], # Level 1
|
||||||
[100, 0, 0, 0, 0], # Level 2
|
[100, 0, 0, 0, 0], # Level 2
|
||||||
@@ -15,8 +15,8 @@ odds = [
|
|||||||
[45, 33, 20, 2, 0], # Level 5
|
[45, 33, 20, 2, 0], # Level 5
|
||||||
[30, 40, 25, 5, 0], # Level 6
|
[30, 40, 25, 5, 0], # Level 6
|
||||||
[19, 30, 40, 10, 1], # Level 7
|
[19, 30, 40, 10, 1], # Level 7
|
||||||
[17, 24, 32, 24, 3], # Level 8
|
[15, 20, 32, 30, 3], # Level 8
|
||||||
[15, 18, 25, 30, 12],# Level 9
|
[10, 17, 25, 33, 15],# Level 9
|
||||||
[5, 10, 20, 40, 25], # Level 10
|
[5, 10, 20, 40, 25], # Level 10
|
||||||
]
|
]
|
||||||
reroll_cost = 2
|
reroll_cost = 2
|
||||||
@@ -29,8 +29,9 @@ copies_for_2star = 3
|
|||||||
copies_for_3star = 9
|
copies_for_3star = 9
|
||||||
|
|
||||||
[xp]
|
[xp]
|
||||||
# XP zum nächsten Level, Index 0 = Level 1 -> 2.
|
# Spiel startet auf Level 2. XP zum nächsten Level, Index 0 = Level 2 -> 3.
|
||||||
to_next = [2, 2, 6, 10, 20, 36, 60, 68, 68]
|
start_level = 2
|
||||||
|
to_next = [2, 6, 10, 20, 36, 60, 68, 68]
|
||||||
passive_per_round = 2
|
passive_per_round = 2
|
||||||
buy_amount = 4
|
buy_amount = 4
|
||||||
buy_cost = 4
|
buy_cost = 4
|
||||||
@@ -43,15 +44,15 @@ early_income = { "1-2" = 2, "1-3" = 2, "1-4" = 3, "2-1" = 4 }
|
|||||||
interest_per_10 = 1
|
interest_per_10 = 1
|
||||||
interest_cap = 5
|
interest_cap = 5
|
||||||
win_bonus = 1
|
win_bonus = 1
|
||||||
# Streak-Gold: [min_streak, gold]
|
# Streak-Gold: [min_streak, gold] — ab 2er-Streak +1 (tft-lab, op.gg)
|
||||||
streak_gold = [[3, 1], [5, 2], [6, 3]]
|
streak_gold = [[2, 1], [5, 2], [6, 3]]
|
||||||
starting_gold = 0
|
starting_gold = 0
|
||||||
|
|
||||||
[damage]
|
[damage]
|
||||||
# Spielerschaden = stage_base[stage] + per_surviving_unit * überlebende Units.
|
# Spielerschaden = stage_base[stage] + 1 pro überlebender Unit (flach, seit Set 11).
|
||||||
# TODO: gegen aktuelle Werte prüfen.
|
# Mehrquellig bestätigt: tft.ninja, op.gg, dotesports.
|
||||||
stage_base = [0, 0, 1, 2, 8, 15, 30, 40] # Index 0 = Stage 1
|
stage_base = [0, 2, 5, 8, 10, 12, 17, 17] # Index 0 = Stage 1
|
||||||
per_surviving_unit = 2
|
per_surviving_unit = 1
|
||||||
player_hp = 100
|
player_hp = 100
|
||||||
|
|
||||||
[rounds]
|
[rounds]
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ def round_income(gold: int, streak: int, won: bool, round_label: str, cfg: dict)
|
|||||||
|
|
||||||
|
|
||||||
def xp_to_next(level: int, cfg: dict) -> int | None:
|
def xp_to_next(level: int, cfg: dict) -> int | None:
|
||||||
to_next = cfg["xp"]["to_next"]
|
xp = cfg["xp"]
|
||||||
if level >= cfg["xp"]["max_level"]:
|
if level >= xp["max_level"]:
|
||||||
return None
|
return None
|
||||||
return to_next[level - 1]
|
return xp["to_next"][level - xp.get("start_level", 1)]
|
||||||
|
|
||||||
|
|
||||||
def apply_xp(level: int, xp: int, gained: int, cfg: dict) -> tuple[int, int]:
|
def apply_xp(level: int, xp: int, gained: int, cfg: dict) -> tuple[int, int]:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class Game:
|
|||||||
|
|
||||||
self.hp = cfg["damage"]["player_hp"]
|
self.hp = cfg["damage"]["player_hp"]
|
||||||
self.gold = cfg["gold"]["starting_gold"]
|
self.gold = cfg["gold"]["starting_gold"]
|
||||||
self.level = 1
|
self.level = cfg["xp"].get("start_level", 1)
|
||||||
self.xp = 0
|
self.xp = 0
|
||||||
self.board: list[dict] = []
|
self.board: list[dict] = []
|
||||||
self.bench: list[dict] = []
|
self.bench: list[dict] = []
|
||||||
@@ -133,7 +133,12 @@ class Game:
|
|||||||
u = units.pop(idx)
|
u = units.pop(idx)
|
||||||
copies = 3 ** (u["stars"] - 1)
|
copies = 3 ** (u["stars"] - 1)
|
||||||
self.pool.put_back(u["api_name"], copies)
|
self.pool.put_back(u["api_name"], copies)
|
||||||
self.gold += self.artifact["static"]["units"][u["api_name"]]["cost"] * copies
|
cost = self.artifact["static"]["units"][u["api_name"]]["cost"]
|
||||||
|
refund = cost * copies
|
||||||
|
# Ab 2 Sternen und Kosten >= 2 gilt -1 Gold; 1-Coster immer voller Wert.
|
||||||
|
if cost > 1 and u["stars"] >= 2:
|
||||||
|
refund -= 1
|
||||||
|
self.gold += refund
|
||||||
self.items.extend(u["items"])
|
self.items.extend(u["items"])
|
||||||
|
|
||||||
def reroll(self) -> None:
|
def reroll(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user