import json 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): units = [{"character_id": unit, "tier": 2, "itemNames": [item]}] traits = [{"name": "TFT17_T", "num_units": 2, "tier_current": trait_tier}] return (placement, json.dumps(units), json.dumps(traits), json.dumps([])) def test_strong_item_gets_multiplier_above_1(): rows = [board_row(1, item="TFT_Item_Good") for _ in range(200)] rows += [board_row(8, item="TFT_Item_Bad") for _ in range(200)] learned = learn(rows) assert learned["items"]["TFT_Item_Good"] > 1.05 assert learned["items"]["TFT_Item_Bad"] < 0.95 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)] 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(): rows = [board_row(2, trait_tier=3) for _ in range(50)] learned = learn(rows) assert "TFT17_T@3" in learned["traits"] 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([])) 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(): learned = learn([]) assert learned["traits"] == {} assert learned["pairs"] == {} assert learned["n_boards"] == 0 def test_unit_multipliers_learned(): def row(placement, unit): units = [{"character_id": unit, "tier": 2, "itemNames": []}] return (placement, json.dumps(units), json.dumps([]), json.dumps([])) rows = [row(1, "TFT17_Strong") for _ in range(150)] rows += [row(8, "TFT17_Weak") for _ in range(150)] learned = learn(rows) assert learned["units"]["TFT17_Strong"] > 1.05 assert learned["units"]["TFT17_Weak"] < 0.95 def test_augment_spec_builder(): from tft.model.augments import build_spec units = {"TFT17_Briar": {"name": "Briar", "cost": 1}} gold = build_spec({"desc": "Gain 6 gold now.", "effects": {"Gold": 6.0}}, units) assert gold["atoms"] == [{"kind": "gold_now", "amount": 6}] buff = build_spec({"desc": "Your team gains 35 Health and 10% Attack Speed.", "effects": {"Health": 35.0, "AS": 0.10}}, units) kinds = {(a["kind"], a.get("stat")) for a in buff["atoms"]} assert ("team_buff", "hp_flat") in kinds assert ("team_buff", "as_pct") in kinds cond = build_spec({"desc": "Your team gains 10 Health for each item.", "effects": {"Health": 10.0}}, units) assert not any(a["kind"] == "team_buff" for a in cond["atoms"]) unit = build_spec({"desc": "Gain a Briar and 2 gold.", "effects": {"Gold": 2.0}}, units) assert {"kind": "unit_grant", "units": ["TFT17_Briar"]} in unit["atoms"] 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