feat: Gildenkampf-Simulation (inkl. Pilzkatapult) #2

Merged
DasAoD merged 6 commits from feature/battle-simulation into main 2026-08-10 23:43:08 +02:00
Owner

Was

Baut eine echte Kampfsimulation (sf_api::simulate) für ausgewählte Gilden in den Server ein, als präzisere Alternative/Ergänzung zum bestehenden Level-basierten Strikt-Modus.

Validiert gegen drei reale Gilden-Matchups (Kéglimugli 1 Mitglied mit/ohne Pilzkatapult, Boba 36 Mitglieder, Wartezimmer 48 Mitglieder) — alle Ergebnisse stimmten mit dem tatsächlichen Kampfausgang überein. Details zur Validierung im Verlauf von examples/sim_probe.rs (bereits vorher gemergter Teil dieses Branches).

Neue Endpunkte

  • POST /api/simulate — simuliert ausgewählte Gilden im Hintergrund (guild_names, mushrooms 0-3, iterations, default 2500 — analog zum Default von sf-tools)
  • POST /api/simulate/abort, GET /api/simulate/progress
  • POST /api/refresh-own-fighters — erzwingt sofortiges Neuladen der eigenen Kampfwerte

Pilzkatapult

Bis zu 3 Riesenpilze treffen vor Kampfbeginn je ein zufälliges Gegner-Mitglied und ziehen ihm additiv 50 Prozentpunkte seines ursprünglichen Lebens ab (2 Treffer auf dasselbe Ziel = 0% Leben, nicht 25% wie bei multiplikativem Stacking — das war ein Bug in einer früheren Version, siehe Commit-Historie). Ziele werden pro simuliertem Einzelkampf neu gewürfelt, nicht einmal für den ganzen Iterations-Batch.

Cache der eigenen Kampfwerte

Damit nicht vor jeder Simulation alle eigenen aktiven Mitglieder per ViewPlayer neu geholt werden müssen, aber auch keine veralteten Werte benutzt werden:

  • Cache bleibt gültig, solange sich das Aktiv-Roster (< 24h offline) nicht ändert und er jünger als 6h ist.
  • Ändert sich das Roster (Zu-/Abgang, Aktiv-Status-Wechsel) oder ist er älter → automatischer Refresh.
  • Zusätzlich manueller „🔄 Eigene Kampfwerte aktualisieren"-Button, unabhängig davon.

Architektur-Fix

ScanProgress/cancel_requested lagen bisher in AppState, das für die gesamte Dauer jedes Spielserver-Requests gesperrt ist (die SimpleSession lebt dort) — /api/progress und /api/scan/abort konnten dadurch hinter einem laufenden Request hängen bleiben. Jetzt in einem eigenen ProgressState-Lock (AppHandles bündelt beide Locks als Axum-State). Gleiches Muster für die neue Simulation, die durch deutlich mehr ViewPlayer-Calls das Problem sonst verschärft hätte.

Test

  • cargo build --all-targets kompiliert ohne Warnungen (Server + sim_probe-Example).
  • JS-Syntax geprüft (node --check), alle $('id')-Referenzen im Script gegen vorhandene HTML-IDs abgeglichen.
  • Funktionales Testen im Browser gegen einen echten Account steht noch aus.

🤖 Generated with Claude Code

