This commit is contained in:
team3
2026-07-23 11:54:46 +02:00
parent f7e569e519
commit f41ac76b57
12 changed files with 331 additions and 386 deletions

View File

@@ -0,0 +1,92 @@
import random
from tft.sim import combat
def prof(off, dfn, defensive=True, name="u"):
return {"off": off, "def": dfn, "defensive": defensive,
"unit": {"api_name": name, "stars": 1, "items": []}}
CFG = {
"combat": {"max_frames": 1800},
"damage": {"stage_base": [0, 2, 5, 8, 10, 12, 17, 17],
"per_surviving_unit": 1, "player_hp": 100},
}
def test_kill_time_matches_dps_at_60fps():
# Def 10 gegen Gesamt-Off 5 -> 2 s = 120 Frames.
a = [prof(5, 1000)]
b = [prof(0, 10)]
res = combat.fight(a, b, random.Random(1), CFG)
assert res.winner == "a"
assert res.frames == 120
assert res.survivors_b == []
def test_overflow_carries_to_next_unit():
# Zwei Units à 10 Def gegen Off 5 -> exakt 4 s, kein Schaden verpufft.
a = [prof(5, 1000)]
b = [prof(0, 10), prof(0, 10)]
res = combat.fight(a, b, random.Random(1), CFG)
assert res.winner == "a"
assert res.frames == 240
def test_dead_units_stop_contributing_offense():
# B killt As Front nach 2 s; danach kämpft A nur noch mit halber Offense.
a = [prof(6, 12, defensive=True), prof(6, 1000, defensive=False)]
b = [prof(6, 60)]
res = combat.fight(a, b, random.Random(1), CFG)
assert res.winner == "a"
assert res.frames == 480 # 2 s + 36/6 s statt 60/12 s bei konstanter Offense
assert len(res.survivors_a) == 1
def test_queue_targets_defensive_units_first():
team = [prof(1, 1, defensive=False, name="carry1"),
prof(1, 1, defensive=True, name="tank1"),
prof(1, 1, defensive=False, name="carry2"),
prof(1, 1, defensive=True, name="tank2")]
q = combat._queue(team, random.Random(5))
assert [p["defensive"] for p in q] == [True, True, False, False]
def test_simultaneous_last_kill_is_draw():
a = [prof(5, 10)]
b = [prof(5, 10)]
res = combat.fight(a, b, random.Random(1), CFG)
assert res.winner is None
assert res.survivors_a == [] and res.survivors_b == []
def test_empty_boards():
res = combat.fight([], [], random.Random(1), CFG)
assert res.winner is None and res.frames == 0
res = combat.fight([prof(1, 1)], [], random.Random(1), CFG)
assert res.winner == "a"
def test_stall_hits_time_limit_more_defense_wins():
a = [prof(0, 100)]
b = [prof(0, 50)]
res = combat.fight(a, b, random.Random(1), CFG)
assert res.winner == "a"
assert res.frames == CFG["combat"]["max_frames"]
def test_same_seed_same_result():
team_a = [prof(7, 300), prof(12, 150, defensive=False), prof(5, 400)]
team_b = [prof(9, 250), prof(10, 200, defensive=False)]
r1 = combat.fight(team_a, team_b, random.Random(42), CFG)
r2 = combat.fight(team_a, team_b, random.Random(42), CFG)
assert (r1.winner, r1.frames) == (r2.winner, r2.frames)
assert [p["api_name"] for p in r1.survivors_a] == \
[p["api_name"] for p in r2.survivors_a]
def test_player_damage_counts_exact_survivors():
survivors = [{"api_name": f"u{i}"} for i in range(4)]
assert combat.player_damage(3, survivors, CFG) == 5 + 4
assert combat.player_damage(1, [], CFG) == 0

View File

