198 lines
7.7 KiB
Python
198 lines
7.7 KiB
Python
"""Gruppen: Kontrolle nach Level und Item-Menge, Schwellen, Schlüsselformat."""
|
|
|
|
from tft.model import groups
|
|
|
|
|
|
def board(place, level, *units):
|
|
"""units: (api_name, itemzahl) — Sterne sind hier immer 2."""
|
|
return {"place": place, "level": level,
|
|
"items": sum(n for _, n in units),
|
|
"entities": sorted((api, 2, n) for api, n in units)}
|
|
|
|
|
|
def test_baseline_splits_by_level_and_items():
|
|
boards = [board(1, 9, ("A", 3)), board(3, 9, ("B", 3)),
|
|
board(7, 5, ("A", 0)), board(8, 5, ("B", 0))]
|
|
base = groups.baseline(boards)
|
|
assert base[(9, 3)] == 2.0
|
|
assert base[(5, 0)] == 7.5
|
|
|
|
|
|
def test_edge_measures_against_comparable_boards():
|
|
"""Zwei Boards auf Level 9 mit gleichen Items: nur der Unterschied zählt."""
|
|
boards = ([board(1, 9, ("A", 3), ("B", 3)) for _ in range(40)]
|
|
+ [board(7, 9, ("C", 3), ("D", 3)) for _ in range(40)])
|
|
base = groups.baseline(boards)
|
|
table = groups.group_edge(boards, 2, base, min_boards=10)
|
|
assert table["A@2@3|B@2@3"]["edge"] > 0
|
|
assert table["C@2@3|D@2@3"]["edge"] < 0
|
|
assert table["A@2@3|B@2@3"]["boards"] == 40
|
|
|
|
|
|
def test_item_count_is_part_of_the_key():
|
|
"""Derselbe Champion mit und ohne Items ist nicht dieselbe Einheit.
|
|
|
|
Beide Boards tragen sechs Items — nur anders verteilt. Genau dafür ist
|
|
die Kontrolle da: die Frage ist, wer die Items tragen soll.
|
|
"""
|
|
boards = ([board(1, 9, ("A", 3), ("B", 3), ("C", 0)) for _ in range(40)]
|
|
+ [board(8, 9, ("A", 0), ("B", 0), ("C", 3), ("D", 3)) for _ in range(40)])
|
|
base = groups.baseline(boards)
|
|
table = groups.group_edge(boards, 2, base, min_boards=10)
|
|
assert "A@2@3|B@2@3" in table and "A@2@0|B@2@0" in table
|
|
assert table["A@2@3|B@2@3"]["edge"] > table["A@2@0|B@2@0"]["edge"]
|
|
|
|
|
|
def test_rare_groups_are_dropped():
|
|
boards = [board(1, 9, ("A", 3), ("B", 3)) for _ in range(9)]
|
|
base = groups.baseline(boards)
|
|
assert groups.group_edge(boards, 2, base, min_boards=10) == {}
|
|
|
|
|
|
def test_groups_smaller_than_size_are_skipped():
|
|
boards = [board(1, 9, ("A", 3)) for _ in range(40)]
|
|
base = groups.baseline(boards)
|
|
assert groups.group_edge(boards, 2, base, min_boards=10) == {}
|
|
|
|
|
|
def test_build_needs_enough_boards():
|
|
import json
|
|
|
|
class Conn:
|
|
"""Minimaler sqlite-Ersatz: execute(...).fetchall()."""
|
|
|
|
def __init__(self, n):
|
|
row = (1, 9, json.dumps([{"character_id": "A", "tier": 2,
|
|
"itemNames": []}]))
|
|
self.rows = [row] * n
|
|
|
|
def execute(self, *_):
|
|
return self
|
|
|
|
def fetchall(self):
|
|
return self.rows
|
|
|
|
static = {"A": {"cost": 1, "traits": []}}
|
|
assert groups.build(Conn(10), 18, static) == {}
|
|
out = groups.build(Conn(groups.MIN_BOARDS * 20), 18, static)
|
|
assert out["boards"] == groups.MIN_BOARDS * 20
|
|
|
|
|
|
def test_only_groups_with_exactly_one_carry():
|
|
"""Ein Champion trägt drei Items, der Rest stützt."""
|
|
assert groups.one_carry("A@2@3|B@2@0")
|
|
assert groups.one_carry("A@2@3|B@1@1|C@3@2")
|
|
assert not groups.one_carry("A@2@3|B@2@3") # zwei Carries
|
|
assert not groups.one_carry("A@2@1|B@2@0") # gar keiner
|
|
|
|
|
|
def test_build_keeps_only_one_carry_groups():
|
|
import json
|
|
|
|
class Conn:
|
|
def __init__(self, rows):
|
|
self.rows = rows
|
|
|
|
def execute(self, *_):
|
|
return self
|
|
|
|
def fetchall(self):
|
|
return self.rows
|
|
|
|
def raw(place, *pairs):
|
|
units = [{"character_id": api, "tier": 1, "itemNames": ["x"] * n}
|
|
for api, n in pairs]
|
|
return (place, 9, json.dumps(units))
|
|
|
|
static = {a: {"cost": 1, "traits": []} for a in "ABCD"}
|
|
rows = ([raw(1, ("A", 3), ("B", 0))] * (groups.MIN_BOARDS * 12)
|
|
+ [raw(2, ("C", 3), ("D", 3))] * (groups.MIN_BOARDS * 12))
|
|
out = groups.build(Conn(rows), 18, static)
|
|
assert "C@1@3|D@1@3" not in out["2"]
|
|
assert "A@1@3|B@1@0" in out["2"]
|
|
|
|
|
|
def carrier(place, *pairs):
|
|
"""pairs: (api, sterne, [items]) — ein Board mit Trägern."""
|
|
return {"place": place, "level": 9, "items": 3, "_edge": 4.5 - place,
|
|
"carried": [(api, st, min(len(items), 3), set(items))
|
|
for api, st, items in pairs]}
|
|
|
|
|
|
def test_only_craftable_items_are_ranked():
|
|
"""Embleme, Artefakte und Radiant-Varianten kann man nicht planen."""
|
|
boards = [carrier(1, ("T", 2, ["I_NORMAL", "I_EMBLEM"]))] * 200
|
|
table = groups.unit_item_edge(boards, {"I_NORMAL"}, min_boards=10)
|
|
assert set(table) == {"T|I_NORMAL"}
|
|
|
|
|
|
def test_item_value_is_measured_against_the_same_champion():
|
|
"""Ein gutes Board macht nicht jedes Item darauf gut.
|
|
|
|
Beide Items liegen auf derselben Unit, aber nur eines auf den guten
|
|
Boards. Das andere darf davon nicht profitieren.
|
|
"""
|
|
boards = ([carrier(1, ("T", 2, ["GOOD"])) for _ in range(60)]
|
|
+ [carrier(7, ("T", 2, ["BAD"])) for _ in range(60)])
|
|
t = groups.unit_item_edge(boards, {"GOOD", "BAD"}, min_boards=10)
|
|
assert t["T|GOOD"]["edge"] > 0 > t["T|BAD"]["edge"]
|
|
|
|
|
|
def test_stars_and_item_count_are_the_comparison_group():
|
|
"""Ein Item darf nicht gut aussehen, nur weil sein Träger der Carry ist.
|
|
|
|
Der Champion steht mit drei Items auf guten Boards, mit einem auf
|
|
schlechten. Beide Items liegen nur auf je einer der beiden Gruppen —
|
|
ohne die Kontrolle gewänne allein das Item der Drei-Item-Boards.
|
|
"""
|
|
boards = ([carrier(2, ("T", 2, ["A", "X", "Y"])) for _ in range(80)]
|
|
+ [carrier(7, ("T", 2, ["B"])) for _ in range(80)])
|
|
t = groups.unit_item_edge(boards, {"A", "B"}, min_boards=10)
|
|
assert abs(t["T|A"]["edge"]) < 0.05 and abs(t["T|B"]["edge"]) < 0.05
|
|
|
|
|
|
def test_items_are_kept_apart_per_champion():
|
|
"""Dieselbe Rolle, anderer Champion: die Werte dürfen nicht verschmelzen."""
|
|
boards = ([carrier(1, ("M", 2, ["AD_ITEM"])) for _ in range(60)]
|
|
+ [carrier(1, ("N", 2, ["AP_ITEM"])) for _ in range(60)])
|
|
t = groups.unit_item_edge(boards, {"AD_ITEM", "AP_ITEM"}, min_boards=10)
|
|
assert set(t) == {"M|AD_ITEM", "N|AP_ITEM"}
|
|
|
|
|
|
def test_role_of_maps_riot_names():
|
|
assert groups.role_of({"role": "MagicTank"}) == "Tank"
|
|
assert groups.role_of({"role": "AttackFighter"}) == "Bruiser"
|
|
assert groups.role_of({"role": "MagicCaster"}) == "Mage"
|
|
assert groups.role_of({"role": "AttackMarksman"}) == "Scharfschütze"
|
|
assert groups.role_of({"role": ""}) is None
|
|
|
|
|
|
def test_unit_edge_uses_one_star_level():
|
|
"""Ohne feste Sternstufe gewinnt die billige Unit, die nur als 3★ überlebt."""
|
|
boards = ([{"place": 1, "level": 9, "items": 0,
|
|
"entities": [("CHEAP", 3, 0)]} for _ in range(60)]
|
|
+ [{"place": 5, "level": 9, "items": 0,
|
|
"entities": [("CHEAP", 2, 0), ("RICH", 2, 0)]} for _ in range(60)])
|
|
base = groups.baseline(boards)
|
|
two = groups.unit_edge(boards, base, min_boards=10, stars=2)
|
|
three = groups.unit_edge(boards, base, min_boards=10, stars=3)
|
|
# Auf 2★ zählen nur die schlechten Boards, auf 3★ nur die guten.
|
|
assert set(two) == {"CHEAP", "RICH"}
|
|
assert set(three) == {"CHEAP"}
|
|
assert three["CHEAP"]["edge"] > two["CHEAP"]["edge"]
|
|
|
|
|
|
def test_thiefs_gloves_carriers_are_excluded():
|
|
"""Die Handschuhe würfeln ihren Inhalt — das hat niemand so gebaut.
|
|
|
|
Riot listet die gewürfelten Items daneben, als wären sie gelegt worden.
|
|
Hier stehen sie nur auf den guten Boards; ohne den Ausschluss würden
|
|
sie dadurch gut aussehen.
|
|
"""
|
|
boards = ([carrier(1, ("T", 2, ["DA_ThiefsGloves", "ZUFALL", "X"]))
|
|
for _ in range(200)]
|
|
+ [carrier(6, ("T", 2, ["ECHT", "X", "Y"])) for _ in range(200)])
|
|
t = groups.unit_item_edge(boards, {"ZUFALL", "ECHT", "X"}, min_boards=10)
|
|
assert "T|ZUFALL" not in t
|
|
assert "T|ECHT" in t
|