M7+M8: sim engine, combat, archetype bots, autoplay policies
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
81
backend/tests/test_game.py
Normal file
81
backend/tests/test_game.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import pytest
|
||||
|
||||
from tft.constants.loader import load_constants
|
||||
from tft.model import artifact as artifact_mod
|
||||
from tft.sim.autoplay import play_afk, play_econ
|
||||
from tft.sim.game import Game, InvalidAction
|
||||
from tests.test_sim import make_static
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def art():
|
||||
static = make_static()
|
||||
static["meta"]["patch"] = "test"
|
||||
return artifact_mod.build(static)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def cfg():
|
||||
return load_constants(17)
|
||||
|
||||
|
||||
def test_afk_game_terminates_last(art, cfg):
|
||||
placements = [play_afk(Game(art, cfg, seed=s)) for s in range(20)]
|
||||
assert all(1 <= p <= 8 for p in placements)
|
||||
assert sum(placements) / len(placements) > 6.5
|
||||
|
||||
|
||||
def test_econ_beats_afk(art, cfg):
|
||||
afk = [play_afk(Game(art, cfg, seed=s)) for s in range(30)]
|
||||
econ = [play_econ(Game(art, cfg, seed=1000 + s)) for s in range(30)]
|
||||
assert sum(econ) / len(econ) < sum(afk) / len(afk)
|
||||
|
||||
|
||||
def test_same_seed_same_outcome(art, cfg):
|
||||
assert play_econ(Game(art, cfg, seed=7)) == play_econ(Game(art, cfg, seed=7))
|
||||
|
||||
|
||||
def test_buy_and_merge(art, cfg):
|
||||
game = Game(art, cfg, seed=1)
|
||||
game.gold = 100
|
||||
api = next(a for a in game.shop if a)
|
||||
unit = {"api_name": api, "stars": 1, "items": []}
|
||||
game.bench = [dict(unit), dict(unit)]
|
||||
game.pool.take(api, 2)
|
||||
game.shop = [api] * 5
|
||||
game.buy(0)
|
||||
assert len(game.bench) == 1
|
||||
assert game.bench[0]["stars"] == 2
|
||||
|
||||
|
||||
def test_sell_refunds_pool_and_gold(art, cfg):
|
||||
game = Game(art, cfg, seed=1)
|
||||
game.gold = 100
|
||||
game.shop[0] = "U1_0"
|
||||
before_pool = game.pool.available("U1_0")
|
||||
game.buy(0)
|
||||
gold_after_buy = game.gold
|
||||
game.sell("bench", len(game.bench) - 1)
|
||||
assert game.gold == gold_after_buy + 1
|
||||
assert game.pool.available("U1_0") == before_pool
|
||||
|
||||
|
||||
def test_board_cap_is_level(art, cfg):
|
||||
game = Game(art, cfg, seed=1)
|
||||
game.bench = [{"api_name": "U1_0", "stars": 1, "items": []} for _ in range(3)]
|
||||
game.level = 2
|
||||
game.move("bench", 0)
|
||||
game.move("bench", 0)
|
||||
with pytest.raises(InvalidAction, match="board full"):
|
||||
game.move("bench", 0)
|
||||
|
||||
|
||||
def test_invalid_actions_raise(art, cfg):
|
||||
game = Game(art, cfg, seed=1)
|
||||
game.gold = 0
|
||||
with pytest.raises(InvalidAction):
|
||||
game.reroll()
|
||||
with pytest.raises(InvalidAction):
|
||||
game.buy_xp()
|
||||
with pytest.raises(InvalidAction):
|
||||
game.sell("bench", 0)
|
||||
96
backend/tests/test_sim.py
Normal file
96
backend/tests/test_sim.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from tft.constants.loader import load_constants
|
||||
from tft.sim import economy
|
||||
from tft.sim.pool import Pool
|
||||
from tft.sim.rounds import schedule
|
||||
from tft.sim.shop import roll
|
||||
|
||||
|
||||
def make_static(per_tier: int = 13) -> dict:
|
||||
units = {}
|
||||
for cost in range(1, 6):
|
||||
for i in range(per_tier):
|
||||
units[f"U{cost}_{i}"] = {
|
||||
"cost": cost,
|
||||
"traits": ["T1"],
|
||||
"role": "Tank",
|
||||
"stats": {"hp": 600 + 100 * cost, "armor": 40, "magicResist": 40,
|
||||
"damage": 55, "attackSpeed": 0.7, "mana": 60,
|
||||
"initialMana": 0, "range": 1},
|
||||
}
|
||||
return {
|
||||
"meta": {"set": 17},
|
||||
"units": units,
|
||||
"traits": {"T1": {"breakpoints": [{"min_units": 2, "style": 1}]}},
|
||||
"items": {f"TFT_Item_{i}": {} for i in range(10)},
|
||||
"augments": {f"TFT17_Augment_{i}": {} for i in range(8)},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def cfg():
|
||||
return load_constants(17)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def static():
|
||||
return make_static()
|
||||
|
||||
|
||||
def test_pool_take_put_back(static, cfg):
|
||||
pool = Pool(static["units"], cfg["pool"]["sizes"])
|
||||
assert pool.available("U1_0") == 30
|
||||
pool.take("U1_0", 3)
|
||||
assert pool.available("U1_0") == 27
|
||||
pool.put_back("U1_0", 3)
|
||||
assert pool.available("U1_0") == 30
|
||||
with pytest.raises(ValueError):
|
||||
pool.take("U5_0", 10)
|
||||
|
||||
|
||||
def test_shop_odds_empirical(static, cfg):
|
||||
pool = Pool(static["units"], cfg["pool"]["sizes"])
|
||||
rng = random.Random(42)
|
||||
counts = [0] * 5
|
||||
n = 10_000
|
||||
for _ in range(n // cfg["shop"]["slots"]):
|
||||
for api in roll(pool, 8, cfg, rng):
|
||||
counts[static["units"][api]["cost"] - 1] += 1
|
||||
expected = cfg["shop"]["odds"][7]
|
||||
for tier in range(5):
|
||||
assert counts[tier] / n == pytest.approx(expected[tier] / 100, abs=0.02)
|
||||
|
||||
|
||||
def test_interest_cap(cfg):
|
||||
assert economy.interest(0, cfg) == 0
|
||||
assert economy.interest(39, cfg) == 3
|
||||
assert economy.interest(80, cfg) == cfg["gold"]["interest_cap"]
|
||||
|
||||
|
||||
def test_streak_gold(cfg):
|
||||
assert economy.streak_gold(1, cfg) == 0
|
||||
assert economy.streak_gold(3, cfg) == 1
|
||||
assert economy.streak_gold(-5, cfg) == 2
|
||||
assert economy.streak_gold(9, cfg) == 3
|
||||
|
||||
|
||||
def test_apply_xp_levels_up(cfg):
|
||||
level, xp = economy.apply_xp(1, 0, 4, cfg)
|
||||
assert level == 3 # 1->2 costs 2, 2->3 costs 2
|
||||
assert xp == 0
|
||||
level, _ = economy.apply_xp(9, 0, 1000, cfg)
|
||||
assert level == cfg["xp"]["max_level"]
|
||||
|
||||
|
||||
def test_schedule(cfg):
|
||||
rounds = schedule(cfg)
|
||||
labels = [r["label"] for r in rounds]
|
||||
assert labels[:4] == ["1-1", "1-2", "1-3", "1-4"]
|
||||
assert rounds[0]["kind"] == "carousel"
|
||||
aug = [r["label"] for r in rounds if r["augment"]]
|
||||
assert aug == cfg["rounds"]["augment_rounds"]
|
||||
stage2 = [r for r in rounds if r["stage"] == 2]
|
||||
assert [r["kind"] for r in stage2] == cfg["rounds"]["stage_n"]
|
||||
Reference in New Issue
Block a user