## Was Baut eine echte Kampfsimulation (`sf_api::simulate`) für ausgewählte Gilden in den Server ein, als präzisere Alternative/Ergänzung zum bestehenden Level-basierten Strikt-Modus. Validiert gegen drei reale Gilden-Matchups (Kéglimugli 1 Mitglied mit/ohne Pilzkatapult, Boba 36 Mitglieder, Wartezimmer 48 Mitglieder) — alle Ergebnisse stimmten mit dem tatsächlichen Kampfausgang überein. Details zur Validierung im Verlauf von `examples/sim_probe.rs` (bereits vorher gemergter Teil dieses Branches). ## Neue Endpunkte - `POST /api/simulate` — simuliert ausgewählte Gilden im Hintergrund (`guild_names`, `mushrooms` 0-3, `iterations`, default 2500 — analog zum Default von [sf-tools](https://github.com/HafisCZ/sf-tools/blob/gh-pages/guilds.html)) - `POST /api/simulate/abort`, `GET /api/simulate/progress` - `POST /api/refresh-own-fighters` — erzwingt sofortiges Neuladen der eigenen Kampfwerte ## Pilzkatapult Bis zu 3 Riesenpilze treffen vor Kampfbeginn je ein zufälliges Gegner-Mitglied und ziehen ihm **additiv** 50 Prozentpunkte seines ursprünglichen Lebens ab (2 Treffer auf dasselbe Ziel = 0% Leben, nicht 25% wie bei multiplikativem Stacking — das war ein Bug in einer früheren Version, siehe Commit-Historie). Ziele werden pro simuliertem Einzelkampf neu gewürfelt, nicht einmal für den ganzen Iterations-Batch. ## Cache der eigenen Kampfwerte Damit nicht vor jeder Simulation alle eigenen aktiven Mitglieder per `ViewPlayer` neu geholt werden müssen, aber auch keine veralteten Werte benutzt werden: - Cache bleibt gültig, solange sich das Aktiv-Roster (< 24h offline) nicht ändert **und** er jünger als 6h ist. - Ändert sich das Roster (Zu-/Abgang, Aktiv-Status-Wechsel) oder ist er älter → automatischer Refresh. - Zusätzlich manueller „🔄 Eigene Kampfwerte aktualisieren"-Button, unabhängig davon. ## Architektur-Fix `ScanProgress`/`cancel_requested` lagen bisher in `AppState`, das für die gesamte Dauer jedes Spielserver-Requests gesperrt ist (die `SimpleSession` lebt dort) — `/api/progress` und `/api/scan/abort` konnten dadurch hinter einem laufenden Request hängen bleiben. Jetzt in einem eigenen `ProgressState`-Lock (`AppHandles` bündelt beide Locks als Axum-State). Gleiches Muster für die neue Simulation, die durch deutlich mehr `ViewPlayer`-Calls das Problem sonst verschärft hätte. ## Test - `cargo build --all-targets` kompiliert ohne Warnungen (Server + `sim_probe`-Example). - JS-Syntax geprüft (`node --check`), alle `$('id')`-Referenzen im Script gegen vorhandene HTML-IDs abgeglichen. - Funktionales Testen im Browser gegen einen echten Account steht noch aus. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Wegwerf-Testbinary zur Validierung von sf_api::simulate gegen echte
Gildenkampf-Ergebnisse, bevor die Simulation in den Server integriert
wird.

- Eigene Seite: nur aktive Mitglieder (< 24h offline), da nur die bei
  einem selbst gestarteten Angriff antreten. Eigener Charakter kommt
  kostenlos aus dem GameState (kein ViewPlayer-Call nötig).
- Gegner-Seite: komplette Gilde per ViewPlayer, kein Offline-Filter
  (bei einem gegnerischen Angriff verteidigt immer die ganze Gilde).
- Beide Seiten aufsteigend nach Level sortiert (schwächster zuerst,
  Last-Man-Standing-Regel).
- is_arena_battle=false als Arbeitsannahme, gegen echte Kämpfe zu
  prüfen.
- Pacing/Zeitbudget-Werte (700ms, 90s) analog zu
  sfguildsv2/rust_examples/character_sync.rs übernommen.

sf-api 0.4.4 (aktuell aufgelöste Version) enthält das simulate-Modul
bereits per Default-Feature, kein Cargo.toml-Änderung nötig.
Vor Kampfbeginn geladene Riesenpilze (bis zu 3) treffen laut Helpshift
je einen zufällig gewählten Gegner und ziehen ihm 50% max_health ab.

- Neue Env-Var MUSHROOMS (0-3, default 0).
- Ziele werden PRO simuliertem Einzelkampf neu gewürfelt (nicht einmal
  für alle Iterationen), sonst würde jede Wiederholung denselben vorab
  geschwächten Gegner-Stand nutzen und die Streuung verfälschen.
- Modelliert als unabhängige Ziehungen mit Zurücklegen (derselbe
  Gegner kann theoretisch mehrfach getroffen werden) - Annahme, noch
  nicht gegen echte Kämpfe verifiziert.
- fastrand als Dev-Dependency für die Zielauswahl ergänzt (dieselbe
  Crate, die sf-api intern bereits nutzt).
- Cargo.lock erstmals committed, damit dieser validierte
  Abhängigkeitsstand (sf-api 0.4.4 u.a.) reproduzierbar bleibt.
