179 lines
6.9 KiB
Python
179 lines
6.9 KiB
Python
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
|