Bot-Policies auf recherchierte Level-Timelines: Runden-Ziele, Rolldown-Runden, Slow-Roll, Not-Roll bei wenig HP
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -240,3 +240,16 @@ def test_loot_drops_components(art, cfg):
|
|||||||
comp_art = {**art, "component_pool": ["C_A", "C_B"]}
|
comp_art = {**art, "component_pool": ["C_A", "C_B"]}
|
||||||
game = Game(comp_art, cfg, seed=2)
|
game = Game(comp_art, cfg, seed=2)
|
||||||
assert all(i in ("C_A", "C_B") for i in game.items) # Carousel-Drop
|
assert all(i in ("C_A", "C_B") for i in game.items) # Carousel-Drop
|
||||||
|
|
||||||
|
|
||||||
|
def test_bot_leveling_follows_targets(art, cfg):
|
||||||
|
game = Game(art, cfg, seed=12)
|
||||||
|
levels_at = {}
|
||||||
|
while not game.over and game.round["label"] != "4-3":
|
||||||
|
label = game.round["label"]
|
||||||
|
if label in ("2-6", "3-3", "4-2"):
|
||||||
|
levels_at[label] = [b.level for b in game.bots if b.alive]
|
||||||
|
game.step()
|
||||||
|
assert all(lvl >= 5 for lvl in levels_at["2-6"]) # L5 ab 2-5
|
||||||
|
assert all(lvl >= 6 for lvl in levels_at["3-3"]) # L6 ab 3-1/3-2
|
||||||
|
assert any(lvl >= 7 for lvl in levels_at["4-2"]) # fast8/streaker auf 7
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
"""Scripted planning policies — drive the bots and the autoplay benchmarks."""
|
"""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
|
import random
|
||||||
|
|
||||||
@@ -7,18 +12,40 @@ from tft.sim.player import InvalidAction, PlayerState
|
|||||||
from tft.sim.pool import Pool
|
from tft.sim.pool import Pool
|
||||||
|
|
||||||
ARCHETYPES = {
|
ARCHETYPES = {
|
||||||
# fast8: früh econ, ab Stage 3 leveln, Rolldown ab Level 8.
|
"fast8": {
|
||||||
"fast8": {"level_cap": 10, "xp_stage": 3, "econ_floor": 54,
|
"level_targets": {"2-1": 4, "2-5": 5, "3-2": 6, "4-1": 7, "4-2": 8, "5-2": 9},
|
||||||
"roll_stage": 4, "roll_level": 8, "roll_floor": 30},
|
"roll_rounds": {"4-2": 30, "5-2": 20},
|
||||||
# reroll: Level-Cap 7, rollt Überschuss über 50 für Upgrades.
|
"roll_low_hp": 40,
|
||||||
"reroll": {"level_cap": 7, "xp_stage": 3, "econ_floor": 50,
|
"slow_roll_level": None,
|
||||||
"roll_stage": 3, "roll_level": 7, "roll_floor": 50},
|
},
|
||||||
# streaker: gibt früh aus, levelt aggressiv, wenig Reserve.
|
"streaker": {
|
||||||
"streaker": {"level_cap": 10, "xp_stage": 2, "econ_floor": 20,
|
"level_targets": {"2-1": 4, "2-5": 5, "3-1": 6, "3-5": 7, "4-5": 8, "5-5": 9},
|
||||||
"roll_stage": 4, "roll_level": 8, "roll_floor": 20},
|
"roll_rounds": {"4-1": 30, "4-5": 20},
|
||||||
|
"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_rounds": {},
|
||||||
|
"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,
|
def act(p: PlayerState, pool: Pool, artifact: dict, cfg: dict, rng: random.Random,
|
||||||
rnd: dict, params: dict) -> None:
|
rnd: dict, params: dict) -> None:
|
||||||
"""Eine Planungsrunde für einen Spieler."""
|
"""Eine Planungsrunde für einen Spieler."""
|
||||||
@@ -27,18 +54,28 @@ def act(p: PlayerState, pool: Pool, artifact: dict, cfg: dict, rng: random.Rando
|
|||||||
|
|
||||||
_buy_from_shop(p, pool, artifact)
|
_buy_from_shop(p, pool, artifact)
|
||||||
|
|
||||||
stage = rnd["stage"]
|
label = rnd["label"]
|
||||||
while (stage >= params["xp_stage"] and p.level < params["level_cap"]
|
target = _target_level(params, label)
|
||||||
and p.gold >= params["econ_floor"] + cfg["xp"]["buy_cost"]):
|
while p.level < target and p.gold >= cfg["xp"]["buy_cost"]:
|
||||||
try:
|
try:
|
||||||
player.buy_xp(p, cfg)
|
player.buy_xp(p, cfg)
|
||||||
except InvalidAction:
|
except InvalidAction:
|
||||||
break
|
break
|
||||||
|
|
||||||
at_target = p.level >= params["roll_level"] or p.level >= params["level_cap"]
|
floor = None
|
||||||
if stage >= params["roll_stage"] and at_target:
|
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
|
||||||
|
if label in params["roll_rounds"]:
|
||||||
|
floor = params["roll_rounds"][label]
|
||||||
|
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)
|
_sell_junk(p, pool, artifact)
|
||||||
while p.gold > params["roll_floor"]:
|
while p.gold > floor:
|
||||||
_buy_upgrades(p, pool, artifact)
|
_buy_upgrades(p, pool, artifact)
|
||||||
try:
|
try:
|
||||||
player.reroll(p, pool, cfg, rng)
|
player.reroll(p, pool, cfg, rng)
|
||||||
|
|||||||
Reference in New Issue
Block a user