Wie das TAC-Rating berechnet wird

Das Rating schätzt die Spielstärke eines Mitglieds (und eines festen Spielerpaars) aus den erfassten Spielergebnissen. Diese Seite erklärt es zuerst allgemein verständlich und danach im Detail für Entwickler.

Allgemein verständlich

Was ist das TAC-Rating?

Das Rating ist eine Zahl, die ausdrückt, wie stark jemand TAC spielt — ähnlich der Elo-Zahl beim Schach, aber mit einem moderneren Verfahren. Es wird ausschließlich aus den in der App erfassten Spielen berechnet. Niemand vergibt Punkte von Hand.

Es gibt zwei Arten von Rating: ein Einzel-Rating für jedes Mitglied und ein Team-Rating für ein festes Spielerpaar (zwei Mitglieder, die zusammen spielen).

Wann entsteht ein Rating?

Ein Rating wird nur berechnet, wenn auf beiden Seiten mindestens ein echtes Mitglied mitspielt. Gäste (Spieler ohne Mitgliedskonto) zählen nicht mit. Spielt also ein einzelnes Mitglied nur mit bzw. gegen Gäste, entsteht kein Vergleich und damit kein Rating. Für ein Team-Rating müssen zwei Mitglieder ohne Gast zusammen auf einer Seite spielen.

Wie verändert sich das Rating?

Nach jedem gewerteten Spiel wird das Rating angepasst. Gewinnt man gegen stärkere Gegner, steigt es deutlich; verliert man gegen schwächere, sinkt es deutlicher. Gewinnt man gegen viel schwächere Gegner, ändert sich kaum etwas, weil das Ergebnis ohnehin erwartet wurde.

Zu jedem Rating gehört auch eine Unsicherheit: Am Anfang weiß das System wenig über dich, deshalb kann sich dein Rating noch stark bewegen. Je mehr du spielst, desto sicherer und stabiler wird die Einschätzung.

Provisorisches Rating

Solange ein Mitglied weniger als 10 gewertete Spiele hat, gilt das Rating als provisorisch — es ist noch nicht aussagekräftig genug für einen fairen Vergleich.

Verschiedene Ranglisten

Das Rating wird für mehrere Bereiche getrennt geführt, damit Vergleiche fair bleiben:

  • Global — über alle Clubs hinweg.
  • Club — nur Spiele innerhalb eines Clubs.
  • Event — nur Spiele eines bestimmten Events/Turniers.
  • Saison — nur Spiele innerhalb eines Saison-Zeitraums eines Clubs.

Inaktivität

Wer länger nicht spielt, dessen Einschätzung wird mit der Zeit wieder unsicherer. Das Rating selbst (die geschätzte Stärke) bleibt dabei gleich — nur die Sicherheit nimmt ab, bis wieder gespielt wird.

Einzelne Spiele können von der Wertung ausgeschlossen werden (z. B. Testspiele). Gäste ohne Mitgliedskonto bekommen kein eigenes Rating.

Technische Beschreibung (für Entwickler)

Modell und Startwerte

Das Rating basiert auf OpenSkill (bayessches Weng-Lin-Verfahren mit dem Plackett-Luce-Modell). Die Referenz-Implementierung läuft in Java mit der Bibliothek com.pocketcombats:openskill; das Verfahren selbst ist sprachunabhängig und unten vollständig beschrieben.

Jedes Rating ist eine Normalverteilung mit Mittelwert μ (geschätzte Stärke) und Standardabweichung σ (Unsicherheit). Startwerte für ein neues Subjekt:

μ₀ = 25.0     σ₀ = 25 / 3 ≈ 8.3333
Initiale Rating-Werte (RatingEngine.DEFAULT_MU / DEFAULT_SIGMA).

Der angezeigte, vergleichbare Wert ist das konservative Ordinal — die Stärke abzüglich eines Sicherheitsabschlags für die Unsicherheit:

ordinal = μ − Z · σ     (Z = 3)
Konservative Punktzahl. Ein neues Subjekt startet bei 25 − 3 · 8.3333 = 0.

Für die Match-Parameter werden die Standardwerte der Bibliothek (Weng-Lin) verwendet, ohne Überschreibung: β = σ₀ / 2 ≈ 4.167 (Streuung von Stärke zu Tagesform) und τ = σ₀ / 100 ≈ 0.083 (additiver Dynamik-Term, der σ vor jeder Aktualisierung leicht erhöht, damit das Rating beweglich bleibt).

Ein Spiel werten

Ein Spiel besteht aus zwei Seiten (Team A und Team B). Jede Seite wird aus den Einzel-Ratings ihrer Spieler zu einem Team-Rating aggregiert (Summe der μ bzw. Summe der σ², DefaultTeamRatingAggregator). Aus dem Ergebnis ergeben sich Ränge:

  • Sieger-Seite: Rang 1, Verlierer-Seite: Rang 2.
  • Unentschieden: beide Seiten Rang 1.

Mit diesen Rängen aktualisiert das Plackett-Luce-Modell jedes beteiligte Subjekt: Die Stärke μ wird in Richtung des tatsächlichen Ergebnisses verschoben, die Unsicherheit σ sinkt mit jedem zusätzlichen Spiel. Die Größe der Anpassung hängt von der Differenz zwischen erwartetem und tatsächlichem Ergebnis sowie von der aktuellen Unsicherheit ab.

