update
This commit is contained in:
107
.planer/einheiten/backend__tft__api__app.py.md
Normal file
107
.planer/einheiten/backend__tft__api__app.py.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Einheiten: backend/tft/api/app.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/api/app.py::ActionRequest
|
||||
beschreibung: Definiert das Pydantic-Schema für eingehende Aktions-Requests einer TFT-Spielpartie.
|
||||
input: HTTP-Request-Body mit den Aktions-Feldern action, slot, where, idx, item_idx, board_idx, choice
|
||||
output: Ein ActionRequest-Modell mit validierten, optional typisierten Aktions-Parametern
|
||||
entscheidungen:
|
||||
- Sämtliche Aktions-Parameter außer action sind als optional deklariert
|
||||
beleg: "slot: int | None = None"
|
||||
- Die Modell-Klasse verwendet Pydantic-BaseModel zur automatischen Validierung
|
||||
beleg: "class ActionRequest(BaseModel):"
|
||||
kanten:
|
||||
|
||||
## backend/tft/api/app.py::health
|
||||
beschreibung: Stellt einen Health-Check-Endpunkt zur Verfügung, der den Betriebszustand des Dienstes meldet.
|
||||
input: HTTP-GET-Aufruf auf /health
|
||||
output: Ein JSON-Objekt mit status="ok"
|
||||
entscheidungen:
|
||||
- Der Endpunkt ist über den Pfad /health erreichbar
|
||||
beleg: "@app.get(\"/health\")"
|
||||
- Der Status wird statisch als "ok" zurückgegeben
|
||||
beleg: "return {\"status\": \"ok\"}"
|
||||
kanten:
|
||||
|
||||
## backend/tft/api/app.py::_load_game_deps
|
||||
beschreibung: Lädt die für eine Spielsitzung benötigten statischen Set-Daten und Konfigurationswerte.
|
||||
input: Keine Argumente; liest den Static-Pointer aus dem Dateisystem
|
||||
output: Ein Tupel aus geladenem Artifact-Set und Konfigurationswerten
|
||||
entscheidungen:
|
||||
- Bei fehlendem Static-Pointer wird ein HTTP-503-Fehler ausgelöst
|
||||
beleg: "raise HTTPException(503, \"no static data: run `tft.cli fetch-static` first\")"
|
||||
- Die benötigten Submodule werden lokal erst zur Laufzeit importiert
|
||||
beleg: "from tft.constants.loader import load_constants"
|
||||
kanten:
|
||||
|
||||
## backend/tft/api/app.py::new_game
|
||||
beschreibung: Erstellt eine neue TFT-Spiellauf-Instanz und registriert sie im globalen Spiellauf-Register.
|
||||
input: Optionaler Seed-Wert für die Initialisierung des Zufallsgenerators
|
||||
output: Ein JSON-Objekt mit der frisch vergebenen game_id und dem serialisierten Spielzustand
|
||||
entscheidungen:
|
||||
- Die Spiele werden in einem prozessglobalen Dictionary GAMES gehalten
|
||||
beleg: "GAMES[game_id] = Game(artifact, cfg, seed=seed)"
|
||||
- Die game_id ist eine 8-stelligen Hex-Kürzel einer UUID
|
||||
beleg: "game_id = uuid.uuid4().hex[:8]"
|
||||
kanten:
|
||||
|
||||
## backend/tft/api/app.py::state
|
||||
beschreibung: Liefert den serialisierten Spielzustand einer bestehenden Spiellauf-Instanz.
|
||||
input: Eine game_id aus der URL
|
||||
output: Ein JSON-Objekt mit dem Spielzustand oder ein 404-Fehler
|
||||
entscheidungen:
|
||||
- Unbekannte Spiele führen zu einem 404-Fehler statt zu einem leeren Zustand
|
||||
beleg: "raise HTTPException(404, \"unknown game\")"
|
||||
- Der Endpunkt ist als GET auf /api/game/{game_id} definiert
|
||||
beleg: "@app.get(\"/api/game/{game_id}\")"
|
||||
kanten:
|
||||
|
||||
## backend/tft/api/app.py::action
|
||||
beschreibung: Interpretiert eine Aktions-Anfrage und leitet sie an die passende Game-Methode weiter.
|
||||
input: Eine game_id sowie ein ActionRequest mit Aktionstyp und Parametern
|
||||
output: Ein serialisierter Spielzustand nach Ausführung oder ein 4xx-Fehler
|
||||
entscheidungen:
|
||||
- Unbekannte Aktionen werden als 400-Fehler abgewiesen
|
||||
beleg: "raise HTTPException(400, f\"unknown action {req.action}\")"
|
||||
- Ungültige Aktionen der Domäne werden als 400-Fehler übersetzt
|
||||
beleg: "except InvalidAction as e:"
|
||||
- Fehlende Parameter werden als 400-Fehler mit Hinweis auf die Aktion gemeldet
|
||||
beleg: "raise HTTPException(400, f\"missing parameters for action {req.action}\")"
|
||||
kanten:
|
||||
|
||||
## backend/tft/api/app.py::upgrade_hint
|
||||
beschreibung: Berechnet, in welche Sternstufe eine Einheit durch einen weiteren Kauf aufsteigen würde.
|
||||
input: Das Game-Objekt sowie der api_name einer Einheit
|
||||
output: 0 für kein Upgrade, 2 für einen 2-Sterne- oder 3 für einen 3-Sterne-Aufstieg
|
||||
entscheidungen:
|
||||
- Bereits 3-Sterne-Einheiten werden vom Upgrade-Pfad ausgeschlossen
|
||||
beleg: "if u[\"api_name\"] == api and u[\"stars\"] < 3"
|
||||
- Höhere Sterne zählen mit Stern-Eskalation als 3^(s-1) Kopien
|
||||
beleg: "sum(3 ** (u[\"stars\"] - 1) for u in owned)"
|
||||
kanten:
|
||||
|
||||
## backend/tft/api/app.py::unit_view
|
||||
beschreibung: Projiziert eine interne Einheit-Darstellung in die API-Antwortstruktur.
|
||||
input: Ein Game-Objekt sowie eine Einheit-Dictionary-Repräsentation
|
||||
output: Ein JSON-Objekt mit Anzeigename, Kosten, Sternen, Traits, Icon und getragenen Items
|
||||
entscheidungen:
|
||||
- Fehlende Anzeigenamen fallen auf den api_name zurück
|
||||
beleg: "\"name\": info.get(\"name\", u[\"api_name\"])"
|
||||
- Item-Namen und -Icons werden tolerant gegen fehlende Einträge ausgelesen
|
||||
beleg: "game.artifact[\"static\"][\"items\"].get(i, {}).get(\"name\", i)"
|
||||
kanten:
|
||||
|
||||
## backend/tft/api/app.py::serialize
|
||||
beschreibung: Erzeugt das vollständige JSON-Spielbild inklusive Brett, Bank, Shop, Traits und Gegnern.
|
||||
input: Ein Game-Objekt der laufenden Simulation
|
||||
output: Ein umfangreiches Dictionary, das alle UI-relevanten Spielzustandsdaten bündelt
|
||||
entscheidungen:
|
||||
- Die Trait-Liste wird absteigend nach Tier und Count sortiert
|
||||
beleg: "key=lambda x: (-x[\"tier\"], -x[\"count\"])"
|
||||
- Gegner werden absteigend nach HP sortiert dargestellt
|
||||
beleg: "key=lambda x: -x[\"hp\"]"
|
||||
- Das Log wird auf die letzten acht Einträge begrenzt
|
||||
beleg: "\"log\": game.log[-8:]"
|
||||
- Aktive Trait-Tiers werden aus Brett-Einheiten berechnet
|
||||
beleg: "tiers = active_trait_tiers(game.board, static[\"traits\"], static[\"units\"])"
|
||||
kanten:
|
||||
34
.planer/einheiten/backend__tft__cli.py.md
Normal file
34
.planer/einheiten/backend__tft__cli.py.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Einheiten: backend/tft/cli.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/cli.py::current_set
|
||||
beschreibung: Liefert die aktuell aktive TFT-Set-Nummer aus dem statischen Daten-Zeiger.
|
||||
input: keine Argumente; liest den vom Modul `tft.paths` bereitgestellten latest-Pointer
|
||||
output: `int` – die Set-Nummer aus dem gelesenen JSON
|
||||
entscheidungen:
|
||||
- Bricht hart ab, wenn noch keine statischen Daten existieren, statt einen Default zurückzugeben
|
||||
beleg: "raise SystemExit(\"no static data yet: run `fetch-static` first\")"
|
||||
- Bezieht den Set-Schlüssel ausschließlich aus dem Feld `set` des Pointer-JSONs
|
||||
beleg: "return json.loads(pointer.read_text())[\"set\"]"
|
||||
- Kapselt den Import von `tft.paths` lokal in der Funktion, um Modullade-Reihenfolge unabhängig zu halten
|
||||
beleg: "from tft import paths"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/cli.py::main
|
||||
|
||||
## backend/tft/cli.py::main
|
||||
beschreibung: Stellt die Kommandozeilen-Schnittstelle des `tft`-Tools bereit und verzweigt eingegebene Subcommands auf die passenden Pipeline-Funktionen.
|
||||
input: Prozess-Argumente aus `argparse` (Subcommand + Optionen wie `--set`, `--policy`, `--games`)
|
||||
output: Konsolen-Statusmeldungen bzw. Fehlerausgaben; bei `refresh` ruft es rekursiv Sub-Stages als Subprozess auf
|
||||
entscheidungen:
|
||||
- Erlaubt ausschließlich die registrierten Subcommands, indem `required=True` gesetzt wird
|
||||
beleg: "sub = parser.add_subparsers(dest=\"command\", required=True)"
|
||||
- Trennt Subcommand-Implementierungen per Lazy-Import innerhalb der `elif`-Zweige
|
||||
beleg: "if args.command == \"fetch-static\":"
|
||||
- Bricht den `refresh`-Lauf beim ersten nicht-null Returncode eines Subprozesses ab
|
||||
beleg: "if result.returncode != 0:"
|
||||
- Übergibt beim `autoplay`-Subcommand die Default-Politik `econ`, falls `--policy` fehlt
|
||||
beleg: "default=\"econ\""
|
||||
- Nutzt beim `calibrate`-Pfad strikt Holdout-Daten via `holdout_only=True`
|
||||
beleg: "holdout_only=True)"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/cli.py::main
|
||||
34
.planer/einheiten/backend__tft__constants__loader.py.md
Normal file
34
.planer/einheiten/backend__tft__constants__loader.py.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Einheiten: backend/tft/constants/loader.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/constants/loader.py::load_constants
|
||||
beschreibung: Lädt die TOML-Konstantendatei für ein gegebenes Set und gibt das eingelesene Dict zurück.
|
||||
input: set_number (int) – die zu ladende Set-Nummer.
|
||||
output: dict – die deserialisierten Konstanten aus der TOML-Datei.
|
||||
entscheidungen:
|
||||
- Fehlt die Set-Datei, bricht der Lader mit einer FileNotFoundError ab und gibt einen Hinweis zur Behebung.
|
||||
beleg: "if not path.exists():"
|
||||
- Die Datei wird im Binary-Modus geöffnet, da tomllib rohe Bytes erwartet.
|
||||
beleg: "with path.open(\"rb\") as f:"
|
||||
- Geladene Werte werden unmittelbar über validate einer Konsistenzprüfung unterzogen.
|
||||
beleg: "validate(c)"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/constants/loader.py::validate
|
||||
|
||||
## backend/tft/constants/loader.py::validate
|
||||
beschreibung: Validiert die Struktur und Plausibilität der geladenen Konstanten und wirft bei Verstößen ValueError.
|
||||
input: c (dict) – das zuvor aus TOML geladene Konstanten-Dict.
|
||||
output: None – signalisiert Gültigkeit durch stilles Zurückkehren oder Auslösen einer Ausnahme.
|
||||
entscheidungen:
|
||||
- Jede Shop-Odds-Zeile muss genau 5 Einträge besitzen.
|
||||
beleg: "if len(row) != 5:"
|
||||
- Die Pool-Größenliste muss exakt 5 Kostenstufen umfassen.
|
||||
beleg: "if len(c[\"pool\"][\"sizes\"]) != 5:"
|
||||
- Die Anzahl der XP-Stufen-Übergänge muss zur maximalen Stufe passen (to_next = max_level − 1).
|
||||
beleg: "if len(xp[\"to_next\"]) != xp[\"max_level\"] - 1:"
|
||||
- Die streak_gold-Tabelle muss aufsteigend nach Streak-Länge sortiert sein.
|
||||
beleg: "if streaks != sorted(streaks):"
|
||||
- Jede Shop-Odds-Zeile muss in Summe genau 100 ergeben.
|
||||
beleg: "if sum(row) != 100:"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/constants/loader.py::load_constants
|
||||
17
.planer/einheiten/backend__tft__db.py.md
Normal file
17
.planer/einheiten/backend__tft__db.py.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Einheiten: backend/tft/db.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/db.py::connect
|
||||
beschreibung: Stellt eine SQLite-Verbindung zur Match-Datenbank her und initialisiert das Schema.
|
||||
input: impliziter Datenbankpfad aus tft.paths.MATCHES_DB
|
||||
output: eine geöffnete sqlite3.Connection mit ausgeführtem SCHEMA
|
||||
entscheidungen:
|
||||
- Das Datenverzeichnis wird vor dem Verbindungsaufbau angelegt, falls es fehlt.
|
||||
beleg: "paths.DATA.mkdir(parents=True, exist_ok=True)"
|
||||
- Das Schema wird bei jeder Verbindung idempotent per executescript angewendet.
|
||||
beleg: "conn.executescript(SCHEMA)"
|
||||
- Die Tabellen nutzen CREATE TABLE IF NOT EXISTS, wodurch Reinitialisierungen gefahrlos sind.
|
||||
beleg: "CREATE TABLE IF NOT EXISTS matches"
|
||||
- candidates_log wird im Schema nicht definiert und damit bewusst ausgelassen.
|
||||
beleg: "CREATE TABLE IF NOT EXISTS crawl_log"
|
||||
kanten:
|
||||
19
.planer/einheiten/backend__tft__matches__crawl.py.md
Normal file
19
.planer/einheiten/backend__tft__matches__crawl.py.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Einheiten: backend/tft/matches/crawl.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/matches/crawl.py::crawl
|
||||
beschreibung: Crawlt gewertete TFT-Matches von Challengern/GM-Spielern und persistiert sie gefiltert nach Queue und Set-Nummer in der SQLite-Datenbank.
|
||||
input: set_number (Ziel-Set-Nummer) und optionales limit (Maximalanzahl neuer Matches)
|
||||
output: Anzahl der neu hinzugefügten Matches (int)
|
||||
entscheidungen:
|
||||
- Bereits vorhandene Match-IDs werden vorab in ein Set geladen, um Duplikate frühzeitig zu überspringen.
|
||||
beleg: "seen = {row[0] for row in conn.execute(\"SELECT match_id FROM matches\")}"
|
||||
- Nur Matches der gewerteten Queue 1100 werden übernommen, andere werden verworfen.
|
||||
beleg: "if info.get(\"queue_id\", info.get(\"queueId\")) != RANKED_QUEUE:"
|
||||
- Nur Matches des angeforderten tft_set_number werden persistiert, Fremd-Sets herausgefiltert.
|
||||
beleg: "if info[\"tft_set_number\"] != set_number:"
|
||||
- Nach jedem neu gespeicherten Match wird sofort committet, um Fortschritt nicht zu verlieren.
|
||||
beleg: "conn.commit()"
|
||||
- Ein täglicher crawl_log-Eintrag aktualisiert die Anzahl hinzugefügter Matches inkrementell.
|
||||
beleg: "ON CONFLICT(day) DO UPDATE SET matches_added = matches_added + ?"
|
||||
kanten:
|
||||
61
.planer/einheiten/backend__tft__matches__extract.py.md
Normal file
61
.planer/einheiten/backend__tft__matches__extract.py.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Einheiten: backend/tft/matches/extract.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/matches/extract.py::patch_of
|
||||
beschreibung: Leitet den zweistelligen Versions-Patch (z. B. „16.14") aus einem beliebigen `game_version`-String ab.
|
||||
input: Ein `game_version`-String mit beliebigen Präfixen oder Trennzeichen.
|
||||
output: Der Patch-Anteil als String der Form „x.y".
|
||||
entscheidungen:
|
||||
- Iteriert über Tokens statt blind zu splitten, um an einen tatsächlichen `x.y`-Token zu gelangen.
|
||||
beleg: "for token in game_version.replace(\"(\", \" \").split():"
|
||||
- Übernimmt nur die ersten beiden Punkt-Segmente und ignoriert alles dahinter.
|
||||
beleg: "return \".\".join(digits.split(\".\")[:2])"
|
||||
- Bevorzugt ein gefundes numerisches Token gegenüber dem Fallback auf das letzte whitespace-getrennte Wort.
|
||||
beleg: "digits = token"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/matches/extract.py::extract_match
|
||||
|
||||
## backend/tft/matches/extract.py::extract_match
|
||||
beschreibung: Überführt ein einzelnes rohes Match-Diktat in eine Liste von Endboard-Zeilen, indem es pro Teilnehmer die relevanten Felder in ein Tupel schreibt.
|
||||
input: Ein `match`-Dictionary gemäß Riot-Match-Schema.
|
||||
output: Eine Liste von Tupeln mit Match-ID, PUUID, Set-Nummer, Patch, Platzierung, Level, letzter Runde, Restgold und JSON-serialisierten Units/Traits/Augments.
|
||||
entscheidungen:
|
||||
- Serialisiert verschachtelte Strukturen (Units, Traits, Augments) vor dem Schreiben als JSON-String, um eine flache Tupel-Form zu behalten.
|
||||
beleg: "json.dumps(p[\"units\"]),"
|
||||
- Liefert bei fehlendem `augments`-Feld eine leere Liste statt einen Fehler.
|
||||
beleg: "json.dumps(p.get(\"augments\", [])),"
|
||||
- Bezieht den Patch je Match zentral aus `info` und nicht pro Teilnehmer neu berechnet.
|
||||
beleg: "patch = patch_of(info[\"game_version\"])"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/matches/extract.py::patch_of
|
||||
- wird-genutzt-von: backend/tft/matches/extract.py::extract_all
|
||||
|
||||
## backend/tft/matches/extract.py::validate_ids
|
||||
beschreibung: Zählt in den Endboard-Zeilen IDs (Units, Items, Traits, Augments), die nicht in den statischen Daten vorkommen, als Patch-Drift-Kanarienvogel.
|
||||
input: Eine Liste von Endboard-Zeilen-Tupeln und ein `static`-Dictionary mit bekannten IDs.
|
||||
output: Ein `Counter` mit Präfix-Schlüsseln `unit:`, `item:`, `trait:` und `augment:`, das unbekannte IDs samt Häufigkeit führt.
|
||||
entscheidungen:
|
||||
- Filtert Unit-Drift um erwartbar unbekannte Beschwörungen/PvE-Einheiten anhand von Namenshinweisen heraus.
|
||||
beleg: "if not any(h in api.lower() for h in BENIGN_UNIT_HINTS):"
|
||||
- Vereinigt Items und Augments zu einem einzigen `known_items`-Set, um Doppellookups zu vermeiden.
|
||||
beleg: "known_items = set(static[\"items\"]) | set(static[\"augments\"])"
|
||||
- Zählt bei Items nur Drift, wenn das Item dem aktuellen Set oder dem generischen `TFT_Item_`-Schema angehört.
|
||||
beleg: "if item.startswith(\"TFT_Item_\") or item.startswith(set_prefix):"
|
||||
kanten:
|
||||
- nutzt: backend/tft/matches/extract.py::BENIGN_UNIT_HINTS
|
||||
- wird-genutzt-von: backend/tft/matches/extract.py::extract_all
|
||||
|
||||
## backend/tft/matches/extract.py::extract_all
|
||||
beschreibung: Liest alle noch nicht verarbeiteten Roh-Matches aus der Datenbank, extrahiert ihre Endboard-Zeilen, prüft sie gegen die statischen Daten und schreibt sie persistent in die `endboards`-Tabelle.
|
||||
input: Eine geöffnete DB-`connection` mit Schema `matches`/`endboards` und ein `static`-Dictionary mit bekannten IDs.
|
||||
output: Ein Tupel `(n, total_unknown)` mit Anzahl geschriebener Zeilen und aggregiertem Drift-Counter.
|
||||
entscheidungen:
|
||||
- Selektiert nur Matches, deren `match_id` noch nicht in `endboards` vorkommt, um Doppelverarbeitung zu vermeiden.
|
||||
beleg: "SELECT raw FROM matches WHERE match_id NOT IN (SELECT DISTINCT match_id FROM endboards)"
|
||||
- Verwendet `INSERT OR IGNORE`, damit Wiederholungsläufe keine Duplikatfehler erzeugen.
|
||||
beleg: "\"INSERT OR IGNORE INTO endboards VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\""
|
||||
- Sammelt Drift in einem einzigen Counter statt pro Match, damit das Endergebnis meldbar bleibt.
|
||||
beleg: "total_unknown.update(validate_ids(rows, static))"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/matches/extract.py::extract_match
|
||||
- ruft-auf: backend/tft/matches/extract.py::validate_ids
|
||||
48
.planer/einheiten/backend__tft__matches__riot.py.md
Normal file
48
.planer/einheiten/backend__tft__matches__riot.py.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Einheiten: backend/tft/matches/riot.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/matches/riot.py::_load_env
|
||||
beschreibung: Lädt Variablen aus backend/.env in os.environ, sofern die Datei existiert.
|
||||
input: keine Argumente; liest backend/.env relativ zum Repo-Root.
|
||||
output: füllt fehlende Umgebungsvariablen in os.environ (None).
|
||||
entscheidungen:
|
||||
- Nur vorhandene Schlüssel werden gesetzt, bestehende Werte bleiben unangetastet.
|
||||
beleg: "os.environ.setdefault(key.strip(), value.strip())"
|
||||
- Kommentarzeilen und Zeilen ohne "=" werden ignoriert.
|
||||
beleg: "if line and not line.startswith(\"#\") and \"=\" in line:"
|
||||
- Fehlt die .env-Datei, wird der Ladevorgang still beendet.
|
||||
beleg: "if not env_file.exists():"
|
||||
anker: paths
|
||||
kanten:
|
||||
|
||||
## backend/tft/matches/riot.py::api_key
|
||||
beschreibung: Liefert den Riot-API-Key aus der Umgebung und bricht bei fehlendem Key ab.
|
||||
input: keine Argumente; liest RIOT_API_KEY aus os.environ.
|
||||
output: den Riot-API-Key als String oder beendet den Prozess via SystemExit.
|
||||
entscheidungen:
|
||||
- Vor dem Auslesen wird die .env-Datei nachgeladen.
|
||||
beleg: "_load_env()"
|
||||
- Ohne gesetzten Key wird das Programm mit klarer Anleitung beendet.
|
||||
beleg: "raise SystemExit("
|
||||
- Der Fehlerhinweis nennt den Bezugsort backend/.env.
|
||||
beleg: "put it in backend/.env"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/matches/riot.py::_load_env
|
||||
|
||||
## backend/tft/matches/riot.py::RiotClient
|
||||
beschreibung: Dünner Wrapper für die Riot-API mit Rate-Limiting, 429-Retry und nutzbaren TFT-Endpunkten.
|
||||
input: Hostname, Pfad, Query-Parameter und ggf. PUUID/Match-ID des Aufrufers.
|
||||
output: JSON-Antwort der Riot-API als dict oder list, bzw. aggregierte Ladder-Einträge.
|
||||
entscheidungen:
|
||||
- Vor jedem Request wird mindestens SLEEP Sekunden gewartet, um das Dev-Key-Limit einzuhalten.
|
||||
beleg: "time.sleep(SLEEP)"
|
||||
- Bei HTTP 429 wird anhand von Retry-After erneut gesendet.
|
||||
beleg: "if resp.status_code == 429:"
|
||||
- Bei 401/403 wird der Prozess mit Hinweis beendet statt eine Exception weiterzureichen.
|
||||
beleg: "if resp.status_code in (401, 403):"
|
||||
- Der API-Key wird als X-Riot-Token-Header übergeben.
|
||||
beleg: "headers={\"X-Riot-Token\": self.key}, timeout=30"
|
||||
- setdefault verhindert das Überschreiben vorhandener Env-Variablen.
|
||||
beleg: "os.environ.setdefault(key.strip(), value.strip())"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/matches/riot.py::api_key
|
||||
116
.planer/einheiten/backend__tft__model__artifact.py.md
Normal file
116
.planer/einheiten/backend__tft__model__artifact.py.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Einheiten: backend/tft/model/artifact.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/model/artifact.py::craftable_items
|
||||
beschreibung: Filtert aus den statischen Items die standardmäßig kombinierbaren Items heraus.
|
||||
input: `static_items: dict` — vom Static-Datensatz abgeleitete Item-Definitionen
|
||||
output: `list[str]` alphabetisch sortierter API-Namen kombinierbarer Items
|
||||
entscheidungen:
|
||||
- Spatula- und FryingPan-Komponenten schließen jedes Item mit einer Emblem-Komponente aus
|
||||
beleg: "not set(comp) & SPATULA_COMPONENTS"
|
||||
- Nur Items mit genau zwei Komponenten und gültigem API-Namenpräfix gelten als kombinierbar
|
||||
beleg: "api.startswith(\"TFT_Item_\")"
|
||||
- Das Ergebnis wird alphabetisch sortiert zurückgegeben
|
||||
beleg: "return sorted(pool)"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/artifact.py::recipes
|
||||
- ruft-auf: backend/tft/model/artifact.py::component_pool
|
||||
- wird-genutzt-von: backend/tft/model/artifact.py::build
|
||||
|
||||
## backend/tft/model/artifact.py::recipes
|
||||
beschreibung: Bildet die kombinierbaren Items auf einen sortierten Komponenten-Schlüssel ab.
|
||||
input: `static_items: dict` — Item-Definitionen aus dem Static-Datensatz
|
||||
output: `dict` mit `"CompA|CompB"`-Schlüssel und Item-API als Wert
|
||||
entscheidungen:
|
||||
- Der Komponenten-Schlüssel wird aus der sortierten Komponentenliste gebildet
|
||||
beleg: "\"|\".join(sorted(static_items[api][\"composition\"]))"
|
||||
- Nur kombinierbare Items fließen über `craftable_items` ein
|
||||
beleg: "for api in craftable_items(static_items)"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/artifact.py::craftable_items
|
||||
- wird-genutzt-von: backend/tft/model/artifact.py::build
|
||||
|
||||
## backend/tft/model/artifact.py::component_pool
|
||||
beschreibung: Bestimmt den Pool an Basis-Komponenten, die häufig in Rezepten vorkommen.
|
||||
input: `static_items: dict` — Item-Definitionen aus dem Static-Datensatz
|
||||
output: `list[str]` sortierte Komponenten-API-Namen, die in mindestens drei Rezepten vorkommen
|
||||
entscheidungen:
|
||||
- Eine Komponente muss in mindestens drei Rezepten vorkommen, um in den Pool zu gelangen
|
||||
beleg: "if n >= 3"
|
||||
- Nur Komponenten aus kombinierbaren Items werden gezählt
|
||||
beleg: "for api in craftable_items(static_items):"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/artifact.py::craftable_items
|
||||
- wird-genutzt-von: backend/tft/model/artifact.py::build
|
||||
|
||||
## backend/tft/model/artifact.py::spell_dps_cap
|
||||
beschreibung: Berechnet das 90. Perzentil der Spell-DPS aller Units auf 1 Stern ohne Items als Deckelwert.
|
||||
input: `static: dict`, `roles: dict` — Static-Datensatz und Rollen-Klassifikation pro Unit
|
||||
output: `float | None` mit dem 90.-Perzentil-Wert oder `None`, wenn keine Werte existieren
|
||||
entscheidungen:
|
||||
- Nur Spell-DPS-Werte größer als null gehen in die Berechnung ein
|
||||
beleg: "if (p := statsheet.unit_stats("
|
||||
- Bei fehlenden Werten wird `None` statt eines Default-Werts zurückgegeben
|
||||
beleg: "return None"
|
||||
- Die Auswahl erfolgt über das 90. Perzentil als relativer Spell-Deckel
|
||||
beleg: "return values[int(len(values) * 0.9)]"
|
||||
kanten:
|
||||
- nutzt: backend/tft/model/statsheet.py::unit_stats
|
||||
- wird-genutzt-von: backend/tft/model/artifact.py::build
|
||||
|
||||
## backend/tft/model/artifact.py::_augment_specs
|
||||
beschreibung: Delegiert die Erzeugung der Augment-Spezifikationen an das Augment-Modul.
|
||||
input: `static: dict` — Static-Datensatz mit Augment- und Unit-Definitionen
|
||||
output: `dict` mit den aufgebauten Augment-Spezifikationen
|
||||
entscheidungen:
|
||||
- Die Augment- und Unit-Sektion des Static-Datensatzes wird an den Builder weitergereicht
|
||||
beleg: "return build_specs(static[\"augments\"], static[\"units\"])"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/augments.py::build_specs
|
||||
- wird-genutzt-von: backend/tft/model/artifact.py::build
|
||||
|
||||
## backend/tft/model/artifact.py::build
|
||||
beschreibung: Baut das versionierte Analyse-Artefakt als Vertrag zwischen Pipeline und Simulation.
|
||||
input: `static: dict`, `learned: dict | None`, `extra_meta: dict | None` — Roh-Daten plus optionale Ergänzungen
|
||||
output: `dict` mit Meta-, Static-, Rollen-, Item- und Augment-Abschnitten des Artefakts
|
||||
entscheidungen:
|
||||
- Die Schemaversion wird als Top-Level-Konstante fixiert
|
||||
beleg: "\"schema_version\": SCHEMA_VERSION,"
|
||||
- Rollen werden über `baseline.classify_role` je Unit bestimmt
|
||||
beleg: "api: baseline.classify_role(unit)"
|
||||
- Ein leerer `augment_pool` signalisiert dem Sim die Nutzung aller geparsten Augments
|
||||
beleg: "\"augment_pool\": [],"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/artifact.py::spell_dps_cap
|
||||
- ruft-auf: backend/tft/model/artifact.py::craftable_items
|
||||
- ruft-auf: backend/tft/model/artifact.py::component_pool
|
||||
- ruft-auf: backend/tft/model/artifact.py::recipes
|
||||
- ruft-auf: backend/tft/model/artifact.py::_augment_specs
|
||||
- nutzt: backend/tft/model/baseline.py::classify_role
|
||||
|
||||
## backend/tft/model/artifact.py::save
|
||||
beschreibung: Schreibt das gebaute Artefakt als JSON in das versionsspezifische Verzeichnis und aktualisiert den Latest-Pointer.
|
||||
input: `artifact: dict` — das von `build` erzeugte Artefakt
|
||||
output: `str` mit dem Pfad zur geschriebenen `analysis.json`
|
||||
entscheidungen:
|
||||
- Zielverzeichnis wird über Set-Nummer und Baudatum aus `paths.artifact_dir` bestimmt
|
||||
beleg: "out_dir = paths.artifact_dir(set_number, build_date)"
|
||||
- Das Verzeichnis wird inklusive Eltern bei Bedarf angelegt
|
||||
beleg: "out_dir.mkdir(parents=True, exist_ok=True)"
|
||||
- Der Latest-Pointer wird separat als kleine JSON-Datei geschrieben
|
||||
beleg: "json.dumps({\"path\": str(out_path)})"
|
||||
kanten:
|
||||
- nutzt: backend/tft/paths.py::artifact_dir
|
||||
- nutzt: backend/tft/paths.py::latest_artifact_pointer
|
||||
|
||||
## backend/tft/model/artifact.py::load
|
||||
beschreibung: Lädt das aktuellste Artefakt für ein gegebenes Set über den Latest-Pointer.
|
||||
input: `set_number: int` — die TFT-Set-Nummer
|
||||
output: `dict` mit dem deserialisierten Artefakt aus der referenzierten `analysis.json`
|
||||
entscheidungen:
|
||||
- Ohne vorhandenen Pointer bricht der Aufruf mit einer Laufzeit-Anweisung ab
|
||||
beleg: "raise SystemExit("
|
||||
- Der tatsächliche Pfad wird aus dem Pointer-JSON gelesen, nicht aus dem Dateinamen abgeleitet
|
||||
beleg: "path = json.loads(pointer.read_text())[\"path\"]"
|
||||
kanten:
|
||||
- nutzt: backend/tft/paths.py::latest_artifact_pointer
|
||||
46
.planer/einheiten/backend__tft__model__augments.py.md
Normal file
46
.planer/einheiten/backend__tft__model__augments.py.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Einheiten: backend/tft/model/augments.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/model/augments.py::_num
|
||||
beschreibung: Liest einen numerischen Effekt-Wert aus einem Dict über eine Liste bevorzugter Schlüssel.
|
||||
input: ein `effects`-Dict und ein Tupel von Schlüsselnamen
|
||||
output: den ersten positiven Zahlenwert oder `None`, falls keiner passt
|
||||
entscheidungen:
|
||||
- Nur der erste positive Treffer zählt; Null und negative Werte werden übersprungen.
|
||||
beleg: "if isinstance(val, (int, float)) and val > 0:"
|
||||
- Es wird strikt zwischen echten Zahlen und Nicht-Zahlen gefiltert, bevor der Wert gilt.
|
||||
beleg: "if isinstance(val, (int, float)) and val > 0:"
|
||||
- Fehlt jeder passende Schlüssel, signalisiert die Funktion das durch `None`.
|
||||
beleg: "return None"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/model/augments.py::build_spec
|
||||
|
||||
## backend/tft/model/augments.py::build_spec
|
||||
beschreibung: Zerlegt ein einzelnes Augment in mechanisch anwendbare Atome aus Beschreibung und Effects-Block.
|
||||
input: ein Augment-Dict (`api`, `desc`, `effects`) und das `static_units`-Verzeichnis
|
||||
output: ein Dict mit `atoms`-Liste und `offerable`-Flag
|
||||
entscheidungen:
|
||||
- Ein Augment gilt nur dann als „offerable", wenn die Beschreibung lang genug und frei von Platzhaltern ist.
|
||||
beleg: "offerable = len(desc) >= 10 and \"_Desc\" not in desc and \"@\" not in desc"
|
||||
- Beim Spieler-HP-Atom wird der Effects-Wert bevorzugt, sonst der Regex-Treffer aus der Beschreibung.
|
||||
beleg: "\"amount\": int(_num(effects, PLAYER_HP_KEYS) or m.group(1))"
|
||||
- Team-Stat-Buffs werden nur übernommen, wenn die Beschreibung unkonditional ist.
|
||||
beleg: "if TEAM_RE.search(desc) and not CONDITIONAL_RE.search(desc):"
|
||||
- Fraktionswerte werden zur Vergleichbarkeit mit `statsheet` über `_fraction` normalisiert.
|
||||
beleg: "value = _fraction(val)"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/augments.py::_num
|
||||
- nutzt: backend/tft/model/statsheet.py::_fraction
|
||||
- wird-genutzt-von: backend/tft/model/augments.py::build_specs
|
||||
|
||||
## backend/tft/model/augments.py::build_specs
|
||||
beschreibung: Wendet `build_spec` auf alle Augmente eines Katalogs an und indiziert das Ergebnis nach API.
|
||||
input: ein `static_augments`-Dict und das `static_units`-Verzeichnis
|
||||
output: ein Dict, das jeder API ihr `build_spec`-Ergebnis zuordnet
|
||||
entscheidungen:
|
||||
- Die Funktion ist eine reine Dictionary-Komprehension über den gesamten Katalog.
|
||||
beleg: "return {api: build_spec(aug, static_units) for api, aug in static_augments.items()}"
|
||||
- Die Schlüssel des Eingabe-Dicts werden unverändert als API-Schlüssel übernommen.
|
||||
beleg: "for api, aug in static_augments.items()"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/augments.py::build_spec
|
||||
32
.planer/einheiten/backend__tft__model__baseline.py.md
Normal file
32
.planer/einheiten/backend__tft__model__baseline.py.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Einheiten: backend/tft/model/baseline.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/model/baseline.py::unit_value
|
||||
beschreibung: Berechnet einen numerischen Einheitenwert aus Einkaufskosten und Stern-Aufstufung.
|
||||
input: cost (int) und stars (int)
|
||||
output: float-Wert gemäß cost · 3^(stars−1)
|
||||
entscheidungen:
|
||||
- Höhere Sternstufen werden exponentiell gewichtet, nicht additiv.
|
||||
beleg: "cost * 3 ** (stars - 1)"
|
||||
- Die Berechnung ist rein deterministisch ohne Seiteneffekte.
|
||||
beleg: "def unit_value(cost: int, stars: int) -> float:"
|
||||
- Das Ergebnis skaliert mit den Kosten der Einheit.
|
||||
beleg: "return cost * 3 ** (stars - 1)"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/model/baseline.py::classify_role
|
||||
|
||||
## backend/tft/model/baseline.py::classify_role
|
||||
beschreibung: Ordnet eine Einheit über ihr Rollenattribut (mit Statistik-Fallback) einer strategischen Rolle zu.
|
||||
input: unit (dict) mit mindestens den Schlüsseln "role" und "stats"
|
||||
output: eine der vier Rollenstrings frontline | ad_carry | ap_carry | utility
|
||||
entscheidungen:
|
||||
- Fehlende Rollen werden über das letzte Element zu frontline bzw. ad_carry aufgelöst.
|
||||
beleg: "return \"frontline\" if stats[\"range\"] <= 1 else \"ad_carry\""
|
||||
- Treffererkennung erfolgt robust auf Teilstrings statt auf exakter Gleichheit.
|
||||
beleg: "if \"tank\" in role:"
|
||||
- Magische/AD-Rollen werden gegenüber dem Stat-Fallback bevorzugt geprüft.
|
||||
beleg: "if \"caster\" in role or \"ap\" in role:"
|
||||
- Die Normalisierung der Rolle erfolgt via lower() auf einen Default-leeren String.
|
||||
beleg: "(unit.get(\"role\") or \"\").lower()"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/model/baseline.py::unit_value
|
||||
69
.planer/einheiten/backend__tft__model__calibrate.py.md
Normal file
69
.planer/einheiten/backend__tft__model__calibrate.py.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Einheiten: backend/tft/model/calibrate.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/model/calibrate.py::_ranks
|
||||
beschreibung: Wandelt eine Liste von Werten in mittlere Ränge um und behandelt dabei gleiche Werte (Ties) korrekt.
|
||||
input: Eine Liste von Float-Werten.
|
||||
output: Eine Liste von Float-Rängen gleicher Länge mit 1-basierter Mittelrang-Vergabe bei Ties.
|
||||
entscheidungen:
|
||||
- Gleiche Werte erhalten alle den Mittelrang ihrer Position.
|
||||
beleg: "midrank = (i + j) / 2 + 1"
|
||||
- Die Ränge sind 1-basiert.
|
||||
beleg: "ranks[order[k]] = midrank"
|
||||
- Iteration erfolgt über sortierte Indizes statt über die Werte direkt.
|
||||
beleg: "order = sorted(range(len(values)), key=lambda i: values[i])"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/model/calibrate.py::spearman
|
||||
|
||||
## backend/tft/model/calibrate.py::spearman
|
||||
beschreibung: Berechnet den Spearman-Rangkorrelationskoeffizienten zwischen zwei gleichlangen Wertelisten.
|
||||
input: Zwei gleich lange Listen von Float-Werten.
|
||||
output: Der Spearman-Korrelationskoeffizient als Float, bei verschwindender Varianz 0.0.
|
||||
entscheidungen:
|
||||
- Division durch Null wird explizit abgefangen und mit 0.0 beantwortet.
|
||||
beleg: "if va == 0 or vb == 0:"
|
||||
- Die Kovarianz und Varianzen werden als Populationsgrößen berechnet.
|
||||
beleg: "va = sum((x - ma) ** 2 for x in ra) ** 0.5"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/calibrate.py::_ranks
|
||||
- wird-genutzt-von: backend/tft/model/calibrate.py::calibrate
|
||||
|
||||
## backend/tft/model/calibrate.py::board_from_row
|
||||
beschreibung: Deserialisiert die aus der DB kommenden JSON-Strings für Units und Augments in Board-Strukturen.
|
||||
input: Zwei JSON-Strings (Units und Augments).
|
||||
output: Ein Tupel aus einer Liste von Unit-Dicts und einer Liste von Augment-Namen.
|
||||
entscheidungen:
|
||||
- Das Feld für Items heißt tolerant „itemNames" und wird mit Default-Leerliste gelesen.
|
||||
beleg: "\"items\": u.get(\"itemNames\", []),"
|
||||
- Die Tier-Information der Unit wird als Sternzahl interpretiert.
|
||||
beleg: "\"stars\": u[\"tier\"],"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/model/calibrate.py::_holdout_scores
|
||||
|
||||
## backend/tft/model/calibrate.py::_holdout_scores
|
||||
beschreibung: Lädt Endboard-Zeilen aus der DB, baut daraus Boards je Match und lässt sie durch score_fn bewerten.
|
||||
input: Eine DB-Connection, ein Artifact-Dict, ein Holdout-Flag und eine score-Funktion.
|
||||
output: Ein Dict, das je match_id eine Liste von (placement, score)-Paaren enthält.
|
||||
entscheidungen:
|
||||
- Die Holdout-Filterung erfolgt match-bezogen über das letzte Zeichen der match_id.
|
||||
beleg: "where += \" AND substr(match_id, -1) IN ('0', '5')\""
|
||||
- Das Set wird aus den Artifact-Metadaten als Filterparameter gelesen.
|
||||
beleg: "set_number = artifact[\"meta\"][\"set\"]"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/calibrate.py::board_from_row
|
||||
- wird-genutzt-von: backend/tft/model/calibrate.py::calibrate
|
||||
|
||||
## backend/tft/model/calibrate.py::calibrate
|
||||
beschreibung: Ermittelt die mittlere Spearman-Korrelation zwischen Board-Score und Platzierung über Holdout-Matches als Kalibrierungsmetrik.
|
||||
input: Eine DB-Connection, ein Artifact-Dict, optional ein Holdout-Flag und eine score-Funktion.
|
||||
output: Ein Dict mit „matches" (Anzahl) und „mean_spearman" (Mittelwert der Korrelationen).
|
||||
entscheidungen:
|
||||
- Nur Matches mit vollständigen 8 Spielern fließen in die Korrelation ein.
|
||||
beleg: "if len(players) < 8:"
|
||||
- Die Korrelation wird negiert, da niedrigere Platzierungen besser sind.
|
||||
beleg: "correlations.append(-spearman(placements, scores))"
|
||||
- Ohne Korrelationen wird 0.0 als Mittelwert zurückgegeben.
|
||||
beleg: "\"mean_spearman\": sum(correlations) / n if n else 0.0,"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/calibrate.py::_holdout_scores
|
||||
- ruft-auf: backend/tft/model/calibrate.py::spearman
|
||||
46
.planer/einheiten/backend__tft__model__learn.py.md
Normal file
46
.planer/einheiten/backend__tft__model__learn.py.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Einheiten: backend/tft/model/learn.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/model/learn.py::_multiplier
|
||||
beschreibung: Berechnet einen um Bayessche Schrumpfung zentrierten Stärke-Multiplikator aus einer Liste von Platzierungen.
|
||||
input: eine Liste von Platzierungswerten (Integers)
|
||||
output: ein Float-Multiplikator in [0.8, 1.25], der bei 1.0 bei mittlerer Platzierung liegt
|
||||
entscheidungen:
|
||||
- Schrupfung gegen das Schnitt-Niveau (MEAN_PLACEMENT) erfolgt via PSEUDO_COUNT Pseudo-Beobachtungen.
|
||||
beleg: "(sum(placements) + MEAN_PLACEMENT * PSEUDO_COUNT) / (n + PSEUDO_COUNT)"
|
||||
- Pro Platzierungs-Punkt Abweichung vom Mittelwert wird DELTA_SCALE = 0.06 aufaddiert.
|
||||
beleg: "mult = 1.0 + (MEAN_PLACEMENT - mean) * DELTA_SCALE"
|
||||
- Der Ergebniswert wird auf das Intervall MULT_CAP = (0.8, 1.25) gekappt.
|
||||
beleg: "return min(max(mult, MULT_CAP[0]), MULT_CAP[1])"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/model/learn.py::learn
|
||||
|
||||
## backend/tft/model/learn.py::learn
|
||||
beschreibung: Aggregiert Endboard-Zeilen zu placementsbezogenen Multiplikatoren für Einheiten, Traits, Items, Augments und Paare.
|
||||
input: eine Liste von Tupeln (placement, units_json, traits_json, augments_json)
|
||||
output: ein Dict mit Multiplikatoren je Kategorie, signifikanten Paar-Lifts sowie Brett-Zähler
|
||||
entscheidungen:
|
||||
- Top-4-Paarungen werden nur gezählt, wenn die Beobachtungszahl PAIR_MIN_N erreicht und ein Erwartungswert > 0 vorliegt.
|
||||
beleg: "if n_ab < PAIR_MIN_N:"
|
||||
- Nur aktive Traits (tier_current > 0) gehen in die Trait-Aggregation ein.
|
||||
beleg: "if t.get(\"tier_current\", 0) > 0:"
|
||||
- Paar-Lifts werden auf PAIR_LIFT_CAP gekappt und auf 4 Nachkommastellen gerundet zurückgegeben.
|
||||
beleg: "lift = min(max(lift, PAIR_LIFT_CAP[0]), PAIR_LIFT_CAP[1])"
|
||||
- Alle vier Kategorie-Multiplikatoren werden ebenfalls auf 4 Nachkommastellen gerundet.
|
||||
beleg: "{k: round(_multiplier(v), 4) for k, v in unit_placements.items()},"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/learn.py::_multiplier
|
||||
- wird-genutzt-von: backend/tft/model/learn.py::learn_from_db
|
||||
|
||||
## backend/tft/model/learn.py::learn_from_db
|
||||
beschreibung: Liest Endboard-Zeilen eines Sets aus einer SQL-Verbindung und gibt das gelernte Multiplikator-Dict zurück.
|
||||
input: eine SQL-Connection, eine Set-Nummer und ein Flag zum Ausschluss von Holdout-Matches
|
||||
output: das von learn() erzeugte Multiplikator-Dict
|
||||
entscheidungen:
|
||||
- Bei exclude_holdout werden Matches ausgefiltert, deren match_id auf '0' oder '5' endet.
|
||||
beleg: "where += \" AND substr(match_id, -1) NOT IN ('0', '5')\""
|
||||
- Die Selektion projiziert nur die für learn() nötigen Spalten.
|
||||
beleg: "\"SELECT placement, units, traits, augments FROM endboards {where}\""
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/learn.py::learn
|
||||
- nutzt: backend/tft/model/learn.py::learn
|
||||
57
.planer/einheiten/backend__tft__model__score.py.md
Normal file
57
.planer/einheiten/backend__tft__model__score.py.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Einheiten: backend/tft/model/score.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/model/score.py::active_trait_tiers
|
||||
beschreibung: Ermittelt zu jedem Trait einer Board-Aufstellung den erreichten Breakpoint und gibt nur die aktiven Stufen zurück.
|
||||
input: board_units (Liste mit Unit-Dicts), static_traits (Trait-Definitionen), static_units (Unit-Definitionen)
|
||||
output: dict trait_api_name → erreichte Breakpoint-Ordnung (1-basiert), nur aktive Traits
|
||||
entscheidungen:
|
||||
- Nur Traits mit einem Treffer in `static_traits` werden berücksichtigt, Unbekannte stillschweigend übersprungen.
|
||||
beleg: "if not info:"
|
||||
- Der höchste Breakpoint, dessen `min_units` erreicht ist, gewinnt — Iteration über alle Breakpoints mit laufender Aktualisierung.
|
||||
beleg: "for i, bp in enumerate(info[\"breakpoints\"], start=1):"
|
||||
- Traits, die keinen Breakpoint erreichen, erscheinen gar nicht im Ergebnis (`ordinal == 0`).
|
||||
beleg: "if ordinal:"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/model/score.py::score_board
|
||||
|
||||
## backend/tft/model/score.py::unit_profile
|
||||
beschreibung: Baut das Kampfprofil einer einzelnen Board-Unit aus DPS, eHP, Defensive-Flag und Unit-Bezug.
|
||||
input: u (Board-Unit-Dict), artifact (gesamtes Artifact mit static-Daten, Rollen, Spell-DPS-Cap)
|
||||
output: dict mit "off", "def", "defensive", "unit" oder None bei unbekannter Unit
|
||||
entscheidungen:
|
||||
- Unbekannte Units (fehlend in `static.units`) führen zu `None`, statt einen Fehler zu werfen.
|
||||
beleg: "if not unit:"
|
||||
- Rollen werden bevorzugt aus dem Artifact gelesen, sonst via `baseline.classify_role` bestimmt.
|
||||
beleg: "artifact.get(\"roles\", {}).get(u[\"api_name\"]) or baseline.classify_role(unit)"
|
||||
- Defensive-Status gilt ausschließlich für Rollen "frontline" und "utility" — Cast-Rollen steuern nur die Zielreihenfolge.
|
||||
beleg: "\"defensive\": role in (\"frontline\", \"utility\")"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/baseline.py::classify_role
|
||||
- nutzt: backend/tft/model/statsheet.py::unit_stats
|
||||
- wird-genutzt-von: backend/tft/model/score.py::board_profiles
|
||||
|
||||
## backend/tft/model/score.py::board_profiles
|
||||
beschreibung: Liefert die Kampfprofile aller Units eines Boards, wobei Units ohne Profil herausgefiltert werden.
|
||||
input: board_units (Liste mit Unit-Dicts), artifact (gesamtes Artifact)
|
||||
output: Liste der Profile (None-Einträge aus `unit_profile` werden übersprungen)
|
||||
entscheidungen:
|
||||
- Die Filterung erfolgt inline per walrus-Operator, sodass kein zweiter Durchlauf nötig ist.
|
||||
beleg: "if (p := unit_profile(u, artifact))"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/score.py::unit_profile
|
||||
- wird-genutzt-von: backend/tft/model/score.py::score_board
|
||||
|
||||
## backend/tft/model/score.py::score_board
|
||||
beschreibung: Berechnet den Board-Score als Quadratwurzel aus (Σoff · Σdef), skaliert durch 100.
|
||||
input: board_units (Liste mit Unit-Dicts), augments (Signatur-Kompatibilität, ungenutzt), artifact (gesamtes Artifact)
|
||||
output: float — der Board-Score für Vergleiche
|
||||
entscheidungen:
|
||||
- Die Formel ist `sqrt(Σoff × Σdef)`; sie kombiniert Offensiv- und Defensiv-Summen multiplikativ unter einer Wurzel.
|
||||
beleg: "return (off * dfn) ** 0.5 / 100"
|
||||
- Augments bleiben bewusst ungenutzt, nur die Signatur bleibt für Aufrufer kompatibel.
|
||||
beleg: "augments bleibt nur für Signatur-Kompatibilität."
|
||||
- Die Division durch 100 dient nur der Anzeige-Skalierung und ist für Vergleiche irrelevant.
|
||||
beleg: "/100: nur Anzeige-Skalierung, für Vergleiche irrelevant."
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/model/score.py::board_profiles
|
||||
95
.planer/einheiten/backend__tft__model__statsheet.py.md
Normal file
95
.planer/einheiten/backend__tft__model__statsheet.py.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Einheiten: backend/tft/model/statsheet.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/model/statsheet.py::_empty_acc
|
||||
beschreibung: Liefert ein frisches Sammel-Dictionary mit allen bekannten Stat-Buff-Akkumulatoren auf 0.0 initialisiert.
|
||||
input: keine Argumente
|
||||
output: ein `dict` mit 12 numerischen Stat-Feldern (z. B. `ad_pct`, `ap_flat`, `hp_pct`).
|
||||
entscheidungen:
|
||||
- Alle Felder werden explizit auf 0.0 gesetzt, damit Akkumulatoren ohne Vorbelastung starten.
|
||||
beleg: "{\"ad_pct\": 0.0, \"ap_flat\": 0.0, \"as_pct\": 0.0, \"hp_flat\": 0.0,"
|
||||
- Es wird genau ein gemeinsames Akkumulator-Schema für Items und Trait-Buffs verwendet.
|
||||
beleg: "return {\"ad_pct\": 0.0, \"ap_flat\": 0.0, \"as_pct\": 0.0, \"hp_flat\": 0.0,"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/model/statsheet.py::_apply_item_effects
|
||||
- wird-genutzt-von: backend/tft/model/statsheet.py::trait_buffs
|
||||
- wird-genutzt-von: backend/tft/model/statsheet.py::unit_stats
|
||||
|
||||
## backend/tft/model/statsheet.py::_apply_item_effects
|
||||
beschreibung: Überträgt die modellierten Item-Effekte aus dem Datenkatalog in den Stat-Akkumulator und normalisiert Werteinheiten.
|
||||
input: `item_apis` (zu applizierende Item-IDs), `static_items` (Item-Katalog), `acc` (Stat-Akkumulator)
|
||||
output: keine Rückgabe; `acc` wird in-place um Item-Boni angereichert.
|
||||
entscheidungen:
|
||||
- Unbekannte oder `None`-Effekte werden bewusst ignoriert, um bespoke Mechanik nicht zu modellieren.
|
||||
beleg: "if val is None:"
|
||||
- Roh-Listenwerte werden via `_fraction` normalisiert, während flache Skalierungen direkt addiert werden.
|
||||
beleg: "elif key in (\"BonusDamage\", \"DamageAmp\"):"
|
||||
- Prozent-Keys (`AS`, `CritChance`) werden durch 100 geteilt, weil sie als Prozentzahl geliefert werden.
|
||||
beleg: "elif key == \"AS\":"
|
||||
- `MAPPED_ITEM_KEYS` definiert die Menge der modellierten Effekte als vertragliche Whitelist.
|
||||
beleg: "MAPPED_ITEM_KEYS = (\"AD\", \"AP\", \"AS\", \"CritChance\", \"Health\", \"Armor\","
|
||||
kanten:
|
||||
- nutzt: backend/tft/model/statsheet.py::_fraction
|
||||
- wird-genutzt-von: backend/tft/model/statsheet.py::unit_stats
|
||||
|
||||
## backend/tft/model/statsheet.py::item_is_modeled
|
||||
beschreibung: Entscheidet anhand der modellierten Effekt-Whitelist, ob ein Item überhaupt in die Berechnung einfließt.
|
||||
input: `item_info` (Item-Datensatz mit `effects`)
|
||||
output: `bool`, `True` sobald mindestens ein modellierter Key einen Nicht-`None`-Wert hat.
|
||||
entscheidungen:
|
||||
- Nur Werte != None zählen, da `None` als „nicht angegeben" interpretiert wird.
|
||||
beleg: "return any(effects.get(k) is not None for k in MAPPED_ITEM_KEYS)"
|
||||
- Die modellierten Keys sind als Modul-Konstante `MAPPED_ITEM_KEYS` zusammengefasst.
|
||||
beleg: "MAPPED_ITEM_KEYS = (\"AD\", \"AP\", \"AS\", \"CritChance\", \"Health\", \"Armor\","
|
||||
kanten:
|
||||
- nutzt: backend/tft/model/statsheet.py::_apply_item_effects
|
||||
|
||||
## backend/tft/model/statsheet.py::_fraction
|
||||
beschreibung: Normalisiert cdragon-Werte, die mal als Fraction (0.15) und mal als Prozentzahl (15.0) vorliegen, auf einen einheitlichen Bruch.
|
||||
input: `value` (float)
|
||||
output: `float` im Bereich [0, 1] (Fraction).
|
||||
entscheidungen:
|
||||
- Schwellwert 1.0 entscheidet, ob ein Wert als bereits normalisierte Fraction gilt.
|
||||
beleg: "return value if abs(value) <= 1.0 else value / 100"
|
||||
- Beträge werden verglichen, damit auch negative Fraktionen korrekt behandelt werden.
|
||||
beleg: "return value if abs(value) <= 1.0 else value / 100"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/model/statsheet.py::_apply_item_effects
|
||||
- wird-genutzt-von: backend/tft/model/statsheet.py::trait_buffs
|
||||
|
||||
## backend/tft/model/statsheet.py::trait_buffs
|
||||
beschreibung: Zerlegt Trait-Variablennamen in Tokens und leitet daraus teamweite Stat-Buffs sowie die Liste der erkannten Traits ab.
|
||||
input: `active_tiers` (aktive Trait-Breakpoints), `static_traits` (Trait-Katalog)
|
||||
output: Tupel `(buffs, recognized)` mit Akkumulator-Dictionary und Set erkannter Trait-IDs.
|
||||
entscheidungen:
|
||||
- Mechanik-Parameter werden über eine Token-Blacklist früh aussortiert.
|
||||
beleg: "if tokens & _SKIP_TOKENS or name.startswith(\"{\"):"
|
||||
- `ap`-Tokens werden mit `* 100` auf den flachen AP-Konventionen aufsummiert.
|
||||
beleg: "elif \"ap\" in tokens:"
|
||||
- Variablen ohne numerischen Wert werden übersprungen, um Token-Mismatches zu vermeiden.
|
||||
beleg: "if value is None or not isinstance(value, (int, float)):"
|
||||
- Nur Traits, bei denen mindestens eine Variable als Buff gemappt wurde, landen in `recognized`.
|
||||
beleg: "if matched:"
|
||||
kanten:
|
||||
- nutzt: backend/tft/model/statsheet.py::_empty_acc
|
||||
- nutzt: backend/tft/model/statsheet.py::_fraction
|
||||
- nutzt: backend/tft/model/statsheet.py::_TOKEN_RE
|
||||
|
||||
## backend/tft/model/statsheet.py::unit_stats
|
||||
beschreibung: Berechnet das effektive Kampfprofil (eHP, Gesamt-DPS, Spell-DPS) einer Unit aus Basiswerten, Sternenmultiplikatoren, Team-Buffs und Items.
|
||||
input: `unit` (Champion-Daten), `stars`, `item_apis`, `static_items`, `team_buffs`, `role`, optional `spell_cap`
|
||||
output: `dict` mit den Schlüsseln `ehp`, `dps` und `spell_dps`.
|
||||
entscheidungen:
|
||||
- HP und AD skalieren über `HP_STAR_MULT` bzw. `AD_STAR_MULT` je nach Sternenlevel.
|
||||
beleg: "hp = (stats.get(\"hp\") or 0) * HP_STAR_MULT ** (stars - 1)"
|
||||
- eHP kombiniert Rüstung/MR und Schadensreduktion multiplikativ.
|
||||
beleg: "ehp = hp * (1 + (armor + mr) / 200) * (1 + acc[\"dr\"])"
|
||||
- Die Cast-Rate wird durch `CAST_RATE_CAP` gedeckelt, um Ausreißer zu verhindern.
|
||||
beleg: "CAST_RATE_CAP,"
|
||||
- Spell-DPS ist auf `SPELL_AUTO_CAP` mal eigene Auto-DPS begrenzt, mit optionalem Floor.
|
||||
beleg: "limit = SPELL_AUTO_CAP * auto_dps"
|
||||
- Team-Buffs und Item-Effekte werden in `_empty_acc` zusammengeführt, um Bonus-Stats konsistent anzureichern.
|
||||
beleg: "acc = _empty_acc()"
|
||||
kanten:
|
||||
- nutzt: backend/tft/model/statsheet.py::_empty_acc
|
||||
- nutzt: backend/tft/model/statsheet.py::_apply_item_effects
|
||||
50
.planer/einheiten/backend__tft__paths.py.md
Normal file
50
.planer/einheiten/backend__tft__paths.py.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Einheiten: backend/tft/paths.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/paths.py::static_dir
|
||||
beschreibung: Liefert den Pfad zum Verzeichnis der statischen Daten für ein bestimmtes Set und einen Patch.
|
||||
input: `set_number: int`, `patch: str`
|
||||
output: Pfad zu `STATIC / f"set{set_number}_{patch}"`.
|
||||
entscheidungen:
|
||||
- Der Verzeichnisname kombiniert Set-Nummer und Patch durch einen Unterstrich.
|
||||
beleg: "return STATIC / f\"set{set_number}_{patch}\""
|
||||
- Der Pfad wird relativ zum vordefinierten STATIC-Wurzelverzeichnis gebildet.
|
||||
beleg: "STATIC = DATA / \"static\""
|
||||
kanten:
|
||||
- nutzt: backend/tft/paths.py::STATIC
|
||||
|
||||
## backend/tft/paths.py::latest_static_pointer
|
||||
beschreibung: Liefert den Pfad zur Pointer-Datei, die auf den aktuellsten statischen Datenstand verweist.
|
||||
input: keine Parameter
|
||||
output: Pfad zu `STATIC / "latest.json"`.
|
||||
entscheidungen:
|
||||
- Der aktuellste Datenstand wird über eine zentrale `latest.json`-Datei markiert.
|
||||
beleg: "return STATIC / \"latest.json\""
|
||||
- Die Funktion kapselt den Pfad zur Latest-Pointer-Datei als eigene Einheit.
|
||||
beleg: "def latest_static_pointer() -> Path:"
|
||||
kanten:
|
||||
- nutzt: backend/tft/paths.py::STATIC
|
||||
|
||||
## backend/tft/paths.py::artifact_dir
|
||||
beschreibung: Liefert den Pfad zum Artefakt-Verzeichnis eines Sets zu einem bestimmten Build-Datum.
|
||||
input: `set_number: int`, `build_date: str`
|
||||
output: Pfad zu `ARTIFACTS / f"set{set_number}" / build_date`.
|
||||
entscheidungen:
|
||||
- Artefakte werden zweistufig organisiert: erst nach Set-Nummer, dann nach Build-Datum.
|
||||
beleg: "return ARTIFACTS / f\"set{set_number}\" / build_date"
|
||||
- Der Pfad wird relativ zum vordefinierten ARTIFACTS-Wurzelverzeichnis gebildet.
|
||||
beleg: "ARTIFACTS = DATA / \"artifacts\""
|
||||
kanten:
|
||||
- nutzt: backend/tft/paths.py::ARTIFACTS
|
||||
|
||||
## backend/tft/paths.py::latest_artifact_pointer
|
||||
beschreibung: Liefert den Pfad zur Pointer-Datei des aktuellsten Artefakt-Builds für ein bestimmtes Set.
|
||||
input: `set_number: int`
|
||||
output: Pfad zu `ARTIFACTS / f"set{set_number}" / "latest.json"`.
|
||||
entscheidungen:
|
||||
- Pro Set existiert eine eigene `latest.json` als Pointer auf den aktuellsten Build.
|
||||
beleg: "return ARTIFACTS / f\"set{set_number}\" / \"latest.json\""
|
||||
- Der Set-spezifische Latest-Pointer liegt innerhalb des Set-Unterordners.
|
||||
beleg: "ARTIFACTS / f\"set{set_number}\""
|
||||
kanten:
|
||||
- nutzt: backend/tft/paths.py::ARTIFACTS
|
||||
45
.planer/einheiten/backend__tft__sim__autoplay.py.md
Normal file
45
.planer/einheiten/backend__tft__sim__autoplay.py.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Einheiten: backend/tft/sim/autoplay.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/sim/autoplay.py::play_afk
|
||||
beschreibung: Spielt ein vollständiges Game headless zu Ende, ohne steuernd einzugreifen.
|
||||
input: ein laufendes `Game`-Objekt.
|
||||
output: die finale Platzierung (`game.placement`) als int.
|
||||
entscheidungen:
|
||||
- Kein eigener Steuerungscode — jede Runde wird nur durch `game.step()` fortgeschaltet.
|
||||
beleg: "game.step()"
|
||||
- Abbruchbedingung ist ausschließlich das Ende des Spiels über `game.over`.
|
||||
beleg: "while not game.over:"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/game.py::Game
|
||||
- wird-genutzt-von: backend/tft/sim/autoplay.py::run
|
||||
|
||||
## backend/tft/sim/autoplay.py::play_econ
|
||||
beschreibung: Spielt ein Game mit der Econ-Policy (`fast8`) zu Ende und liefert die Platzierung.
|
||||
input: ein laufendes `Game`-Objekt.
|
||||
output: die finale Platzierung (`game.placement`) als int.
|
||||
entscheidungen:
|
||||
- Verwendet fest das Archetyp-Profil `fast8` aus der Policy-Bibliothek.
|
||||
beleg: "params = policy.ARCHETYPES[\"fast8\"]"
|
||||
- Greift pro Runde sowohl über die Policy als auch über `game.step()` in den Spielablauf ein.
|
||||
beleg: "policy.act(game.player, game.pool, game.artifact, game.cfg, game.rng,"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/policy.py::act
|
||||
- nutzt: backend/tft/sim/game.py::Game
|
||||
- wird-genutzt-von: backend/tft/sim/autoplay.py::run
|
||||
|
||||
## backend/tft/sim/autoplay.py::run
|
||||
beschreibung: Führt n headless Spiele mit der gewählten Policy aus und aggregiert Platzierungen.
|
||||
input: `artifact`, `cfg`, ein `policy_name` und die Anzahl `n` (optional `seed`).
|
||||
output: ein Dict mit `games`, `avg_placement` und `top4_rate`.
|
||||
entscheidungen:
|
||||
- Die Auswahl der konkreten Strategie erfolgt über das Mapping `POLICIES` per Name.
|
||||
beleg: "POLICIES[policy_name](game)"
|
||||
- Pro Lauf wird ein neuer Seed aus `seed + i` abgeleitet, um Wiederholbarkeit pro Spiel zu garantieren.
|
||||
beleg: "Game(artifact, cfg, seed=seed + i)"
|
||||
- `top4_rate` zählt Platzierungen ≤ 4 als Erfolg.
|
||||
beleg: "if p <= 4"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/autoplay.py::play_afk
|
||||
- ruft-auf: backend/tft/sim/autoplay.py::play_econ
|
||||
- nutzt: backend/tft/sim/game.py::Game
|
||||
76
.planer/einheiten/backend__tft__sim__combat.py.md
Normal file
76
.planer/einheiten/backend__tft__sim__combat.py.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Einheiten: backend/tft/sim/combat.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/sim/combat.py::FightResult
|
||||
beschreibung: Datenklasse, die das Ergebnis eines Kampfes zwischen zwei Teams festhält.
|
||||
input: Siegerkennzeichen "a"/"b"/None, gespielte Frames und überlebende Units beider Seiten
|
||||
output: Ein `FightResult`-Objekt mit `winner`, `frames`, `survivors_a` und `survivors_b`
|
||||
entscheidungen:
|
||||
- Das Ergebnis-Format schließt einen Unentschieden-Fall explizit als `None` mit ein.
|
||||
beleg: "winner: str | None # \"a\" | \"b\" | None = Unentschieden"
|
||||
- Überlebende werden seitenweise als Liste von Unit-Dicts geführt, nicht als Indizes.
|
||||
beleg: "survivors_a: list[dict]"
|
||||
- Die Anzahl der simulierten Frames wird als Ganzzahl gerundet (`ceil`).
|
||||
beleg: "frames=math.ceil(elapsed * FPS),"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/sim/combat.py::fight
|
||||
|
||||
## backend/tft/sim/combat.py::_queue
|
||||
beschreibung: Bestimmt die Bearbeitungsreihenfolge der Units eines Teams als defensive zuerst, intern zufällig gemischt.
|
||||
input: Team-Liste von Unit-Dicts und ein `random.Random`-RNG
|
||||
output: Neue Liste, in der defensive Units vor offensiven stehen, beide Gruppen intern gemischt
|
||||
entscheidungen:
|
||||
- Defensive Units werden grundsätzlich vor offensiven angegriffen.
|
||||
beleg: "defensive = [p for p in team if p[\"defensive\"]]"
|
||||
- Innerhalb jeder Gruppe wird die Reihenfolge per `rng.shuffle` zufällig gemischt.
|
||||
beleg: "rng.shuffle(defensive)"
|
||||
- Die Sortierung ist nicht stabil, da die ursprüngliche Reihenfolge überschrieben wird.
|
||||
beleg: "return defensive + offensive"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/combat.py::_suffix_off
|
||||
- wird-genutzt-von: backend/tft/sim/combat.py::fight
|
||||
|
||||
## backend/tft/sim/combat.py::_suffix_off
|
||||
beschreibung: Berechnet die Suffix-Summe der Offensive-Werte einer Ziel-Queue.
|
||||
input: Liste von Unit-Dicts mit `"off"`-Feld
|
||||
output: Liste gleicher Länge + 1, in der `suffix[i]` die Gesamt-Offense ab Unit `i` ist
|
||||
entscheidungen:
|
||||
- Die Liste ist absichtlich um einen Eintrag länger, damit `suffix[len(queue)] = 0` als Sicherheitselement dient.
|
||||
beleg: "suffix = [0.0] * (len(queue) + 1)"
|
||||
- Die Summation läuft rückwärts, um die echte Suffix-Summe zu bilden.
|
||||
beleg: "for i in range(len(queue) - 1, -1, -1):"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/sim/combat.py::fight
|
||||
|
||||
## backend/tft/sim/combat.py::fight
|
||||
beschreibung: Simuliert einen deterministischen Kampf zweier Teams eventbasiert bis zum nächsten Tod oder Zeitlimit.
|
||||
input: Zwei Team-Profil-Listen, ein `random.Random`-RNG und eine Config mit `combat.max_frames`
|
||||
output: `FightResult` mit Sieger, Framezahl und Überlebenden beider Seiten
|
||||
entscheidungen:
|
||||
- Zwischen zwei Todesereignissen werden beide Offensen als konstant angenommen, `dt = min(ta, tb)`.
|
||||
beleg: "dt = min(ta, tb)"
|
||||
- Bei Überschreitung des Zeitlimits wird `dt` auf das verbleibende Restbudget begrenzt.
|
||||
beleg: "dt = limit - elapsed"
|
||||
- Überzähliger Schaden wird verlustfrei auf das nächste Ziel übertragen, indem die Rest-Defense weiter reduziert wird.
|
||||
beleg: "rb -= oa * dt"
|
||||
- Bei Zeitlimit ohne verbleibende Offense wird der Sieg nach größerer Rest-Defense entschieden.
|
||||
beleg: "winner = \"a\" if rest_a > rest_b + EPS else \"b\" if rest_b > rest_a + EPS else None"
|
||||
- Ein gleicher Kill-Frame kann nicht beide Seiten treffen, da `else`-Zweige getrennt ausgewertet werden.
|
||||
beleg: "else:"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/combat.py::_queue
|
||||
- ruft-auf: backend/tft/sim/combat.py::_suffix_off
|
||||
- nutzt: backend/tft/sim/combat.py::FightResult
|
||||
- wird-genutzt-von: backend/tft/sim/combat.py::player_damage
|
||||
|
||||
## backend/tft/sim/combat.py::player_damage
|
||||
beschreibung: Berechnet den Schaden, den ein Spieler nach einer Runde abhängig von erreichter Stage und Überlebenden erhält.
|
||||
input: Stage-Index (1-basiert), Liste der überlebenden Units und eine Config mit `damage`
|
||||
output: Schadenswert als `int`
|
||||
entscheidungen:
|
||||
- Der Basis-Schaden wird stageweise aus `cfg["damage"]["stage_base"]` gelesen, bei Überschreitung wird der letzte Eintrag verwendet.
|
||||
beleg: "min(stage - 1, len(d[\"stage_base\"]) - 1)"
|
||||
- Pro überlebender Unit wird ein linearer Zusatzschaden addiert.
|
||||
beleg: "return base + d[\"per_surviving_unit\"] * len(survivors)"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/combat.py::fight
|
||||
59
.planer/einheiten/backend__tft__sim__economy.py.md
Normal file
59
.planer/einheiten/backend__tft__sim__economy.py.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Einheiten: backend/tft/sim/economy.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/sim/economy.py::interest
|
||||
beschreibung: Berechnet die passiven Zinserträge auf Basis des aktuellen Goldstands.
|
||||
input: aktueller Goldstand `gold` und die Konfiguration `cfg`
|
||||
output: gekappter Zinsbetrag gemäß Konfigurationsobergrenze
|
||||
entscheidungen:
|
||||
- Der Zins wird in 10er-Schritten gewährt und auf einen Maximalwert gedeckelt.
|
||||
beleg: "min(gold // 10 * g[\"interest_per_10\"], g[\"interest_cap\"])"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/sim/economy.py::round_income
|
||||
|
||||
## backend/tft/sim/economy.py::streak_gold
|
||||
beschreibung: Liefert den Streak-Bonus auf Basis der absoluten Streaklänge aus einer gestaffelten Konfigurationstabelle.
|
||||
input: aktuelle Streaklänge `streak` und die Konfiguration `cfg`
|
||||
output: höchster anwendbarer Streak-Bonuswert (oder 0)
|
||||
entscheidungen:
|
||||
- Es wird der größte passende Schwellenwert verwendet, da spätere Iterationen frühere überschreiben.
|
||||
beleg: "if abs(streak) >= min_streak:"
|
||||
- Streaks werden symmetrisch auf ihre Länge betrachtet, sodass Sieg- und Niederlagenserien gleichbehandelt werden.
|
||||
beleg: "if abs(streak) >= min_streak:"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/sim/economy.py::round_income
|
||||
|
||||
## backend/tft/sim/economy.py::round_income
|
||||
beschreibung: Aggregiert die gesamten Goldeinnahmen einer Runde aus Grundeinkommen, Zinsen, Streak-Bonus und Siegbonus.
|
||||
input: Goldstand `gold`, Streak `streak`, Siegstatus `won`, Rundenlabel `round_label` und Konfiguration `cfg`
|
||||
output: Gesamtes Goldeinkommen dieser Runde als Summe aller Komponenten
|
||||
entscheidungen:
|
||||
- Frühe Runden erhalten ein spezielles Grundeinkommen, andere fallen auf den Standardwert zurück.
|
||||
beleg: "g[\"early_income\"].get(round_label, g[\"base_income\"])"
|
||||
- Nur bei einem Rundensieg wird der Siegbonus addiert.
|
||||
beleg: "if won:"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/economy.py::interest
|
||||
- ruft-auf: backend/tft/sim/economy.py::streak_gold
|
||||
|
||||
## backend/tft/sim/economy.py::xp_to_next
|
||||
beschreibung: Liefert die XP-Schwelle bis zum nächsten Level oder signalisiert das Erreichen des Maximallevels.
|
||||
input: aktuelles Level `level` und die Konfiguration `cfg`
|
||||
output: benötigte XP für das nächste Level, oder `None` am Maximallevel
|
||||
entscheidungen:
|
||||
- Am oder über dem Maximallevel wird `None` zurückgegeben, um einen weiteren Aufstieg zu verhindern.
|
||||
beleg: "if level >= xp[\"max_level\"]:"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/sim/economy.py::apply_xp
|
||||
|
||||
## backend/tft/sim/economy.py::apply_xp
|
||||
beschreibung: Wendet gewonnene XP auf einen Spieler an und levelt ihn automatisch so weit wie möglich auf.
|
||||
input: aktuelles Level `level`, aktuelle XP `xp`, hinzugewonnene XP `gained` und Konfiguration `cfg`
|
||||
output: aktualisiertes Level und verbleibende XP nach möglichen Levelaufstiegen
|
||||
entscheidungen:
|
||||
- Mehrere Levelaufstiege werden in einer Schleife konsumiert, solange XP reicht und das Maximallevel nicht erreicht ist.
|
||||
beleg: "while (need := xp_to_next(level, cfg)) is not None and xp >= need:"
|
||||
- Beim Erreichen des Maximallevels werden verbleibende XP verworfen, um Überlauf zu vermeiden.
|
||||
beleg: "if level >= cfg[\"xp\"][\"max_level\"]:"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/economy.py::xp_to_next
|
||||
71
.planer/einheiten/backend__tft__sim__game.py.md
Normal file
71
.planer/einheiten/backend__tft__sim__game.py.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Einheiten: backend/tft/sim/game.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/sim/game.py::pair_players
|
||||
beschreibung: Ermittelt die PvP-Paarungen der lebenden Spieler und einen eventuellen Ghost-Gegner unter Vermeidung von Wiederholungsgegnern.
|
||||
input: Liste lebender PlayerState und ein random.Random
|
||||
output: Tupel aus Paaren, einem möglichen odd-Spieler und einem möglichen Ghost-Spieler
|
||||
entscheidungen:
|
||||
- Es werden bis zu 20 Shuffle-Versuche unternommen, um eine kollisionsfreie Paarung zu finden.
|
||||
beleg: "for _ in range(20):"
|
||||
- Wiederholungsgegner werden bei mehr als zwei Lebenden explizit ausgeschlossen.
|
||||
beleg: "all(a.last_opponent != b.name for a, b in pairs)"
|
||||
- Bei ungerader Spielerzahl wird ein Spieler als Ghost gegen einen zufälligen anderen gespiegelt.
|
||||
beleg: "ghost_src = rng.choice([p for p in alive if p is not odd]) if odd else None"
|
||||
- Die Paarungsinformation wird über last_opponent beidseitig festgehalten.
|
||||
beleg: "a.last_opponent, b.last_opponent = b.name, a.name"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/player.py::PlayerState
|
||||
|
||||
## backend/tft/sim/game.py::Game
|
||||
beschreibung: Orchestriert eine komplette TFT-Partie als endliche Zustandsmaschine über Runden, Spielerauswahl und Kampfauswertung.
|
||||
input: artifact (dict), cfg (dict), optional seed (int)
|
||||
output: ein lauffähiger Spielzustand mit Spieler, Bots, Rundenplan, Pool und Log, der per `step()` voranschreitet
|
||||
entscheidungen:
|
||||
- Der Mensch steuert nur `self.player`, Bots werden fest auf 7 Instanzen aus rotierenden Archetypen aufgeteilt.
|
||||
beleg: "for i in range(7)"
|
||||
- Carousels laufen automatisch für alle ab, ohne Einkommen oder XP zu gewähren.
|
||||
beleg: "for p in self.players:"
|
||||
- Bei Rundenstart werden leere Shop-Slots vorbereitet, da zu 1-2 noch niemand Gold hat.
|
||||
beleg: "for p in self.players:"
|
||||
- Streak-Gold und Siegbonus werden ausschließlich in PvP-Runden gewertet, in PvE nicht.
|
||||
beleg: "if is_pvp:"
|
||||
- Augment-Angebote werden deterministisch pro Runde aus einer gewichteten Stufe gezogen, nicht pro Spieler separat.
|
||||
beleg: "self.rng.choices((1, 2, 3), weights=weights)[0]"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/pool.py::Pool
|
||||
- ruft-auf: backend/tft/sim/player.py::new_player
|
||||
- ruft-auf: backend/tft/sim/player.py::score
|
||||
- ruft-auf: backend/tft/sim/player.py::grant_loot
|
||||
- ruft-auf: backend/tft/sim/player.py::grab_carousel_unit
|
||||
- ruft-auf: backend/tft/sim/player.py::buy
|
||||
- ruft-auf: backend/tft/sim/player.py::sell
|
||||
- ruft-auf: backend/tft/sim/player.py::reroll
|
||||
- ruft-auf: backend/tft/sim/player.py::buy_xp
|
||||
- ruft-auf: backend/tft/sim/player.py::move
|
||||
- ruft-auf: backend/tft/sim/player.py::equip
|
||||
- ruft-auf: backend/tft/sim/player.py::pick_augment
|
||||
- ruft-auf: backend/tft/sim/player.py::fill_board
|
||||
- ruft-auf: backend/tft/sim/player.py::merge
|
||||
- ruft-auf: backend/tft/sim/player.py::refresh_shop
|
||||
- ruft-auf: backend/tft/sim/policy.py::act
|
||||
- ruft-auf: backend/tft/sim/policy.py::ARCHETYPES
|
||||
- ruft-auf: backend/tft/sim/economy.py::apply_xp
|
||||
- ruft-auf: backend/tft/sim/economy.py::round_income
|
||||
- ruft-auf: backend/tft/sim/combat.py::fight
|
||||
- ruft-auf: backend/tft/sim/combat.py::player_damage
|
||||
- ruft-auf: backend/tft/sim/combat.py::pair_players
|
||||
- ruft-auf: backend/tft/sim/combat.py::board_profiles
|
||||
|
||||
## backend/tft/sim/game.py::_delegate
|
||||
beschreibung: Erzeugt eine Property, die Lese- und Schreibzugriff transparent an ein Attribut eines delegierten Objekts weiterreicht.
|
||||
input: ein Attributname als String
|
||||
output: ein `property`-Objekt mit Getter und Setter, die das gleichnamige Attribut auf `self.player` lesen bzw. schreiben
|
||||
entscheidungen:
|
||||
- Getter und Setter greifen beide über `self.player` auf das Zielattribut zu, sodass Aufrufer das Delegate-Objekt nicht vom echten Spieler unterscheiden müssen.
|
||||
beleg: "lambda self: getattr(self.player, attr)"
|
||||
- Schreibzugriff wird ebenfalls an `self.player` durchgereicht, wodurch das Delegate zweiseitig konsistent bleibt.
|
||||
beleg: "lambda self, value: setattr(self.player, attr, value)"
|
||||
- Die Attribute werden als ungeprüfte Strings übergeben, was eine sehr flexible, aber untypisierte Bindung an beliebige `player`-Attribute erlaubt.
|
||||
beleg: "attr: str"
|
||||
kanten:
|
||||
228
.planer/einheiten/backend__tft__sim__player.py.md
Normal file
228
.planer/einheiten/backend__tft__sim__player.py.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Einheiten: backend/tft/sim/player.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/sim/player.py::InvalidAction
|
||||
beschreibung: Markiert ungültige Spielaktionen als Ausnahme.
|
||||
input: eine Fehlermeldung als Zeichenkette.
|
||||
output: eine Exception, die von Aufrufern abgefangen wird.
|
||||
entscheidungen:
|
||||
- Wird als eigene Exception-Klasse statt generischer `Exception` definiert, damit Aufrufer gezielt reagieren können.
|
||||
beleg: "class InvalidAction(Exception):"
|
||||
- Enthält bewusst keine eigene Logik, sondern dient nur als Typ-Marker.
|
||||
beleg: " pass"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::buy
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::sell
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::buy_xp
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::move
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::equip
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::pick_augment
|
||||
|
||||
## backend/tft/sim/player.py::PlayerState
|
||||
beschreibung: Hält den kompletten Zustand eines Spielers über alle Runden hinweg.
|
||||
input: Initialwerte für Name, Archetyp, HP, Gold und Level.
|
||||
output: ein Datensatz-Objekt mit Board, Bench, Items, Augments und Shop.
|
||||
entscheidungen:
|
||||
- Verwendet `field(default_factory=list)` für mutable Defaults, um die geteilte-Liste-Falle zu vermeiden.
|
||||
beleg: " board: list = field(default_factory=list)"
|
||||
- `alive` ist als abgeleitete Property modelliert, damit Konsistenz mit `hp` garantiert ist.
|
||||
beleg: " return self.hp > 0"
|
||||
- Optionale Felder wie `placement` und `last_opponent` werden explizit typisiert, damit Phasen ohne Gegner sauber abgebildet werden.
|
||||
beleg: " placement: int | None = None"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/player.py::score
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::new_player
|
||||
|
||||
## backend/tft/sim/player.py::new_player
|
||||
beschreibung: Erzeugt einen frischen `PlayerState` aus einer Konfiguration.
|
||||
input: Name, Archetyp und ein Konfigurations-Dictionary.
|
||||
output: ein neu initialisierter `PlayerState` mit Startwerten.
|
||||
entscheidungen:
|
||||
- Liest Startwerte aus `cfg`, um Regelvarianten ohne Code-Änderung zu erlauben.
|
||||
beleg: " hp=cfg[\"damage\"][\"player_hp\"],"
|
||||
- Fallback auf Level 1, falls `start_level` fehlt.
|
||||
beleg: " level=cfg[\"xp\"].get(\"start_level\", 1),"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/player.py::PlayerState
|
||||
- nutzt: backend/tft/sim/player.py::PlayerState
|
||||
|
||||
## backend/tft/sim/player.py::score
|
||||
beschreibung: Berechnet die Spielstärke eines Spielers für die Schaden-Berechnung.
|
||||
input: ein Spielerzustand und ein Artifact-Dictionary.
|
||||
output: ein Float-Score des aktuellen Boards inklusive Augments.
|
||||
entscheidungen:
|
||||
- Delegiert die eigentliche Berechnung an `score_board`, um die Bewertungslogik zentral zu halten.
|
||||
beleg: " return score_board(p.board, p.augments, artifact)"
|
||||
- Bezieht Augments in den Score ein, damit gewählte Bonüsse kampfrelevant zählen.
|
||||
beleg: " return score_board(p.board, p.augments, artifact)"
|
||||
kanten:
|
||||
|
||||
## backend/tft/sim/player.py::unit_worth
|
||||
beschreibung: Liefert den ökonomischen Gegenwert einer Einheit für Aufstellungsentscheidungen.
|
||||
input: Artifact-Dictionary und eine Einheit mit `api_name` und `stars`.
|
||||
output: ein Float-Wert als Kombination aus Kosten und Sternen.
|
||||
entscheidungen:
|
||||
- Skaliert den Wert mit `3 ** (stars - 1)`, da Merge-Stufen das Vielfache an Einheiten ersetzen.
|
||||
beleg: " return artifact[\"static\"][\"units\"][u[\"api_name\"]][\"cost\"] * 3 ** (u[\"stars\"] - 1)"
|
||||
- Liest Einheitenkosten aus den statischen Artifact-Daten, damit Balance-Anpassungen ohne Code-Change greifen.
|
||||
beleg: " return artifact[\"static\"][\"units\"][u[\"api_name\"]][\"cost\"] * 3 ** (u[\"stars\"] - 1)"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::fill_board
|
||||
|
||||
## backend/tft/sim/player.py::refresh_shop
|
||||
beschreibung: Erneuert die Shop-Anzeige eines Spielers mit neuen Roll-Ergebnissen.
|
||||
input: Spielerzustand, Pool, Konfiguration und RNG.
|
||||
output: keine Rückgabe; der Shop des Spielers wird ersetzt.
|
||||
entscheidungen:
|
||||
- Überschreibt `p.shop` vollständig statt zu mischen, um das TFT-typische „neuer Roll" zu modellieren.
|
||||
beleg: " p.shop = roll(pool, p.level, cfg, rng)"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::reroll
|
||||
|
||||
## backend/tft/sim/player.py::merge
|
||||
beschreibung: Führt automatisch passende Einheiten gleicher Sternstufe zu höheren Sternen zusammen.
|
||||
input: Spielerzustand, API-Name und aktuelle Sternstufe.
|
||||
output: keine Rückgabe; Board und Bench werden konsolidiert.
|
||||
entscheidungen:
|
||||
- Iteriert mit `while True`, bis weniger als drei Kopien vorhanden sind.
|
||||
beleg: " while True:"
|
||||
- Übernimmt die Items der fusionierten Kopien auf die verbleibende Einheit.
|
||||
beleg: " keep[\"items\"].extend(u[\"items\"])"
|
||||
- Kürzt das Item-Inventar der behaltenen Einheit auf das maximale Limit.
|
||||
beleg: " keep[\"items\"] = keep[\"items\"][:MAX_ITEMS_PER_UNIT]"
|
||||
- Entfernt überschüssige Einheiten aus Board oder Bench je nach Herkunft.
|
||||
beleg: " (p.board if u in p.board else p.bench).remove(u)"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::buy
|
||||
- wird-genutzt-von: backend/tft/sim/player.py::grab_carousel_unit
|
||||
|
||||
## backend/tft/sim/player.py::buy
|
||||
beschreibung: Kauft eine Einheit aus dem Shop und legt sie auf die Bank.
|
||||
input: Spielerzustand, Shop-Slot, Pool und Artifact-Dictionary.
|
||||
output: keine Rückgabe; Bench und Pool werden verändert.
|
||||
entscheidungen:
|
||||
- Prüft Shop-Slot, Gold, Bank-Kapazität und Pool-Verfügbarkeit in dieser Reihenfolge, um Mehrfach-Meldungen pro Aktion zu vermeiden.
|
||||
beleg: " if not (0 <= slot < len(p.shop)) or p.shop[slot] is None:"
|
||||
- Leert den Shop-Slot nach erfolgreichem Kauf explizit.
|
||||
beleg: " p.shop[slot] = None"
|
||||
- Startet sofort einen Merge-Versuch auf Sternstufe 1, damit Käufe direkt stapeln.
|
||||
beleg: " merge(p, api, 1)"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/player.py::merge
|
||||
- nutzt: backend/tft/sim/player.py::InvalidAction
|
||||
|
||||
## backend/tft/sim/player.py::sell
|
||||
beschreibung: Verkauft eine Einheit vom Board oder Bench und erstattet Gold und Items zurück.
|
||||
input: Spielerzustand, Ort, Index, Pool und Artifact-Dictionary.
|
||||
output: keine Rückgabe; Gold, Items und Pool werden aktualisiert.
|
||||
entscheidungen:
|
||||
- Berechnet die Anzahl der Pool-Copies exponentiell zur Sternstufe.
|
||||
beleg: " copies = 3 ** (u[\"stars\"] - 1)"
|
||||
- Zieht 1 Gold ab, sobald die Einheit nicht 1 kostet und mindestens 2 Sterne hat.
|
||||
beleg: " if cost > 1 and u[\"stars\"] >= 2:"
|
||||
- Gibt Items aus dem Inventar der Einheit in das Spieler-Inventar frei.
|
||||
beleg: " p.items.extend(u[\"items\"])"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/player.py::InvalidAction
|
||||
|
||||
## backend/tft/sim/player.py::reroll
|
||||
beschreibung: Würfelt den Shop gegen Gold neu aus.
|
||||
input: Spielerzustand, Pool, Konfiguration und RNG.
|
||||
output: keine Rückgabe; der Shop wird aktualisiert.
|
||||
entscheidungen:
|
||||
- Prüft Gold erst nach dem Lesen der Kosten, damit einheitliche Fehlermeldungen entstehen.
|
||||
beleg: " if p.gold < cost:"
|
||||
- Delegiert das eigentliche Neu-Würfeln an `refresh_shop`, um Logik-Duplikation zu vermeiden.
|
||||
beleg: " refresh_shop(p, pool, cfg, rng)"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/player.py::refresh_shop
|
||||
- nutzt: backend/tft/sim/player.py::InvalidAction
|
||||
|
||||
## backend/tft/sim/player.py::buy_xp
|
||||
beschreibung: Kauft zusätzliche Erfahrungspunkte und aktualisiert das Level.
|
||||
input: Spielerzustand und Konfiguration.
|
||||
output: keine Rückgabe; Level und XP werden fortgeschrieben.
|
||||
entscheidungen:
|
||||
- Lehnt den Kauf ab, wenn das maximale Level bereits erreicht ist.
|
||||
beleg: " if p.level >= xp[\"max_level\"]:"
|
||||
- Übergibt die XP-Berechnung an `economy.apply_xp`, damit Level-Schwellen zentral gepflegt werden.
|
||||
beleg: " p.level, p.xp = economy.apply_xp(p.level, p.xp, xp[\"buy_amount\"], cfg)"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/player.py::InvalidAction
|
||||
|
||||
## backend/tft/sim/player.py::move
|
||||
beschreibung: Verschiebt eine Einheit zwischen Bank und Kampf-Board.
|
||||
input: Spielerzustand, Richtung und Index.
|
||||
output: keine Rückgabe; Board oder Bench werden mutiert.
|
||||
entscheidungen:
|
||||
- Wählt Quell- und Ziel-Liste abhängig von der Richtung in einer Zeile.
|
||||
beleg: " src, dst = (p.bench, p.board) if where == \"bench\" else (p.board, p.bench)"
|
||||
- Prüft die Board-Kapazität gegen das aktuelle Level des Spielers, nicht gegen einen festen Maximalwert.
|
||||
beleg: " if dst is p.board and len(p.board) >= p.level:"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/player.py::InvalidAction
|
||||
|
||||
## backend/tft/sim/player.py::equip
|
||||
beschreibung: Legt ein Item aus dem Inventar auf eine Board-Einheit und craftet bei passender Komponente.
|
||||
input: Spielerzustand, Item-Index, Board-Index und optionales Artifact.
|
||||
output: keine Rückgabe; Einheit und Inventar werden angepasst.
|
||||
entscheidungen:
|
||||
- Sortiert die beiden Komponenten vor dem Lookup, damit die Reihenfolge keine Rolle spielt.
|
||||
beleg: " crafted = recipes.get(\"|\".join(sorted((held, new_item))))"
|
||||
- Bricht die Funktion mit `return` ab, sobald ein Rezept erfolgreich gecraftet wurde.
|
||||
beleg: " p.items.pop(item_idx)"
|
||||
beleg: " return"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/player.py::InvalidAction
|
||||
|
||||
## backend/tft/sim/player.py::pick_augment
|
||||
beschreibung: Wählt einen Augment aus dem aktuellen Angebot des Spielers.
|
||||
input: Spielerzustand und Wahl-Index.
|
||||
output: keine Rückgabe; Augments-Liste wird erweitert und Angebot geleert.
|
||||
entscheidungen:
|
||||
- Lehnt die Aktion ab, wenn aktuell kein Angebot vorliegt.
|
||||
beleg: " if not p.augment_offer:"
|
||||
- Leert das Angebot nach der Auswahl, damit kein zweiter Pick möglich ist.
|
||||
beleg: " p.augment_offer = []"
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/player.py::InvalidAction
|
||||
|
||||
## backend/tft/sim/player.py::grant_loot
|
||||
beschreibung: Verteilt Loot-Belohnungen wie Komponenten und Gold an einen Spieler.
|
||||
input: Spielerzustand, Anzahl Komponenten, Gold, Artifact und RNG.
|
||||
output: keine Rückgabe; Inventar und Gold werden erhöht.
|
||||
entscheidungen:
|
||||
- Wählt die Item-Quelle in absteigender Priorität aus Komponenten- und Item-Pool.
|
||||
beleg: " pool = (artifact.get(\"component_pool\") or artifact.get(\"item_pool\")"
|
||||
- Erlaubt nur das Droppen von Komponenten, fertige Items entstehen ausschließlich durch Crafting.
|
||||
beleg: " # Loot droppt Komponenten; fertige Items entstehen nur durch Craften."
|
||||
kanten:
|
||||
- nutzt: backend/tft/sim/player.py::InvalidAction
|
||||
|
||||
## backend/tft/sim/player.py::grab_carousel_unit
|
||||
beschreibung: Greift eine zufällige Karussell-Einheit und legt sie auf die Bank.
|
||||
input: Spielerzustand, Pool und RNG.
|
||||
output: keine Rückgabe; Bench und Pool werden verändert.
|
||||
entscheidungen:
|
||||
- Sammelt Kandidaten aus den Kostenstufen 1 bis 3, da Karussell-Einheiten günstig sind.
|
||||
beleg: " candidates = [a for c in (1, 2, 3) for a in pool.units_of_cost(c)]"
|
||||
- Überspringt den Greif, wenn die Bank voll ist, ohne Fehler zu werfen.
|
||||
beleg: " if candidates and len(p.bench) < BENCH_SIZE:"
|
||||
- Startet sofort einen Merge-Versuch, damit Drops direkt stapeln.
|
||||
beleg: " merge(p, api, 1)"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/player.py::merge
|
||||
|
||||
## backend/tft/sim/player.py::fill_board
|
||||
beschreibung: Füllt freie Board-Plätze und tauscht stärkere Bank-Einheiten ein.
|
||||
input: Spielerzustand und Artifact-Dictionary.
|
||||
output: keine Rückgabe; Board und Bench werden umsortiert.
|
||||
entscheidungen:
|
||||
- Sortiert die Bank absteigend nach `unit_worth`, damit starke Einheiten vorne stehen.
|
||||
beleg: " p.bench.sort(key=lambda u: -unit_worth(artifact, u))"
|
||||
- Tauscht Board-Einheiten nur aus, wenn der Bank-Kandidat strikt stärker ist.
|
||||
beleg: " if unit_worth(artifact, best) > unit_worth(artifact, u):"
|
||||
- Bricht die Swap-Schleife ab, sobald die Bank leer ist.
|
||||
beleg: " if not p.bench:"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/sim/player.py::unit_worth
|
||||
19
.planer/einheiten/backend__tft__sim__pool.py.md
Normal file
19
.planer/einheiten/backend__tft__sim__pool.py.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Einheiten: backend/tft/sim/pool.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/sim/pool.py::Pool
|
||||
beschreibung: Verwaltet einen endlichen Vorrat pro Champion-API-Einheit und regelt Entnahme sowie Rückgabe.
|
||||
input: Konstruktor erhält ein Mapping von API-Namen zu Einheitendaten sowie eine Liste von Größen je Kostenstufe; Methoden nehmen API-Namen und Mengen entgegen.
|
||||
output: Aktueller verfügbarer Bestand je Einheit, eine Liste lebender Einheiten eines bestimmten Kostens, sowie ein mutierter Vorrat nach Entnahme oder Rückgabe.
|
||||
entscheidungen:
|
||||
- Einheiten ohne gültige Kostenstufe (1 bis len(sizes)) werden aus dem Vorrat ausgeschlossen.
|
||||
beleg: "if 1 <= u[\"cost\"] <= len(sizes)"
|
||||
- Die Kosten jedes API-Namens werden in einem separaten Mapping gehalten, um günstig nach Kosten zu filtern.
|
||||
beleg: "self.cost_of = {api: u[\"cost\"] for api, u in static_units.items()}"
|
||||
- Eine Einheit gilt nur dann als verfügbar, wenn sie noch nicht aufgebraucht ist.
|
||||
beleg: "self.copies[a] > 0"
|
||||
- Eine Entnahme über den aktuellen Bestand hinaus wird als Fehler signalisiert.
|
||||
beleg: "raise ValueError(f\"pool exhausted for {api_name}\")"
|
||||
- Nicht im Vorrat enthaltene API-Namen werden bei Anfragen als null verfügbar behandelt.
|
||||
beleg: "return self.copies.get(api_name, 0)"
|
||||
kanten:
|
||||
17
.planer/einheiten/backend__tft__sim__rounds.py.md
Normal file
17
.planer/einheiten/backend__tft__sim__rounds.py.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Einheiten: backend/tft/sim/rounds.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/sim/rounds.py::schedule
|
||||
beschreibung: Erzeugt die vollständige Stage-/Round-Folge eines Spiels inklusive Augment-Markierung.
|
||||
input: ein Konfigurations-Dikt mit den Schlüsseln `rounds.stage1`, `rounds.stage_n` und `rounds.augment_rounds`.
|
||||
output: eine Liste von Round-Dicts mit `label`, `stage`, `kind` und `augment`.
|
||||
entscheidungen:
|
||||
- Stage 1 wird separat aus `stage1` gelesen, alle weiteren Stages teilen sich `stage_n`.
|
||||
beleg: "for i, kind in enumerate(r[\"stage1\"], start=1):"
|
||||
- Stages werden ab 2 bis inklusive `MAX_STAGE` durchlaufen.
|
||||
beleg: "for stage in range(2, MAX_STAGE + 1):"
|
||||
- Die Augment-Markierung erfolgt nachträglich per Membership-Test gegen `augment_rounds`.
|
||||
beleg: "rnd[\"augment\"] = rnd[\"label\"] in r[\"augment_rounds\"]"
|
||||
- Labels folgen dem Schema `{stage}-{i}` mit 1-basierter Nummerierung je Stage.
|
||||
beleg: "f\"{stage}-{i}\""
|
||||
kanten:
|
||||
17
.planer/einheiten/backend__tft__sim__shop.py.md
Normal file
17
.planer/einheiten/backend__tft__sim__shop.py.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Einheiten: backend/tft/sim/shop.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/sim/shop.py::roll
|
||||
beschreibung: Führt einen Shop-Roll aus und liefert eine Anzahl zufälliger Units gemäß Level-Odds und verbleibenden Pool-Kopien.
|
||||
input: Pool-Instanz, Spieler-Level, Konfigurations-Dictionary mit Shop-Odds und Slot-Anzahl sowie ein random.Random-RNG.
|
||||
output: Liste von Unit-Champion-IDs (oder None für leere Slots) in Länge der konfigurierten Slot-Anzahl.
|
||||
entscheidungen:
|
||||
- Die Tier-Odds werden level-indiziert aus der Konfiguration gelesen, sodass die Wahrscheinlichkeiten pro Level konfigurierbar bleiben.
|
||||
beleg: "odds = cfg[\"shop\"][\"odds\"][level - 1]"
|
||||
- Für jeden Slot wird zuerst per gewichtetem Zufall ein Tier gezogen, bevor unter den Units dieses Tiers ausgewählt wird.
|
||||
beleg: "tier = rng.choices(range(1, 6), weights=odds)[0]"
|
||||
- Steht kein Kandidat eines gezogenen Tiers im Pool zur Verfügung, wird der Slot als None belegt statt zu scheitern.
|
||||
beleg: "if not candidates:"
|
||||
- Die Auswahl unter Kandidaten erfolgt gewichtet nach verfügbaren Pool-Kopien jeder Unit.
|
||||
beleg: "weights = [pool.available(a) for a in candidates]"
|
||||
kanten:
|
||||
31
.planer/einheiten/backend__tft__staticdata__fetch.py.md
Normal file
31
.planer/einheiten/backend__tft__staticdata__fetch.py.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Einheiten: backend/tft/staticdata/fetch.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/staticdata/fetch.py::fetch_static
|
||||
beschreibung: Lädt TFT-Rohdaten von Community Dragon, parst sie und schreibt sie zusammen mit Metadaten versioniert auf die Festplatte.
|
||||
input: optionale `set_override`-ID, sonst aktuelle Set-Auswahl der Quelldaten
|
||||
output: das geparste TFT-Statikdaten-Dictionary (zugleich als versionierte JSON-Dateien inklusive Pointer-Datei persistiert)
|
||||
entscheidungen:
|
||||
- Patch wird aus dem Content-Metadata-Endpoint auf die ersten zwei Versionssegmente reduziert.
|
||||
beleg: "patch = \".\".join(version.split(\".\")[:2])"
|
||||
- Rohdaten werden unverändert zusätzlich als `raw.json` archiviert.
|
||||
beleg: "(out_dir / \"raw.json\").write_text(json.dumps(raw))"
|
||||
- Geschriebene Domänen-Dateien werden mit eingerücktem, nicht-ASCII-escapiertem JSON serialisiert.
|
||||
beleg: "json.dumps(parsed[name], indent=1, ensure_ascii=False)"
|
||||
- Nach erfolgreichem Schreiben wird ein `latest_static_pointer` aktualisiert, damit Aufrufer das aktuelle Verzeichnis finden.
|
||||
beleg: "paths.latest_static_pointer().write_text(json.dumps(pointer, indent=1))"
|
||||
kanten:
|
||||
|
||||
## backend/tft/staticdata/fetch.py::load_static
|
||||
beschreibung: Lädt den zuletzt von `fetch_static` geschriebenen Satz normalisierter TFT-Statikdaten über den Pointer wieder ein.
|
||||
input: nichts; liest selbstständig den Pointer-Pfad aus `tft.paths`
|
||||
output: ein Dictionary mit den Schlüsseln `units`, `traits`, `items`, `augments` und `meta` aus dem zuletzt gespeicherten Statikdaten-Verzeichnis
|
||||
entscheidungen:
|
||||
- Das Zielverzeichnis wird über `paths.latest_static_pointer()` und nicht über einen Aufrufparameter bestimmt.
|
||||
beleg: "pointer = json.loads(paths.latest_static_pointer().read_text())"
|
||||
- Der `pathlib`-Import erfolgt erst innerhalb der Funktion, um den Modulimport schlank zu halten.
|
||||
beleg: "from pathlib import Path"
|
||||
- Beim Laden werden ausschließlich die fünf bekannten Domänen-Schlüssel zusammengeführt, unbekannte Einträge ignoriert.
|
||||
beleg: "for name in (\"units\", \"traits\", \"items\", \"augments\", \"meta\")"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/staticdata/fetch.py::fetch_static
|
||||
86
.planer/einheiten/backend__tft__staticdata__parse.py.md
Normal file
86
.planer/einheiten/backend__tft__staticdata__parse.py.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Einheiten: backend/tft/staticdata/parse.py
|
||||
stand: f41ac76b5789
|
||||
|
||||
## backend/tft/staticdata/parse.py::augment_tier
|
||||
beschreibung: Leitet die Augment-Stufe (I/II/III) aus dem Suffix des Icon-Dateinamens ab.
|
||||
input: icon_path — optionaler Pfad zum Augment-Icon.
|
||||
output: Ganzzahl 1–3 für die erkannte Stufe oder None bei fehlendem Pfad.
|
||||
entscheidungen:
|
||||
- Tier wird aus einem Römisch-Zahl-Suffix vor dem Punkt ermittelt, unabhängig von Groß-/Kleinschreibung.
|
||||
beleg: "AUGMENT_TIER_RE = re.compile(r\"[-_](I{1,3})\.\", re.I)"
|
||||
- Rückgabe ist die Länge der gematchten Römisch-Zahl, nicht ihr numerischer Wert.
|
||||
beleg: "return len(m.group(1)) if m else None"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/staticdata/parse.py::parse
|
||||
|
||||
## backend/tft/staticdata/parse.py::render_desc
|
||||
beschreibung: Füllt Platzhalter in einem Tooltip-Text mit Effekt-Werten und entfernt übriges Markup.
|
||||
input: desc — Tooltip-Text mit @…@-Platzhaltern; effects — Dict mit numerischen Effekt-Werten.
|
||||
output: Bereinigter, einsatzfertiger Beschreibungstext.
|
||||
entscheidungen:
|
||||
- Unbekannte Platzhalter werden als "?" dargestellt, statt den Text zu verwerfen.
|
||||
beleg: "return \"?\""
|
||||
- HTML-Tags und %i:…%-Blöcke werden per Regex entfernt, bevor Mehrfach-Whitespace kollabiert wird.
|
||||
beleg: "text = re.sub(r\"<[^>]+>\", \" \", text)"
|
||||
- Ganze Zahlen werden ohne Dezimalstellen ausgegeben, andere auf eine Nachkommastelle gerundet.
|
||||
beleg: "str(int(value)) if float(value).is_integer() else str(round(value, 1))"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/staticdata/parse.py::parse
|
||||
|
||||
## backend/tft/staticdata/parse.py::resolve_spell
|
||||
beschreibung: Ermittelt Schadens-Array, Schadenstyp und Skalierung einer Champion-Fähigkeit aus dem Desc-Markup.
|
||||
input: ability — Dict mit Desc-Text, Variablen und Schadens-Tags.
|
||||
output: Dict mit spell_damage, spell_damage_type und spell_scaling.
|
||||
entscheidungen:
|
||||
- Erstes im Desc gefundenes Schadens-Tag gewinnt; zugehöriger Variablenname wird gegen "Modified"-Variante geprüft.
|
||||
beleg: "array = get(name) or get(name.removeprefix(\"Modified\"))"
|
||||
- Ohne expliziten Tag wird ADDamage+APDamage kombiniert, sonst greift die Fallback-Liste in festgelegter Reihenfolge.
|
||||
beleg: "for name in DAMAGE_FALLBACKS:"
|
||||
- Skalierung wird aus %i:scaleAP% / %i:scaleAD%-Markern im Desc abgeleitet, nicht aus Variablen.
|
||||
beleg: "has_ap = \"%i:scaleAP%\" in desc"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/staticdata/parse.py::parse
|
||||
|
||||
## backend/tft/staticdata/parse.py::icon_url
|
||||
beschreibung: Wandelt einen Community-Dragon-Asset-Pfad in eine URL zum PNG-Abbild um.
|
||||
input: asset_path — interner Asset-Pfad mit Endung .tex oder .dds.
|
||||
output: URL auf raw.communitydragon.org mit .png-Endung.
|
||||
entscheidungen:
|
||||
- Endungen werden case-insensitive ersetzt, sodass .TEX und .DDS ebenfalls zu .png werden.
|
||||
beleg: "p = asset_path.lower()"
|
||||
- Basis-URL wird als Modul-Konstante gehalten, nicht pro Aufruf neu gebaut.
|
||||
beleg: "return f\"{CDRAGON_GAME}/{p}\""
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/staticdata/parse.py::parse
|
||||
|
||||
## backend/tft/staticdata/parse.py::detect_set
|
||||
beschreibung: Wählt aus dem Rohtext die höchste Set-Nummer, die Champion-Daten enthält.
|
||||
input: raw — vollständiges cdragon-JSON mit einem sets-Dict.
|
||||
output: Integer der gewählten Set-Nummer.
|
||||
entscheidungen:
|
||||
- Nur Sets mit tatsächlichen Champions werden als Kandidaten zugelassen.
|
||||
beleg: "if v.get(\"champions\")"
|
||||
- Ohne Kandidat wird hart ein ValueError geworfen, statt zu raten.
|
||||
beleg: "raise ValueError(\"no set with champions found in cdragon data\")"
|
||||
kanten:
|
||||
- wird-genutzt-von: backend/tft/staticdata/parse.py::parse
|
||||
|
||||
## backend/tft/staticdata/parse.py::parse
|
||||
beschreibung: Normalisiert das Rohtext-JSON in flache Tabellen für Units, Traits, Items und Augments.
|
||||
input: raw — cdragon-Gesamtdaten; set_override — optionale Set-Nummer.
|
||||
output: Dict mit meta, units, traits, items und augments.
|
||||
entscheidungen:
|
||||
- Minions und Beschwörungen werden über apiName bzw. leere Trait-Listen ausgefiltert, nicht über eine eigene Allowlist.
|
||||
beleg: "if not c[\"traits\"] or \"Minion\" in c[\"apiName\"]:"
|
||||
- Trait-Anzeigenamen werden nachträglich auf api-Namen gemappt, damit die Unit-Tabelle konsistente Identifier führt.
|
||||
beleg: "u[\"traits\"] = [trait_name_to_api[name] for name in u[\"traits\"]]"
|
||||
- Augments werden anhand von Präfix oder apiName-Inhalt gefiltert und um Shop-/Tooltip-Müll bereinigt.
|
||||
beleg: "if \"MarketOffering\" in api or \"@\" in (i[\"name\"] or \"\")"
|
||||
- Set-Nummer kann per Parameter überschrieben werden, sonst fällt parse auf detect_set zurück.
|
||||
beleg: "set_number = set_override if set_override is not None else detect_set(raw)"
|
||||
kanten:
|
||||
- ruft-auf: backend/tft/staticdata/parse.py::detect_set
|
||||
- ruft-auf: backend/tft/staticdata/parse.py::icon_url
|
||||
- ruft-auf: backend/tft/staticdata/parse.py::resolve_spell
|
||||
- ruft-auf: backend/tft/staticdata/parse.py::augment_tier
|
||||
- ruft-auf: backend/tft/staticdata/parse.py::render_desc
|
||||
Reference in New Issue
Block a user