This commit is contained in:
team3
2026-09-07 07:44:09 +02:00
parent 848c8ea1aa
commit e12c2595fc
69 changed files with 7080 additions and 1165 deletions

View File

@@ -1,6 +1,9 @@
import json
from tft.model.learn import learn
from tft.model.learn import MEAN_PLACEMENT, learn
# Bester Fall: alle Boards auf Platz 1, also 3.5 Plätze über dem Schnitt.
MEAN_PLACEMENT_DELTA = MEAN_PLACEMENT - 1
def board_row(placement, unit="TFT17_A", item="TFT_Item_X", trait_tier=1):
@@ -18,9 +21,19 @@ def test_strong_item_gets_multiplier_above_1():
def test_shrinkage_with_thin_data():
"""Drei Boards dürfen den Multiplikator kaum bewegen.
Der Vergleich läuft gegen DELTA_SCALE, nicht gegen eine feste Zahl:
3 gegen 30 Pseudo-Boards lassen höchstens ein Zehntel des vollen
Effekts durch, egal wie stark die Skala eingestellt ist.
"""
from tft.model.learn import DELTA_SCALE
rows = [board_row(1, item="TFT_Item_Rare") for _ in range(3)]
learned = learn(rows)
assert abs(learned["items"]["TFT_Item_Rare"] - 1.0) < 0.06
thin = learn(rows)["items"]["TFT_Item_Rare"]
full = learn([board_row(1, item="TFT_Item_Rare") for _ in range(2000)])
assert abs(thin - 1.0) < 0.1 * MEAN_PLACEMENT_DELTA * DELTA_SCALE
assert full["items"]["TFT_Item_Rare"] > thin
def test_trait_keys_match_score_format():
@@ -29,20 +42,38 @@ def test_trait_keys_match_score_format():
assert "TFT17_T@3" in learned["traits"]
def test_pair_lift_for_cooccurring_units():
def pair_row(placement):
units = [
{"character_id": "TFT17_A", "tier": 2, "itemNames": []},
{"character_id": "TFT17_B", "tier": 2, "itemNames": []},
]
def test_pair_surplus_finds_real_synergy():
"""Zwei Units, die einzeln mittelmäßig sind, aber zusammen gewinnen."""
def row(placement, *ids):
units = [{"character_id": i, "tier": 1, "itemNames": []} for i in ids]
return (placement, json.dumps(units), json.dumps([]), json.dumps([]))
# A und B stehen immer zusammen in Top-4-Boards; C ist überall.
rows = [pair_row(1) for _ in range(100)]
solo = [{"character_id": "TFT17_C", "tier": 1, "itemNames": []}]
rows += [(1, json.dumps(solo), json.dumps([]), json.dumps([])) for _ in range(100)]
learned = learn(rows)
assert learned["pairs"]["TFT17_A|TFT17_B"] > 0.5
rows = []
# A und B einzeln mit Füllern: Mittelfeld.
rows += [row(4, "A", "X") for _ in range(120)]
rows += [row(5, "A", "Y") for _ in range(120)]
rows += [row(4, "B", "X") for _ in range(120)]
rows += [row(5, "B", "Y") for _ in range(120)]
# Zusammen: Platz 1.
rows += [row(1, "A", "B") for _ in range(120)]
pairs = learn(rows)["pairs"]
assert pairs["A|B"]["surplus"] > pairs["A|X"]["surplus"]
assert pairs["A|B"]["surplus"] > 0
def test_pair_surplus_cannot_separate_units_that_never_split():
"""Bekannte Grenze: stehen zwei Units immer zusammen, zählt der
Einzelwert beide Male dieselbe Stärke — der Überschuss kippt ins Minus.
In echten Daten fällt das kaum ins Gewicht: nur 9 von 1784 Paaren
hängen zu über 50 % aneinander, der Median liegt bei 2 %.
"""
def row(placement):
units = [{"character_id": i, "tier": 1, "itemNames": []} for i in ("A", "B")]
return (placement, json.dumps(units), json.dumps([]), json.dumps([]))
pairs = learn([row(1) for _ in range(120)])["pairs"]
assert pairs["A|B"]["surplus"] < 0
def test_empty_rows():
@@ -86,3 +117,62 @@ def test_augment_spec_builder():
junk = build_spec({"desc": "", "effects": {}}, units)
assert junk["offerable"] is False
def test_conditional_clause_does_not_leak_into_atoms():
"""Nur der unbedingte Satz zählt — effects["Gold"] ist der bedingte Nachschlag."""
from tft.model.augments import build_spec
units = {}
survivor = build_spec(
{"desc": "Gain 4 gold. After 3 players are eliminated, gain 100 more gold.",
"effects": {"Gold": 100.0, "InitialGold": 4.0, "PlayersDead": 3.0}}, units)
assert survivor["atoms"] == [{"kind": "gold_now", "amount": 4}]
avenge = build_spec(
{"desc": "After 3 allies die, your team gains 28% Attack Damage.",
"effects": {"AD": 28.0}}, units)
assert avenge["atoms"] == []
# Wiederkehrend, aber unbedingt: bleibt erhalten.
each_round = build_spec(
{"desc": "Your team gains 2% Attack Damage per level. "
"Gain 2 XP each round for the rest of the game.", "effects": {}}, units)
assert each_round["atoms"] == [{"kind": "xp_per_round", "amount": 2}]
def test_pair_surplus_isolates_the_synergy():
"""Ein Paar zählt nur, was es über seine zwei Units hinaus bringt.
A ist stark, B schwach. Zusammen laufen sie genau so, wie die Summe
erwarten lässt — also kein Überschuss.
"""
from tft.model.learn import pair_surplus, value
a_only = [1] * 100
b_only = [8] * 100
together = [1, 8] * 50
units = {"A": value(a_only + together[::2]), "B": value(b_only + together[1::2])}
pairs = pair_surplus({("A", "B"): together}, units)
assert abs(pairs["A|B"]["surplus"]) < 0.5
assert pairs["A|B"]["boards"] == 100
def test_pair_surplus_needs_enough_boards():
from tft.model.learn import PAIR_MIN_N, pair_surplus, value
units = {"A": 0.0, "B": 0.0}
thin = pair_surplus({("A", "B"): [1] * (PAIR_MIN_N - 1)}, units)
thick = pair_surplus({("A", "B"): [1] * PAIR_MIN_N}, units)
assert thin == {}
assert "A|B" in thick
# Unbekannte Units fliegen raus statt still als 0 zu zählen.
assert pair_surplus({("A", "X"): [1] * PAIR_MIN_N}, units) == {}
def test_value_shrinks_towards_average():
from tft.model.learn import value
assert value([1] * 3) < value([1] * 300) # wenige Boards -> naher an 0
assert value([8] * 300) < 0 < value([1] * 300)
assert abs(value([])) < 1e-9