Realer Testfall: bei einer 1-Mitglied-Gegnergilde treffen sich 2
geladene Pilze zwangsläufig gegenseitig auf dasselbe (einzige) Ziel.
Nutzer bestätigt zuverlässigen Sieg mit 2 Pilzen in echten Kämpfen -
die alte multiplikative Rechnung (100% -> 50% -> 25%) sagte aber nur
0.6% Sieg-Chance voraus.

Der Helpshift-Text "zieht 50% der Lebensenergie ab" liest sich additiv:
jeder Treffer zieht 50 Prozentpunkte vom URSPRÜNGLICHEN max_health ab,
nicht 50% vom jeweiligen Rest. Zwei Treffer auf dasselbe Ziel = 0%
Leben übrig (praktisch tot vor Kampfbeginn), nicht 25%.

Noch zu verifizieren: ob diese Korrektur das reale Ergebnis jetzt
trifft, und ob die Annahme (unabhängige Zielziehung mit Zurücklegen)
stimmt.
Matcht den Default-Wert (sim-iterations = 2500), den sf-tools'
Gildenkampf-Simulator selbst verwendet (guilds.html Zeile 84). Bei 500
Iterationen lag der Standardfehler bei kleinen Effekten (z.B. 3 Pilze
gegen eine 48-köpfige Gilde) bei ca. ±1.1 Prozentpunkten - zu hoch, um
solche Effekte verlässlich von Rauschen zu unterscheiden.
# Conflicts:
#	Cargo.lock
Baut die in examples/sim_probe.rs validierte Kampfsimulation
(sf_api::simulate, additives Pilzkatapult-Stacking, 2500 Iterationen)
in den eigentlichen Server ein.

Neu:
- POST /api/simulate: simuliert ausgewählte Gilden im Hintergrund
  (guild_names, mushrooms 0-3, iterations), analog zum bestehenden
  Scan-Hintergrund-Task.
- POST /api/simulate/abort, GET /api/simulate/progress.
- POST /api/refresh-own-fighters: erzwingt sofortiges Neuladen der
  eigenen Kampfwerte, unabhängig vom Cache.
- Cache der eigenen aktiven Kampfwerte (OwnFightersCache): wird
  wiederverwendet, solange sich das Aktiv-Roster nicht geändert hat
  UND er jünger als 6h ist - sonst automatisch aufgefrischt. Dadurch
  keine unnötigen ViewPlayer-Calls bei mehreren Simulationen kurz
  hintereinander, aber auch keine dauerhaft veralteten Stats/Aktiv-
  Status (siehe Diskussion zu "unregelmäßig aktualisieren").
- DetailedGuild um sim_win_ratio/sim_mushrooms/sim_iterations/
  sim_evaluated_at erweitert, wird nach einer Simulation in scan_data
  persistiert.
- Frontend: "🎲 Simulieren"-Button + Pilz-Auswahl (0-3) bei der
  bestehenden Checkbox-Gildenauswahl, eigene Progress-Anzeige, neue
  "Sim"-Spalte (sortierbar) in der Ergebnistabelle, Sim-Info im
  Detail-Modal, "🔄 Eigene Kampfwerte aktualisieren"-Button bei der
  Gildenkarte.

Architektur-Fix (siehe erste Analyse des Repos):
- ScanProgress/cancel_requested waren bisher Teil von AppState und
  damit für die gesamte Dauer jedes einzelnen Spielserver-Requests
  gesperrt (die SimpleSession lebt dort) - /api/progress und
  /api/scan/abort konnten dadurch hinter einem laufenden Request
  hängen bleiben. Jetzt in einem eigenen ProgressState/SharedProgress-
  Lock, containerisiert in AppHandles zusammen mit dem bisherigen
  SharedState. Gleiches Muster für die neue Simulation (SimProgress/
  cancel_sim), die durch die vielen zusätzlichen ViewPlayer-Calls das
  Problem sonst verschärft hätte.
- fastrand von Dev- zu normaler Dependency (wird jetzt auch vom
  Server-Binary für die Pilz-Zielauswahl gebraucht, nicht mehr nur
  vom sim_probe-Example).

Lokal getestet: cargo build --all-targets kompiliert ohne Warnungen
(main-Binary + sim_probe-Example). Funktionales Testen im Browser
gegen einen echten Account steht noch aus.
DasAoD merged commit 2e4fa9af3f into main 2026-08-10 23:43:08 +02:00
Sign in to join this conversation.
No description provided.