Files
tft/backend/tft/sim/policy.py

142 lines
4.9 KiB
Python

"""Scripted planning policies — drive the bots and the autoplay benchmarks.
Level-Ziele und Rolldown-Zeitpunkte nach Standard-Spielweisen (Set 17):
Baseline L4@2-1, L5@2-5, L6@3-2, L7@4-1, L8@4-5. Fast 8 spiked bei 4-2,
Streaker levelt früh und rollt klein, Reroll cappt bei 6 und slow-rollt >50.
"""
import random
from tft.sim import player
from tft.sim.player import InvalidAction, PlayerState
from tft.sim.pool import Pool
ARCHETYPES = {
# roll_at_level: ab diesem Level (und Stage >= 4) wird auf roll_floor gerollt.
"fast8": {
"level_targets": {"2-1": 4, "2-5": 5, "3-2": 6, "4-1": 7, "4-2": 8, "5-2": 9},
"roll_at_level": 8,
"roll_floor": 30,
"roll_low_hp": 40,
"slow_roll_level": None,
},
"streaker": {
"level_targets": {"2-1": 4, "2-5": 5, "3-1": 6, "3-5": 7, "4-5": 8, "5-5": 9},
"roll_at_level": 7,
"roll_floor": 30,
"roll_low_hp": 50,
"slow_roll_level": None,
},
"reroll": {
"level_targets": {"2-1": 4, "2-5": 5, "3-2": 6, "5-1": 7, "6-1": 8},
"roll_at_level": None,
"roll_floor": 30,
"roll_low_hp": 40,
"slow_roll_level": 6, # ab Level 6: Gold über 50 verrollen (Slow-Roll)
},
}
def _round_key(label: str) -> tuple[int, int]:
stage, rnd = label.split("-")
return int(stage), int(rnd)
def _target_level(params: dict, label: str) -> int:
key = _round_key(label)
return max(
(lvl for rnd, lvl in params["level_targets"].items() if _round_key(rnd) <= key),
default=0,
)
def act(p: PlayerState, pool: Pool, artifact: dict, cfg: dict, rng: random.Random,
rnd: dict, params: dict) -> None:
"""Eine Planungsrunde für einen Spieler."""
if p.augment_offer:
player.pick_augment(p, rng.randrange(len(p.augment_offer)))
# Geplantes Leveln hat Vorrang vor Shop-Käufen ("on curve").
label = rnd["label"]
target = _target_level(params, label)
while p.level < target and p.gold >= cfg["xp"]["buy_cost"]:
try:
player.buy_xp(p, cfg)
except InvalidAction:
break
_buy_from_shop(p, pool, artifact)
floor = None
slow_level = params.get("slow_roll_level")
if slow_level is not None and p.level >= slow_level:
floor = 50
if p.level >= max(params["level_targets"].values()):
floor = 50 # Endstufe erreicht: Überschuss in Upgrades rollen
roll_level = params.get("roll_at_level")
if roll_level is not None and p.level >= roll_level and rnd["stage"] >= 4:
floor = params["roll_floor"]
if rnd["stage"] >= 4 and p.hp < params["roll_low_hp"]:
floor = 0 # Not-Rolldown bei niedrigem Leben
if floor is not None:
_sell_junk(p, pool, artifact)
while p.gold > floor:
_buy_upgrades(p, pool, artifact)
try:
player.reroll(p, pool, cfg, rng)
except InvalidAction:
break
_equip_items(p, artifact)
player.fill_board(p, artifact)
def _buy_from_shop(p: PlayerState, pool: Pool, artifact: dict) -> None:
for slot, api in enumerate(list(p.shop)):
if api is None:
continue
cost = artifact["static"]["units"][api]["cost"]
owned = sum(1 for u in p.board + p.bench
if u["api_name"] == api and u["stars"] == 1)
if p.gold >= cost and (owned >= 1 or len(p.bench) < 7):
try:
player.buy(p, slot, pool, artifact)
except InvalidAction:
pass
def _buy_upgrades(p: PlayerState, pool: Pool, artifact: dict) -> None:
"""Im Rolldown: Kopien für Upgrades UND neue starke Carries (4-Cost+) kaufen."""
for slot, api in enumerate(list(p.shop)):
if api is None:
continue
cost = artifact["static"]["units"][api]["cost"]
owned = sum(1 for u in p.board + p.bench
if u["api_name"] == api and u["stars"] < 3)
wants = owned >= 1 or (cost >= 4 and len(p.bench) < 7)
if p.gold >= cost and wants:
try:
player.buy(p, slot, pool, artifact)
except InvalidAction:
return
def _sell_junk(p: PlayerState, pool: Pool, artifact: dict) -> None:
"""Sell 1-star bench units that have no merge partner anywhere."""
for idx in range(len(p.bench) - 1, -1, -1):
u = p.bench[idx]
copies = sum(1 for o in p.board + p.bench
if o["api_name"] == u["api_name"] and o["stars"] == u["stars"])
if u["stars"] == 1 and copies == 1 and not u["items"]:
player.sell(p, "bench", idx, pool, artifact)
def _equip_items(p: PlayerState, artifact: dict) -> None:
while p.items and any(len(u["items"]) < 3 for u in p.board):
target = min(range(len(p.board)), key=lambda i: len(p.board[i]["items"]))
try:
player.equip(p, 0, target, artifact)
except InvalidAction:
break