@@ -173,17 +173,24 @@ def test_pairing():
def test_ghost_source_takes_no_damage(art, cfg):
game = Game(art, cfg, seed=3)
game.bots[0].hp = 0
game.bots[0].placement = 8
game.bots[0].board, game.bots[0].bench = [], []
# 7 Lebende -> jede PvP-Runde hat einen Ghost-Kampf; HP-Summe der
# Ghost-Quelle darf nur durch ihren echten Kampf sinken (kein Doppelschaden).
hp_before = {p.name: p.hp for p in game.players}
while game.round["kind"] != "pvp":
game.step()
# Auf 3 Lebende reduzieren -> genau 1 Paar + 1 Ghost-Kampf.
for b in game.bots[2:]:
b.hp, b.placement, b.board, b.bench = 0, 8, [], []
boards = [
(game.player, [{"api_name": "U5_0", "stars": 3, "items": []}]),
(game.bots[0], [{"api_name": "U3_0", "stars": 2, "items": []}]),
(game.bots[1], [{"api_name": "U1_0", "stars": 1, "items": []}]),
]
for p, board in boards:
p.board, p.bench, p.gold = board, [], 0
game.idx = next(i for i, r in enumerate(game.rounds) if r["kind"] == "pvp")
hp_before = {p.name: p.hp for p in game._alive()}
game.step()
drops = sum(1 for p in game.players if p.hp < hp_before[p.name])
assert drops <= 4 # max. 3 Paar-Verlierer + 1 Ghost-Verlierer
# Nur Paar-Verlierer + ggf. Ghost-Verlierer nehmen Schaden; die
# Ghost-Quelle darf durch ihren Klon-Kampf nicht getroffen werden.
drops = sum(1 for p in game.players if p.name in hp_before
and p.hp < hp_before[p.name])
assert drops <= 2
def test_pve_freezes_streak_and_pays_no_win_bonus(art, cfg):
@@ -279,13 +286,3 @@ def test_augment_effects_apply(art, cfg):
game.pick_augment(0)
assert len(game.player.bench) == bench_before + 2
assert sum(game.pool.available(a) for a in game.pool.copies) == pool_before - 2
def test_team_buff_augment_raises_score(art, cfg):
from tft.model.score import score_mechanical
buffed_art = {**art, "augment_specs": {
"AUG_BUFF": {"atoms": [{"kind": "team_buff", "stat": "hp_pct", "value": 0.3}],
"offerable": True}}}
board = [{"api_name": "U2_0", "stars": 2, "items": []}]
assert score_mechanical(board, ["AUG_BUFF"], buffed_art) > \
score_mechanical(board, [], buffed_art)

View File