Die Siegwahrscheinlichkeit von Seite A gegen Seite B (für Vorhersagen/Matchmaking) ergibt sich aus den aggregierten Team-Werten über die kumulative Normalverteilung Φ:

P(A > B) = Φ( (μ_A − μ_B) / √(σ_A² + σ_B² + β²) )
RatingEngine.predictWin — Gaußsche kumulative Verteilungsfunktion.

Zwei Projektionen: Einzel und Paar

Jedes Spiel wird in zwei unabhängigen Projektionen gewertet:

  • Einzel — Subjekt ist die Mitglieds-ID. Benötigt mindestens ein echtes Mitglied auf beiden Seiten.
  • Paar (Team) — Subjekt ist ein ungeordnetes Mitglieds-Paar. Eine vollständige Seite (2 echte Mitglieder) bildet ein Paar; eine Seite mit Gast bildet kein Paar.

Team-Zuordnung aus den erfassten Daten: result_player.team ist A oder B; der Sieger steht in result.winner_team (ist er leer, gilt das Spiel als Unentschieden). Spieler ohne Mitglieds-ID (Gäste) werden bei der Wertung entfernt. Besteht eine Seite nur aus Gästen, wird sie nicht selbst gewertet, dient aber als Standard-Gegner (Subjekt mit Startwerten), damit die vollständige andere Seite trotzdem gewertet werden kann. Spiele mit exclude_from_rating werden komplett übersprungen.

Partitionen und chronologische Wiederholung

Eine Partition ist ein Tupel (scope, scopeRefId). Es gibt vier Scopes: GLOBAL (kein Ref), CLUB (clubId), EVENT (eventId), SEASON (seasonId). Jede Partition wird unabhängig berechnet — ein Spiel fließt gleichzeitig in mehrere Partitionen ein.

Ratings sind reihenfolgeabhängig und gekoppelt (das Ergebnis eines Spiels hängt von den Ratings davor ab). Deshalb werden sie nicht inkrementell „nachgerechnet", sondern durch chronologische Wiederholung der Spiele innerhalb der Partition gebildet. Die Snapshot-Historie pro Spiel (μ/σ vor und nach jedem Spiel) ist die Quelle der Wahrheit; die materialisierten Tabellen member_rating / team_rating halten nur den aktuellen Stand für schnelle Lesezugriffe.

  • Voller Neuaufbau: alle Spiele der Partition werden ab Startwerten neu durchgespielt.
  • Teil-Wiederholung ab einem Zeitpunkt: der Zustand wird aus dem letzten Snapshot vor dem Änderungszeitpunkt geladen (Seeding) und ab dort neu durchgespielt — Snapshots ab dem Zeitpunkt werden vorher gelöscht.

Materialisierte Werte pro Subjekt

Nach der Wiederholung wird je Subjekt der aktuelle Eintrag neu geschrieben:

FeldBedeutung
mu, sigmaWerte nach dem letzten Spiel
ordinalmu − 3 · sigma (konservativ)
ordinalPreviousOrdinal vor dem letzten Spiel (für Trendpfeil)
peakOrdinal / peakAthöchstes je erreichtes Ordinal und Zeitpunkt
gamesCountAnzahl gewerteter Spiele in der Partition
provisionaltrue, solange gamesCount < 10
lastGameAtZeitpunkt des letzten gewerteten Spiels
Alle gespeicherten μ/σ/ordinal-Werte werden auf 4 Nachkommastellen gerundet (HALF_UP).

Inaktivitäts-Decay

Ein täglicher Job (Standard: 04:00 Uhr) erhöht die Unsicherheit ruhender Ratings. Für jedes Rating, dessen letztes Spiel länger als 30 Tage zurückliegt, gilt:

σ ← min(σ + 0.05, 8.3333)
ordinal ← μ − 3 · σ
Pro Job-Lauf. μ bleibt unverändert; es wird keine Snapshot-Historie geschrieben (Decay ist kein Spiel-Ereignis).
  • Pro Lauf wird σ um 0.05 erhöht, gedeckelt bei σ_max = 8.3333 (= Startwert σ₀).
  • Nur Ratings unterhalb der Obergrenze werden angefasst (eine Batch-UPDATE-Anweisung).
  • EVENT-Scope ist vom Decay ausgenommen (ein Turnier ist ein abgeschlossener Zeitraum).

Verarbeitung (asynchron, eventual)

Die Berechnung läuft nie inline mit dem Speichern eines Ergebnisses, sondern eventual: Eine Ergebnis-Änderung markiert die betroffenen Partitionen nach dem Commit (AFTER_COMMIT) als „dirty" (Einfügen in eine Recompute-Queue). Ein einzelner Worker arbeitet die Queue seriell ab und spielt jede betroffene Partition ab dem Änderungszeitpunkt neu durch. Dadurch bleiben gekoppelte Ratings konsistent, ohne das Speichern zu blockieren.

Betroffen von einer Ergebnis-Änderung sind immer: GLOBAL, der CLUB des Spiels, das EVENT (alt und neu, falls verschoben) sowie jede SEASON, deren Zeitraum den Spielzeitpunkt enthält.