@@ -2,7 +2,7 @@ import pytest
from tft.model.baseline import classify_role, unit_value
from tft.model.calibrate import spearman
from tft.model.score import active_trait_tiers, score_board
from tft.model.score import active_trait_tiers, board_profiles, score_board
STATIC = {
"units": {
@@ -28,8 +28,15 @@ STATIC = {
},
}
ARTIFACT = {"meta": {"set": 17}, "static": {**STATIC, "items": {}, "augments": {}},
"roles": {}, "learned": {}}
ARTIFACT = {
"meta": {"set": 17},
"static": {
**STATIC,
"items": {"TFT_Item_InfinityEdge": {"effects": {"AD": 0.35, "CritChance": 35.0}}},
"augments": {},
},
"roles": {},
}
def test_unit_value():
@@ -57,19 +64,30 @@ def test_score_ordering():
assert score_board(strong, [], ARTIFACT) > score_board(weak, [], ARTIFACT)
def test_active_trait_beats_inactive():
pair = [{"api_name": "TFT17_A", "stars": 1, "items": []},
{"api_name": "TFT17_B", "stars": 1, "items": []}]
solo_sum = score_board(pair[:1], [], ARTIFACT) + score_board(pair[1:], [], ARTIFACT)
assert score_board(pair, [], ARTIFACT) > solo_sum
def test_items_increase_score():
bare = [{"api_name": "TFT17_B", "stars": 1, "items": []}]
with_item = [{"api_name": "TFT17_B", "stars": 1, "items": ["TFT_Item_InfinityEdge"]}]
assert score_board(with_item, [], ARTIFACT) > score_board(bare, [], ARTIFACT)
def test_augments_ignored():
board = [{"api_name": "TFT17_B", "stars": 1, "items": []}]
assert score_board(board, ["TFT17_Augment_X"], ARTIFACT) == \
score_board(board, [], ARTIFACT)
def test_board_profiles_roles_and_unknown_units():
board = [{"api_name": "TFT17_A", "stars": 1, "items": []},
{"api_name": "TFT17_B", "stars": 1, "items": []},
{"api_name": "UNKNOWN", "stars": 1, "items": []}]
profiles = board_profiles(board, ARTIFACT)
assert len(profiles) == 2 # unbekannte Units fallen raus
tank, carry = profiles
assert tank["defensive"] and not carry["defensive"]
assert tank["def"] > carry["def"]
assert carry["off"] > tank["off"]
def test_spearman():
assert spearman([1, 2, 3, 4], [10, 20, 30, 40]) == pytest.approx(1.0)
assert spearman([1, 2, 3, 4], [40, 30, 20, 10]) == pytest.approx(-1.0)
@@ -88,39 +106,7 @@ def test_craftable_item_pool():
assert craftable_items(items) == ["TFT_Item_InfinityEdge"]
def test_stat_mults_rank_within_cost():
from tft.model.baseline import compute_stat_mults
def unit(hp, armor, ad, aspd):
return {"cost": 2, "traits": [], "role": "Tank",
"stats": {"hp": hp, "armor": armor, "magicResist": armor,
"damage": ad, "attackSpeed": aspd, "mana": 60,
"initialMana": 0, "range": 1}}
units = {"tanky": unit(900, 60, 45, 0.6), "avg": unit(700, 40, 55, 0.7),
"weak": unit(550, 25, 45, 0.6)}
mults = compute_stat_mults(units)
assert mults["tanky"] > mults["avg"] > mults["weak"]
assert all(0.9 <= m <= 1.1 for m in mults.values())
def test_stat_mults_identical_units_are_neutral():
from tft.model.baseline import compute_stat_mults
from tests.test_sim import make_static
mults = compute_stat_mults(make_static()["units"])
assert all(m == 1.0 for m in mults.values())
def test_stat_mult_raises_score_legacy():
from tft.model.score import score_board_legacy as score_board
art = {**ARTIFACT, "stat_mults": {"TFT17_B": 1.1}}
board = [{"api_name": "TFT17_B", "stars": 1, "items": []}]
assert score_board(board, [], art) > score_board(board, [], ARTIFACT)
def _mech_artifact():
def _built_artifact():
from tft.model import artifact as artifact_mod
static = {
@@ -143,30 +129,18 @@ def _mech_artifact():
return artifact_mod.build(static)
def test_mechanical_balanced_beats_lopsided():
from tft.model.score import score_mechanical
art = _mech_artifact()
def test_balanced_beats_lopsided():
art = _built_artifact()
balanced = [{"api_name": "TANK", "stars": 1, "items": []},
{"api_name": "CARRY", "stars": 1, "items": []}]
tanks = [{"api_name": "TANK", "stars": 1, "items": []} for _ in range(2)]
assert score_mechanical(balanced, [], art) > score_mechanical(tanks, [], art)
assert score_board(balanced, [], art) > score_board(tanks, [], art)
def test_mechanical_items_and_stars_raise_strength():
from tft.model.score import score_mechanical
art = _mech_artifact()
def test_items_and_stars_raise_strength():
art = _built_artifact()
base = [{"api_name": "CARRY", "stars": 1, "items": []}]
starred = [{"api_name": "CARRY", "stars": 2, "items": []}]
equipped = [{"api_name": "CARRY", "stars": 1, "items": ["SWORD"]}]
assert score_mechanical(starred, [], art) > score_mechanical(base, [], art)
assert score_mechanical(equipped, [], art) > score_mechanical(base, [], art)
def test_mechanical_recognized_trait_buffs_stats():
from tft.model.score import score_mechanical
art = _mech_artifact()
pair = [{"api_name": "TANK", "stars": 1, "items": []},
{"api_name": "CARRY", "stars": 1, "items": []}]
solo_sum = (score_mechanical(pair[:1], [], art)
+ score_mechanical(pair[1:], [], art))
assert score_mechanical(pair, [], art) > solo_sum
assert score_board(starred, [], art) > score_board(base, [], art)
assert score_board(equipped, [], art) > score_board(base, [], art)