Das gesamte Spiel ist über diese API spielbar (Web-Client & externe Bots). Basis: REST + JSON unter
/api/v1, plus WebSocket für Live-Kämpfe & Events. FastAPI liefert zusätzlich automatische OpenAPI/Swagger-Doku unter/docs. Status: Bereit für Umsetzung · 2026-07-02
https://api.seekampf.de/api/v1 (oder https://seekampf.de/api/v1).{ "error": { "code", "message" } }.Authorization: Bearer <JWT> (Browser) oder
X-API-Key: <key> (Bots). Beide gleichwertig für Spielaktionen.
Einen Key erzeugt man entweder direkt über POST /me/apikeys (s. §2) oder
bequem über den Web-Client selbst: Einstellungen → API-Zugriff — der
Klartext-Key erscheint dort genau einmal direkt nach dem Erzeugen, der
Server speichert danach nur noch den Hash.Idempotency-Key-Header).?limit=&offset=. Rate-Limits pro Key.GET /auth/captcha → Bot-Schutz für die Registrierung:
{challenge_id, frage} — einfache
Rechenaufgabe, NUR EINMAL gültig
(verfällt nach dem ersten
POST /auth/register-Versuch,
egal ob richtig/falsch
beantwortet), Ablauf nach
CAPTCHA["gueltigkeit_s"] (10 Min.)
ohnehin. Rate-Limit "captcha"
(großzügiger als "register" —
legitime Tippfehler/Neuladen).
POST /auth/register {name, email, password,
challenge_id, captcha_antwort}
→ Account + Startinsel (Login erst
nach E-Mail-Bestätigung!). 422
invalid_captcha bei falscher/
fehlender/abgelaufener Antwort —
dann NEUE Aufgabe über
GET /auth/captcha holen (die alte
ist verbraucht). Betrifft NUR die
Registrierung selbst — ab einem
bestehenden Account ist wie immer
jede Spielaktion per API/Bot ohne
jede weitere Einschränkung nutzbar
(BOT-GUIDE.md §0).
POST /auth/login {name|email, password} → {access, refresh}; 403 email_not_verified vor Bestätigung
POST /auth/refresh {refresh} → {access}
POST /auth/logout
POST /auth/verify-email {token} → E-Mail bestätigen (schaltet Login frei)
POST /auth/resend-verification {email} → Bestätigungsmail erneut senden (neutrale Antwort)
POST /auth/forgot-password {email} → Reset-Mail (SMTP, siehe ARCHITEKTUR §3a)
POST /auth/reset-password {token, new_password} → Passwort neu setzen
POST /auth/change-password {old_password, new_password} → Passwort ändern (eingeloggt)
GET /me → Profil, Punkte, Allianz, Bio
— `punkte` hier LIVE (einzige
Ausnahme im gesamten System,
überall sonst kommen Punkte
aus dem täglichen Snapshot)
POST /me/profile {bio} → kurzes Spieler-Profil setzen
GET /users/{user_id}/profile → öffentliches Profil (Bio,
Punkte, Inselliste Name/
Koordinaten/Punkte/
Tageszuwachs — KEINE
Rohstoffe/Gebäude/Truppen;
zusätzlich: `online` (bool,
letzte Aktivität ≤
ONLINE_SCHWELLE_MIN=5 Min.),
`allianz` ({id,tag,name} oder
null), `quests_erfuellt`
(öffentlich sichtbare
Auszeichnungen, siehe §12).
`punkte` (Gesamt UND je Insel
in `inseln`) kommt aus dem
täglichen Tages-Snapshot,
NICHT live — gilt auch fürs
EIGENE Profil unter dieser
Route (nur `/me` ist live).
Jede Insel in `inseln` hat
zusätzlich ein `differenz`-Feld
= fertig berechneter
Punkte-Zuwachs/-Verlust
VOM VORTAG (NICHT der
laufende Zuwachs seit
Mitternacht) — kommt aus
`Island.punkte_differenz`,
einmal täglich beim
Rollover aus dem ALTEN
`punkte_snapshot`
berechnet, danach stabil
bis zum nächsten Tag).
`null` vor dem zweiten
Snapshot der Insel.
GET /users/lookup ?name= → wie oben, per Name gesucht
GET /me/islands → Liste eigener Inseln
GET /me/islands/overview → alle eigenen Inseln kompakt:
je Insel {id, name, koordinaten,
punkte, rohstoffe (inkl. kapazitaet),
truppen, schiffe, gebaeude
(Stufen, OHNE deaktivierte wie
Marktplatz/Labor), bauauftrag_aktiv,
bedrohung_im_anflug,
handel_im_anflug} — für den
Inseln-Tab (Web-Client zeigt
Einheiten/Schiffe ODER Gebäude,
umschaltbar). Zwei getrennte Booleans
statt einem: `bedrohung_im_anflug`
NUR bei Angriff/Spionage/Kolonisation,
`handel_im_anflug` bei ausschließlich
bestätigtem Handel zu dieser Insel
(rot vs. blau im Web-Client) — beide
schließen sich gegenseitig aus.
POST /me/current-island {island_id} → aktive Insel wechseln
# API-Keys für Bots
GET /me/apikeys → Liste eigener Keys (id, prefix,
label, scopes, last_used_at,
created_at — NIE der Klartext,
der Server speichert nur den Hash)
POST /me/apikeys {label, scopes} → einmalig Klartext-Key
({id, key, prefix, label, hinweis})
— wird SONST NIE WIEDER angezeigt,
auch nicht über GET /me/apikeys
DELETE /me/apikeys/{id} → widerrufen (sofort ungültig)
# DSGVO
GET /me/export → Datenexport (Auskunft, JSON)
DELETE /me {password} → Account löschen (Recht auf Vergessenwerden)
GET /legal/impressum → Impressum (HTML/Markdown)
GET /legal/datenschutz → Datenschutzerklärung
GET /legal/regeln → Spielregeln
Im Footer des Web-Clients verlinkt. Inhalte editierbar (Template/Markdown), Werte liefert der Betreiber.
GET /islands/{id} → Insel-Detail (Rohstoffe live, Produktion/h,
Lager, Gebäude+Stufen, Garnison, Schiffe,
laufende Aufträge, Punkte)
GET /islands/{id}/resources → aktueller Bestand + Produktion + Kapazität
Lagerkapazität ist eine harte Grenze für JEDEN Zugang — nicht nur für die passive
Produktion (die sowieso stoppt, sobald das Lager voll ist), sondern genauso
für Flottenrückkehr (Beute, §7), Quest-Belohnungen (§12) und Ausbildungs-
Stornierung (§5): POST/interner economy.gutschreiben() kappt IMMER auf
lager_kapazitaet(lagerhaus_stufe). Was über die Kapazität hinausgeht, geht
in diesen Fällen ersatzlos verloren — kein Überlauf, keine API gibt einen
Hinweis darauf zurück, wie viel tatsächlich verloren ging (die Antwort
enthält weiterhin den vollen angeforderten/erwarteten Betrag, nicht den
tatsächlich gutgeschriebenen). Ein bereits VORHANDENER Überbestand (z. B.
nach Lagerhaus-Rückstufung durch Razzia) wird dadurch nicht zerstört — nur
was NEU dazukäme, wird gekappt.
Zwei Ausnahmen, bei denen ein Überschuss NICHT verloren geht:
1. Allianzkasse-Auszahlung (§9) rechnet ihre Kapazitätsgrenze selbst
separat und leitet Überschuss aktiv zurück in die Kasse.
2. Handel/Transport-Lieferung (§7, inkl. Handelsrouten §10): passt eine
Rohstoff-Lieferung nicht
komplett ins Lager der Zielinsel, nimmt die Flotte den Überschuss mit
zurück zur Heimatinsel (services/fleets._deliver() setzt fleet.
resources auf den nicht zugestellten Rest, NUR bei mission="transport"
— echte Handelsschiffe kehren ohnehin heim). Der Handelsbericht nennt
explizit sowohl die tatsächlich angekommene Menge als auch den
zurückfahrenden Überschuss (payload.ueberschuss). Erst wenn AUCH die
Heimatinsel bei Rückkehr keinen Platz mehr hat, geht der Rest endgültig
verloren (normale Kappung bei _unload_at_home()). Eine abstrakte
Markt-Lieferung (mission="market", aktuell irrelevant — Marktplatz
deaktiviert) hat KEINE Rückfahrt, dort bleibt ein Überschuss verloren.
GET /islands/{id}/buildings → alle Typen + Stufen
(haupthaus_ab, verfuegbar)
GET /islands/{id}/buildings/{type}/detail → Beschreibung + volle
Stufentabelle (Kosten/Zeit/
Effekt/Freischaltung)
POST /islands/{id}/buildings/{type}/upgrade → in Bau-Warteschlange (max 3)
GET /islands/{id}/construction → aktiver Bau + Queue
DELETE /islands/{id}/construction/{orderId} → Auftrag abbrechen
POST /islands/{id}/rename {name} → Insel umbenennen
Fehler wenn: Voraussetzung (Haupthaus-Stufe — Kaserne ab 5, Hafen ab 7,
Labor ab 10, Marktplatz ab 10, Steinmauer/Wachturm ab 1) fehlt, Rohstoffe
fehlen, Queue voll, Max-Stufe erreicht, oder das Gebäude deaktiviert ist
(aktuell: Marktplatz, Labor — 409 building_disabled, siehe
balancing.DEAKTIVIERTE_GEBAEUDE; Code/Daten bleiben vollständig erhalten).
POST /islands/{id}/training {facility: kaserne|hafen, item, count}
GET /islands/{id}/training → aktive Ausbildung + Queue
DELETE /islands/{id}/training/{orderId}
GET /islands/{id}/research
POST /islands/{id}/research/{tech}/upgrade
Baubarkeit prüft Kaserne-/Hafen-Stufe (z. B. Koloschiff = Hafen 20).
Forschung (speer/schild/bogen/segel/spionage/engineering, je Stufe +10 %
Effekt auf Angriff/Verteidigung/Schiffsgeschwindigkeit/Spionage-Chance/
Bauzeit) ist aktuell deaktiviert: POST /research/{tech}/upgrade →
409 feature_disabled, und research.forschung_stufen() liefert serverseitig
nur noch 0 zurück (wirkt sich also auch nirgends mehr aus) — Code/Daten
bleiben vollständig erhalten, System ist für eine Reaktivierung vorbereitet.
GET /map/sector/{x}/{y} → 5×5 Inseln des Sektors (Basis-Infos:
Besitzer/Name/Allianz/Punkte; KEINE Truppen/Rohstoffe)
GET /map/region/{x}/{y} → 3×3-Sektoren-Ausschnitt (15×15 Inseln) in
einer Antwort — fürs Ozean-Raster
GET /map/island/{x}/{y}/{z} → öffentliche Hover-Infos einer Insel; für
eingeloggte Betrachter zusätzlich
`eigene_berichte` — letzte 10, neueste
zuerst: eigene Angriffe auf GENAU diese
Insel UND
eingehende Angriffe vom aktuellen Besitzer
dieser Insel gegen eine eigene Insel;
ungefiltert nach `Message.hidden` — im
Postfach ausgeblendete Berichte bleiben
hier trotzdem sichtbar
# Detail-Infos (Truppen/Rohstoffe) NUR via Spionage (siehe Kampfbericht)
punkte in ALLEN drei Endpunkten oben (Sektor/Region/Insel) kommt aus dem
täglichen Tages-Snapshot, NICHT live — eine brandneue Insel ohne bisherigen
Snapshot zeigt live, sonst stünde sie permanent mit 0 Punkten da.
POST /fleets {origin_island_id, mission_type, target:{x,y,z},
ships:{type:count}, units:{type:count}, resources:{...},
razzia_ziel?, razzia_prioritaet?}
(beide nur für mission_type=attack)
→ berechnet Fahrzeit/Ankunft; validiert Ladevolumen & Kapazität
(Fahrzeit-Minimum 5 Min., innerhalb
desselben Sektors nur 2 Min., `formulas.
fahrzeit_minuten(..., gleicher_sektor)`)
GET /fleets → eigene Flotten (outbound/returning) + Status,
inkl. `origin` ({x,y,z,name} —
`origin_island_id` allein ist für den
Client nur eine ID) und `target`
({x,y,z,name}). `name` ist NULL, wenn dort
keine Insel-Zeile existiert (leeres Wasser) —
sonst frei einsehbar wie im Ozean-Popup,
keine zusätzliche Aufklärung.
GET /fleets/incoming → auf eigene Inseln anfliegende Flotten,
eigene UND fremde (Sichtbarkeit je
Wachturm-Stufe DER ZIELINSEL — auch eigene
Flotten erst innerhalb der Sichtweite).
Bei fremden Flotten sind `absender`,
`allianz_flagge` ({tag, icon, farbe} |
null, s. §9) UND `mission` (attack/transport/
spy/colonize) IMMER Teil der Antwort,
unabhängig von der Wachturm-Stufe —
`ships`/Einheiten-Anzahl fehlen bei fremden
Flotten dagegen GRUNDSÄTZLICH (keine
Wachturm-Stufe schaltet das frei,
`WACHTURM_DETAILS_AB_STUFE` entfernt; der
Wachturm erhöht nur noch die Sichtweite:
1 Feld/Stufe, gedeckelt auf max. 20 Felder —
`WACHTURM_SICHTWEITE_PRO_STUFE`/
`WACHTURM_SICHTWEITE_MAX`).
Eigene Flotten in der Liste haben immer
volle Details (`ships`, `units`).
+ eigene Flotten, die eine eigene
Insel gerade verlassen haben
({abfahrend: true, herkunft_insel_id,
herkunft_koordinaten, sichtbar_bis} —
sichtbar exakt solange, wie sie auch
zurückgerufen werden könnte) +
vorbeifahrende fremde Flotten
({vorbeifahrend: true, ziel_insel_id,
absender, allianz_flagge, richtung} —
"da fliegt was",
Absender ("Flagge") + grobe Himmelsrichtung
(eine von acht 45°-Sektoren: Norden,
Nordosten, Osten, Südosten, Süden,
Südwesten, Westen, Nordwesten) sichtbar,
aber KEINE Herkunft/Ziel-Koordinaten/Mission)
POST /fleets/{id}/recall → Rückruf (nur solange in Wachturm-Sichtweite)
mission_type: nur noch "attack" oder
"handel" — die konkrete Abwicklung (in GET /fleets als mission sichtbar:
attack/colonize/spy/transport) ergibt sich automatisch aus der Fracht:
- attack + NUR Spähschiffe an Bord (keine Einheiten/Rohstoffe) → Spionage
— geht auf JEDE existierende Insel, auch unbewohnte (eine Ruine kann noch
eine Restgarnison/Rohstoffe haben). Nur echtes leeres Wasser (kein
Inselplatz) ist kein gültiges Ziel.
- attack + Kolonisationsschiff an Bord + Ziel ist eine FREIE Insel (owner =
niemand) → Kolonisation. Ist das Ziel bewohnt, bleibt es ein normaler
Angriff — das mitgeführte Kolonisationsschiff ist dann Teil der
Eroberungs-Mechanik (gelingt die Eroberung, wird es verbraucht, genau wie
bei einer normalen Kolonisation), NICHT der Kolonisation einer freien Insel.
- attack sonst → normaler Angriff/Plünderung.
- handel → Transport: Einheiten/Rohstoffe an eine EXISTIERENDE Insel liefern
(bewohnt oder unbewohnt/Ruine — z. B. um eine Ruine als Vorposten zu
bestücken). Einheiten UND Rohstoffe an JEDE existierende Insel — egal ob
eigene, Allianzmitglied, fremder Spieler oder unbewohnt (die frühere
D-Reinforce-Einschränkung auf eigene/Allianz-Inseln bei bewohnten fremden
Zielen ist entfernt).
Regeln: nur Einheiten+Rohstoffe transportierbar (keine Schiffe verlegen);
Rückkehr = gleiche Dauer; Beute fährt automatisch zur Heimatinsel; per handel
verschickte Einheiten gehen dauerhaft in die Garnison der Zielinsel über.
Handelsbericht: bei Ankunft einer
Transport-/Markt-Lieferung (services/fleets._deliver()) bekommt der
Absender IMMER eine Postfach-Nachricht (payload.typ == "handel",
folder="combat" — liegt im selben Ordner/Reiter wie Kampf-/Spionage-
berichte, NICHT im normalen Postfach; kein battle_id/Bericht-Link, reiner
Text) mit der TATSÄCHLICH
angekommenen Menge (Vorher/Nachher-Differenz, gedeckelt auf die
Lagerkapazität der Zielinsel — Einheiten haben kein Kapazitätslimit). Gehört
die Zielinsel jemand anderem als dem Absender, bekommt zusätzlich der
Insel-Besitzer einen eigenen Bericht mit Absendername (payload.absender).
Gilt für JEDE _deliver()-Lieferung, also auch für automatische
Handelsrouten-Läufe (§ Handel) und — sobald der Marktplatz reaktiviert wird
— Marktangebots-Zustellungen. Wie Kampf-/Spionageberichte wird ein
gelöschter Handelsbericht nur hidden gesetzt (historisches Archiv), nicht
wirklich gelöscht (DELETE /messages/{id}, folder == "combat"-Zweig).
Kolonisation zielt NUR auf eine existierende, unbewohnte Insel (owner =
niemand) — leeres Wasser ohne Inselplatz ist nicht kolonisierbar. Der
Weltgenerator legt regelmäßig neue unbewohnte Inseln an (siehe ARCHITEKTUR).
razzia_ziel (Gebäudetyp) ist optional — ohne Angabe raziert der Server bei
Sieg automatisch das Haupthaus.
Kolonisation übernimmt die Insel 1:1:
services/fleets._kolonisieren() setzt NUR owner_id/name/
punkte_snapshot neu — Gebäudestufen bleiben komplett unverändert (bei
einer Ruine also inkl. bereits ausgebauter Minen etc.), mitgeführte Fracht
der Flotte wird zum bisherigen Rohstoffbestand der Insel ADDIERT (nicht
ersetzt), normale Lagerkapazitäts-Deckelung. Eine evtl. vorhandene
Restgarnison/Schiffe bleiben ebenfalls erhalten. Dasselbe gilt für eine
Eroberung (§8) — dort war es schon immer so. Bei JEDER Kolonisation (Erfolg
UND Fehlschlag) geht eine Nachricht in folder="combat"
(payload.typ == "kolonisation", payload.erfolg: bool) — analog zum
Handelsbericht oben.
razzia_prioritaet (Rohstofftyp gold/stein/holz, optional): steuert die Plünderung bei Sieg. Ohne Angabe wie
bisher "water filling" — die Handelskapazität wird gleichmäßig auf alle noch
verfügbaren Rohstoffsorten verteilt (§ Kampf-Details/combat.pluenderung()).
MIT Angabe wird NUR dieser eine Rohstoff geplündert — alle anderen Sorten
werden komplett ignoriert, auch wenn dadurch das Ladevolumen nicht
ausgeschöpft wird (Schiffe kommen ggf. nicht voll beladen zurück, z. B. wenn
weniger vom priorisierten Rohstoff auf der Zielinsel liegt als Kapazität da
ist). Landet auf Fleet.meta.razzia_prioritaet, analog zu razzia_ziel.
GET /battles/{id} → Bericht, SOFORT ab Ankunft der Flotte
vollständig verfügbar (kein Live-Playback,
keine Berichtssperre — beides 2026-07-19
entfernt). Sichtbarkeit je Rolle:
Verteidiger immer vollständig, Angreifer
Kampfverlauf IMMER + Zusatz-Aufklärung nur
bei Spähschiff+Sieg, s. 14a.1/14a.4.
POST /alliances {name, tag}
GET /alliances/{id} → Info, Mitglieder (inkl. Inselanzahl je
Mitglied), Diplomatie, Punkte
POST /alliances/{id}/invite {user_id} (leader/diplomat)
POST /alliances/{id}/join | /leave | /kick
POST /alliances/{id}/roles {user_id, role}
POST /alliances/{id}/kasse/deposit | /withdraw {island_id, resources}
→ {id, richtung, resources, fertig_at, kasse}
(deposit: kein kasse-Feld. withdraw als
Anführer: fertig_at gesetzt, status="aktiv".
withdraw als Diplomat: fertig_at=null,
status="wartet_auf_freigabe" — s. u.)
POST /alliances/{id}/kasse/withdraw/{auftrag_id}/approve | /reject
(nur Anführer — Freigabe/Ablehnung eines
wartenden Diplomaten-Auszahlungsantrags)
POST /alliances/{id}/kasse/{auftrag_id}/cancel
(nur der Antragsteller — bricht einen
LAUFENDEN Transfer ab, s. u.)
GET /alliances/{id}/kasse/log ?limit&offset (Default 15/0) → ABGESCHLOSSENE Ein-/
Auszahlungen (wer, wann, Richtung, Menge),
neueste zuerst — für jedes Mitglied
einsehbar. Bleibt eine reine Liste (kein
Gesamt-Zähler) — eine volle Seite
(Länge == limit) heißt "es könnte mehr
geben".
GET /alliances/{id}/kasse/bilanz
→ Buchhaltung je Mitglied (s. u.), für
jedes Mitglied einsehbar.
POST /alliances/{id}/diplomacy {other_alliance_id, type: krieg|buendnis|nap|neutral}
POST /alliances/{id}/broadcast {message} (Rundbrief/Chat)
POST /alliances/{id}/description {text} (leader/diplomat — öffentlicher Beschreibungstext)
GET /alliances/flaggen-optionen → {icons: [...], farben: {schluessel: hex}}
(festes wählbares Set, s. u. — Route MUSS
vor /{id} geroutet sein)
POST /alliances/{id}/flagge {icon, farbe} (leader/diplomat) → {icon, farbe}
GET /users/lookup ?name= → öffentliches Profil (für Einladungen; siehe §2)
Diplomatie-Semantik: krieg wirkt einseitig & sofort. buendnis/nap sind
Angebote — aktiv, sobald die Gegenseite denselben Typ setzt. neutral kündigt
Angebote/Bündnisse/NAPs sofort; ein laufender Krieg endet erst, wenn BEIDE
Seiten neutral setzen (Friedensangebot wird protokolliert).
handel-Missionen (§7) mit Einheiten sind zu JEDER Insel erlaubt, bewohnt
oder unbewohnt, unabhängig von Allianz-Zugehörigkeit.
Die punkte je Mitglied in der Mitgliederliste kommen aus dem täglichen
Tages-Snapshot, NICHT live — Name/Inselanzahl/Rolle bleiben live.
Allianz-Flagge: Icon + Grundfarbe aus einem
festen Set (balancing.ALLIANZ_FLAGGEN, kein Freitext/Upload), setzbar von
Anführer/Diplomat über POST /alliances/{id}/flagge. GET /alliances/{id}
liefert zusätzlich flagge: {icon, farbe} | null (null = noch keine gewählt).
farbe ist der SCHLÜSSEL aus ALLIANZ_FLAGGEN["farben"] (z. B. "blau"),
nicht der Hex-Wert — den liefert GET /alliances/flaggen-optionen. Dieselbe
Flagge erscheint außerdem bei fremden Flotten in Wachturm-Sichtweite: GET
/fleets/incoming (§7) liefert für jeden fremden absender-Eintrag
(ankommend UND vorbeifahrend) zusätzlich allianz_flagge: {tag, icon, farbe}
| null.
24h-Sperrzeit: Sobald eine Diplomatie-
Beziehung einen settled Zustand erreicht (Krieg erklärt, Bündnis/NAP
bestätigt, Frieden geschlossen, Bündnis/NAP gekündigt), ist ein Wechsel in
einen ANDEREN Typ für ALLIANZ["diplomatie_sperre_stunden"] (Default: 24h)
gesperrt — verhindert Hin- und Her-Wechseln zwischen den Zuständen im
Minutentakt. Ein Versuch während der Sperre liefert 409 diplomacy_locked
mit dem Zeitpunkt, ab dem wieder geändert werden darf. Laufende Angebote/
Friedensverhandlungen (Status angeboten/friede_angeboten) sind von der
Sperre NICHT betroffen — Annehmen/Ablehnen/Zurückziehen geht immer sofort.
Eine gekündigte Beziehung wird intern als neutral mit Sperrfrist
gespeichert (statt gelöscht) und verschwindet aus der Diplomatie-Liste,
sobald die Sperre abgelaufen ist.
Kasse-Transferzeit: Ein-/Auszahlung wirken
NICHT mehr sofort, sondern erst nach ALLIANZ["kasse_transferzeit_h"]
(Default 2h, unter Turbo TURBO_FAKTOR-mal kürzer wie überall sonst).
Einzahlung: Rohstoffe werden SOFORT von der Insel abgezogen, kommen aber
erst nach der Transferzeit in der Kasse an. Auszahlung durch den ANFÜHRER:
der Betrag wird SOFORT aus der Kasse gebucht (reserviert, kein Doppel-
Ausgeben), kommt aber erst nach der Transferzeit auf der Zielinsel an —
Lagerkapazität wird ERST BEI ANKUNFT geprüft (kann sich in 2h ändern), was
dann nicht passt, geht zurück in die Kasse.
Kasse-Deckel: ALLIANZ["kasse_kappung"]
(150.000) je Rohstoff — greift ebenfalls erst bei ANKUNFT einer fälligen
Ein-/Auszahlung, analog zur Lagerkapazität einer Insel. Was darüber ankäme,
geht verloren (kein Zurückbuchen, keine Fehlermeldung beim Einzahlen selbst
— der Betrag wird wie gehabt sofort von der Insel abgezogen). Der Kasse-Log
(GET /alliances/{id}/kasse/log) zeigt dabei die TATSÄCHLICH gutgeschriebene
Menge, nicht den ursprünglich eingezahlten Betrag.
GET /alliances/{id} liefert
für Mitglieder zusätzlich kasse_unterwegs: [{id, richtung, resources,
spieler, user_id, fertig_at, status, insel}] — alle noch nicht abgeschlossenen
Transfers. status: "wartet_auf_freigabe" | "aktiv" | "storniert"
(s. u.). user_id identifiziert den
Antragsteller — der Client zeigt ihm damit den Abbrechen-Button nur bei
eigenen Transfers. insel: {name, koordinaten} | null — NUR beim eigenen
Eintrag gefüllt (user_id == Aufrufer), bei fremden Einträgen immer null,
auch wenn der Rest sichtbar bleibt.
Einzahlung gesperrt bei erkanntem Angriff:
POST .../kasse/deposit liefert 409 attack_incoming, wenn der Wachturm
der einzahlenden Insel gerade eine anfliegende Angriffsflotte in Sichtweite
hat (dieselbe Sichtweiten-Logik wie GET /fleets/incoming) — verhindert,
Rohstoffe kurz vor einem erkannten Angriff in die Kasse zu "retten".
Betrifft nur Einzahlungen, keine Auszahlungen.
Erwartete Zahlung: GET /islands/{id}
liefert zusätzlich kasse_zahlung_erwartet_at: datetime | null — Zeitpunkt
der nächsten bereits laufenden (nicht mehr wartenden) Allianz-Kasse-
Auszahlung, die auf DIESER Insel ankommt.
GET .../kasse/log zeigt weiterhin nur ABGESCHLOSSENE Bewegungen (mit der
tatsächlich angekommenen Menge, bei Auszahlung ggf. weniger als angefordert
wegen Lagerkapazität).
Kasse-Gebühr: jede erfolgreiche Ein- ODER
Auszahlung kostet ALLIANZ["kasse_gebuehr_anteil"] (Default 12%) Gebühr,
abgerundet (floor). Die Gebühr wird ERST BEI ANKUNFT abgezogen (nicht beim
Antrag) — resources in der POST .../deposit//withdraw-Antwort und in
kasse_unterwegs ist weiterhin der VOLLE angeforderte Betrag; erst der
tatsächlich gutgeschriebene (Netto-)Betrag in GET .../kasse/log und
.../kasse/bilanz ist bereits um die Gebühr bereinigt. Bei Auszahlung wird
der Kasse trotzdem der VOLLE angeforderte Betrag belastet (die Kasse muss
also den vollen Betrag decken können, nicht nur den Netto-Betrag) — die
Gebühr "verschwindet" einfach, sie fließt niemandem zu. Ein stornierter
Transfer (s. u.) ist GEBÜHRENFREI — das ist keine Ein-/Auszahlung, sondern
nur die Rückgabe des eigenen Geldes zum Ursprung.
Rolle "Schatzmeister" + Auszahlungs-Freigabe: vierte Allianz-Rolle NUR für die Kasse zuständig (keine
invite/diplomatie/rundbrief-Rechte wie Anführer/Diplomat). JEDES Mitglied
darf eine Auszahlung ANFRAGEN (POST .../kasse/withdraw, vorher nur
Anführer/Diplomat). Anführer UND Schatzmeister können sich dabei selbst
jederzeit SOFORT auszahlen (status: "aktiv", fertig_at gesetzt — wie
oben) UND fremde Anträge freigeben/ablehnen. Alle anderen Anträge (Diplomat,
Mitglied) belasten die Kasse NOCH NICHT — der Auftrag bekommt
fertig_at: null, status: "wartet_auf_freigabe". Erst
POST .../kasse/withdraw/{auftrag_id}/approve (Anführer ODER Schatzmeister)
belastet die Kasse (erneute Prüfung — kann sich seit dem Antrag geändert
haben) und startet die Transferzeit; POST .../reject verwirft den Antrag
ersatzlos (Kasse wurde nie belastet). Beide Aktionen schicken dem
Antragsteller eine Postfach-Meldung (Ergebnis). Die Benachrichtigung AN
Anführer/Schatzmeister über einen NEUEN Antrag ist dagegen KEINE
Postfach-Meldung, sondern rein eine farbige Hervorhebung des
Allianz-Navireiters im Web-Client (aus kasse_unterwegs abgeleitet, kein
eigener Endpunkt). kasse_unterwegs-Einträge mit
status: "wartet_auf_freigabe" stehen zuerst in der Liste, unabhängig von
fertig_at (das ist bei denen ja null).
Transfer abbrechen: POST
/alliances/{id}/kasse/{auftrag_id}/cancel — NUR der Antragsteller
(auftrag.user_id), NUR für einen LAUFENDEN Transfer (fertig_at gesetzt,
status: "aktiv"), NUR einmal (ein bereits "storniert"er Transfer kann
nicht erneut abgebrochen werden — 404 not_found). Die Rohstoffe fahren
zum URSPRUNG zurück (Insel bei Einzahlung, Kasse bei Auszahlung) — die
Rückfahrt dauert GENAUSO LANGE wie die bisher verstrichene Zeit seit
Transferbeginn (analog zu fleets.recall_fleet, Rückweg = bisherige
Strecke), NICHT sofort und NICHT erneut die volle Transferzeit. Beispiel:
nach 10 Minuten abgebrochen ⇒ fertig_at wird auf "jetzt + 10 Minuten"
gesetzt. Antwort: {id, fertig_at, storniert: true}. Ein wartender
("wartet_auf_freigabe") Antrag kann NICHT über diesen Endpunkt storniert
werden (dafür gibt es .../reject, die Kasse wurde da ja noch nie belastet).
Buchhaltung: GET
/alliances/{id}/kasse/bilanz → [{user_id, spieler, einzahlung: {gold,
stein, holz}, auszahlung: {gold, stein, holz}, saldo: {gold, stein, holz}}]
— Summe ALLER abgeschlossenen Bewegungen je Mitglied, aus AllianceKasseLog
aggregiert (nicht aus kasse_unterwegs, die sind ja noch nicht
abgeschlossen). Die Beträge sind die NETTO-Beträge nach Abzug der
Kasse-Gebühr (s. o.) — ein stornierter Transfer zählt dabei ohne Gebühr (voller
Rückfluss). saldo = einzahlung − auszahlung je Rohstoff, bewusst
OHNE Untergrenze — ein Mitglied, das mehr entnommen als eingezahlt hat,
zeigt hier negative Werte ("Summiere eingezahlt und ausgezahlt zusammen,
auch wenn dann Minuszahlen entstehen"). Absteigend nach Gesamtvolumen
(Einzahlung + Auszahlung, NICHT nach Saldo) sortiert. Ein stornierter und
zurückgekehrter Transfer zählt hier als die TATSÄCHLICHE Bewegung (z. B.
eine abgebrochene Einzahlung, die zur Insel zurückkehrt, zählt als
Auszahlung), nicht als der ursprünglich beantragte Transfer. Gelöschte
Accounts UND Mitglieder, die die Allianz inzwischen verlassen haben oder
gekickt wurden, werden zu einer gemeinsamen Zeile user_id: null, spieler:
"unbekannt" zusammengefasst — nur AKTUELLE Mitglieder erscheinen namentlich
(wie beim Kasse-Protokoll, aber zusätzlich für Ex-Mitglieder mit noch
existierendem Account).
GET /market/offers ?want=&offer= → Angebote filtern
POST /market/offers {offer_res, offer_amount, want_res, want_amount}
POST /market/offers/{id}/accept {from_island_id} → erzeugt Transport-Fleet
DELETE /market/offers/{id}
# Freier (riskanter) Versand:
POST /transfers {from_island_id, target:{x,y,z}, resources}
# Handelsrouten (wiederkehrend, eigene Inseln):
GET/POST/DELETE /trade-routes POST: {from_island_id, to_island_id,
resources, interval_h}
Marktplatz/Marktangebote sind aktuell deaktiviert (POST /islands/{id}/
buildings/marktplatz/upgrade → 409 building_disabled; POST /market/offers
→ 409 feature_disabled) — Endpunkte, Code und Daten bleiben vollständig
erhalten, siehe balancing.DEAKTIVIERTE_GEBAEUDE. Markt-Semantik (F16b-
Defaults, Werte in balancing.py → MARKT), falls/wenn reaktiviert: Angebot
braucht Marktplatz ≥ 1; angebotene Menge + Gebühr wird beim Einstellen
hinterlegt (Escrow — das Verhältnis ist abgesichert). Kapazität je Angebot/
Annahme = 500 × Marktplatz-Stufe; max. Angebote je Insel = 2 + Stufe;
Gebühr = 10 % − 0,4 %/Stufe. Lieferungen fahren als abstrakte Markt-
Handelsschiffe (4 Knoten, echte Fahrzeit) — keine eigenen Schiffe nötig.
Zurückziehen/Ablauf erstattet die Menge, die Gebühr verfällt.
Freier Versand (/transfers) nutzt EIGENE Handelsschiffe (automatisch
gewählt, größte zuerst) — ohne jede Absicherung. Handelsrouten liefern mit
eigenen Handelsschiffen; fehlen Schiffe/Rohstoffe, wird der Lauf übersprungen.
Beide sind AKTIV, unabhängig vom deaktivierten Marktplatz.
GET /messages ?folder=inbox|combat (ignoriert, wenn archiv=true)
&archiv=true → NUR archivierte (hidden=true),
ordnerübergreifend gemischt
&limit=&offset= (Pagination, s. u.)
POST /messages {recipient, subject, body}
POST /messages/{id}/read
POST /messages/{id}/archive → Soft-Hide (hidden=true), jeder Nachrichtentyp
DELETE /messages/{id}
GET /messages/verlauf/{other_user_id}
→ Nachrichtenverlauf mit einem Spieler
DELETE /messages/{id}: bei folder=combat (Kampf-/Spionageberichte) NUR ein
Soft-Hide (hidden=true, verschwindet aus GET /messages) — Kampfberichte
werden nie wirklich gelöscht, sie bleiben für spätere Statistiken im System
(GRUNDBEDINGUNGEN §2.6b). Bei folder=inbox (normale Nachrichten) echtes
Löschen der Zeile.
Archiv: POST
/messages/{id}/archive setzt bei JEDER Nachricht (Kampf-/Spionagebericht
UND Postfach) hidden=true — dieselbe Soft-Hide-Mechanik wie das bisherige
DELETE bei folder=combat, nur als eigene benannte Aktion. Für Postfach-
Nachrichten ist das eine ZUSÄTZLICHE Option NEBEN dem weiterhin bestehenden
echten DELETE (Löschen) — der Web-Client zeigt dort beide Aktionen. Für
Kampf-/Spionageberichte ersetzt "archivieren" im Web-Client das bisherige
"ausblenden" 1:1 (kein separates Löschen dort, wie schon vorher).
GET /messages?archiv=true zeigt NUR archivierte Nachrichten, ignoriert
dabei folder komplett und mischt Kampf-/Spionageberichte UND Postfach-
Nachrichten chronologisch (neueste zuerst) in einer Liste — Pagination über
limit/offset wie gehabt (Web-Client nutzt limit=25 mit Vor-/Zurück).
GET /messages/verlauf/{other_user_id}
→ {spieler, nachrichten: [{id, subject, body, created_at, von_mir}]} —
BEIDE Richtungen chronologisch (älteste zuerst), nur echte Spieler-
Nachrichten (folder=inbox UND sender_id gesetzt — System-/Kampf-/
Spionageberichte sind kein Zwiegespräch und tauchen hier nie auf). Markiert
dabei automatisch alle noch ungelesenen Nachrichten VOM anderen Spieler AN
mich als gelesen (wie beim Öffnen einer Einzelnachricht). Message-Objekte
aus GET /messages tragen dafür zusätzlich sender_id (null bei System-/
Kampfberichten) — der Web-Client zeigt "Antworten" nur, wenn sender_id
gesetzt UND folder=inbox UND kein Kampf-/Spionage-/Einladungs-Payload.
GET /forum/threads ?sichtbarkeit=oeffentlich|allianz (optional; ohne
Parameter beide, je nach Sichtbarkeitsregel gemischt)
&limit=&offset=
POST /forum/threads {titel, body, sichtbarkeit?: "oeffentlich"|"allianz"}
sichtbarkeit-Default: "oeffentlich"
GET /forum/threads/{id} ?limit=&offset= → Thema + alle Beiträge
POST /forum/threads/{id}/posts {body} → Antwort auf ein Thema
POST /forum/posts/{id}/report → melden (jeder eingeloggte Spieler)
DELETE /forum/posts/{id} (eigener Beitrag, Admin, oder
Allianz-Führung bei Allianz-Themen)
DELETE /forum/threads/{id} (eigenes Thema, Admin, oder
Allianz-Führung bei Allianz-Themen)
Ein bewusst stark vereinfachtes In-Game-Brett — kein Editieren, keine Kategorien/Unterforen, nur Themen + Antworten. Nur eingeloggt erreichbar (kein öffentlicher HTTP-Endpunkt wie bei §2a).
Sichtbarkeit: jedes Thema ist "oeffentlich" (für alle sichtbar) oder
"allianz" (nur für Mitglieder GENAU dieser Allianz + Admin). Ein
allianzinternes Thema anlegen ohne eigene Allianz → 403 not_member. Zugriff
auf ein fremdes Allianz-Thema (View/Antworten/Melden) → 404 not_found (keine
Info-Preisgabe über die Existenz, analog zu fremden Inseln). Admins sehen und
moderieren alle Themen unabhängig von eigener Allianz-Mitgliedschaft.
Moderation: POST /forum/posts/{id}/report ist idempotent (mehrfaches
Melden durch denselben Spieler erzeugt keinen zweiten Eintrag) und für jeden
eingeloggten Spieler erlaubt — nur klein sichtbar im Frontend, kein
Sofort-Effekt. Die Felder gemeldet (Themenliste) und melde_anzahl
(Beitrag-Detail) kommen von der API nur mit, wenn der Aufrufer is_admin
ist — normale Spieler sehen nie, ob/wie oft etwas gemeldet wurde.
Löschen: jeder Spieler darf seinen EIGENEN Beitrag oder sein eigenes
Thema jederzeit löschen. Admin darf zusätzlich alles löschen (protokolliert
im Admin-Audit-Log). Bei einem Allianz-Thema (sichtbarkeit: "allianz")
darf zusätzlich die Führung DIESER Allianz (Rolle anfuehrer oder
diplomat) fremde Beiträge/Themen löschen — aber nur innerhalb der eigenen
Allianz, nicht im öffentlichen Forum (dort bleibt fremdes Löschen admin-only).
Löscht man den letzten Beitrag eines Themas, verschwindet das dann leere
Thema automatisch mit. Themenliste und Thema-Detail liefern je Eintrag
darf_loeschen: bool, damit der Client den Löschen-Link nur zeigt, wenn die
API ihn auch tatsächlich zulässt — die eigentliche Prüfung ist serverseitig.
Link-Verbot: Titel und Beitragstext (Thema wie Antwort) werden serverseitig
gegen eine Link-Heuristik geprüft (Protokoll-URLs, www., wortartige
domain.tld-Muster mit KLEINGESCHRIEBENER Endung ohne Leerzeichen) — bei
Treffer 422 link_verboten. Eine Heuristik, kein vollständiger Parser;
erkennt nicht jede Verschleierung, löst aber nicht bei normalen Satzenden
ohne Leerzeichen aus (Bugfix 2026-08-19: die Endung musste ursprünglich nur
2+ Buchstaben sein, GROSS ODER klein — jeder Satz ohne Leerzeichen nach
einem Punkt vor einem großgeschriebenen Folgewort, z. B. "Danke.Bis später",
wurde dadurch fälschlich blockiert; echte Domains/TLDs sind praktisch nie
großgeschrieben, die Endung ist jetzt auf Kleinbuchstaben beschränkt).
GET /rankings/players → nach Gesamtpunkten
GET /rankings/alliances → nach Punkten
GET /stats/overview → öffentlich, KEIN Login nötig — Server-Kennzahlen
(Spieler/Inseln/Allianzen/Flotten unterwegs),
Top-10-Spieler/Top-5-Allianzen, weltweiter
Einheiten-/Schiffsbestand (Häfen) UND was
gerade auf See ist (Schiffe + Einheiten in
Flotten). Auch als Seite unter /statistik
(HTML, von der Startseite verlinkt).
GET /quests → Tutorial + Meilensteine + heutige Dailies + Fortschritt
POST /quests/{id}/claim → Belohnung (Rohstoffe) abholen
Rangliste live vs. eingefroren: Rang/Punkte/
Differenz kommen aus dem täglichen Snapshot und ändern sich erst beim
nächsten Tageswechsel. Name/Inseln/Allianz werden dagegen bei JEDEM
Abruf frisch aus der Datenbank nachgeladen (live) — ein Allianzwechsel oder
gelöschter Account ist also sofort sichtbar, nur die Punktzahl/der Rang
selbst bleibt bis Mitternacht fix. Gelöschte Spieler/Allianzen werden aus
dem Snapshot entfernt, sobald sie beim nächsten Abruf nicht mehr existieren.
GET /rankings/players liefert außerdem online: bool je Spieler —
ebenfalls live (letzte Aktivität < ONLINE_SCHWELLE_MIN
Minuten), analog zum online-Feld im öffentlichen Profil (§ Nutzer); die
genaue letzte Aktivität selbst bleibt privat, nur der Bool wird ausgeliefert.
rang_aenderung: int | null (beide Endpunkte):
Platzveränderung seit dem VORHERIGEN Tages-Snapshot — positiv = aufgestiegen,
negativ = abgestiegen, 0 = unverändert, null = kein Vortages-Rang
vorhanden (Konto/Allianz ist neu). Wie differenz nur beim täglichen Snapshot
neu berechnet, nicht live.
Allianz-Flagge (beide Endpunkte, live wie
Name/Allianz): GET /rankings/alliances liefert je Allianz zusätzlich
flagge: {icon, farbe} | null; GET /rankings/players liefert sie
verschachtelt unter allianz.flagge (§9).
Tageswechsel = Mitternacht in der Server-Zeitzone Europe/Berlin (DST-
sicher via app.core.timeutil.server_datum()), NICHT UTC-Mitternacht — Fix
2026-07-05, vorher wechselte der Snapshot erst 1–2h nach der deutschen
Mitternacht, was wie ein Berechnungsfehler wirkte.
Öffentliche Auszeichnungen: quests_erfuellt
im öffentlichen Profil (§2) zeigt NUR Quests aus einer fest hinterlegten
Whitelist, die nichts über die eigene Insel oder deren Ausbaustufen verrät
(z. B. Allianz beigetreten, Kampf gewonnen, Insel erobert, geplündert,
spioniert, Kontoalter, sowie die drei Lieferungs-/Kolonisations-Quests unten)
— Gebäude-/Forschungs-/Punkte-Quests bleiben privat.
Meilensteine M13–M15 (neu): - M13 Truppentransport — 1.000 Steinewerfer in EINER Lieferung verschickt (Maximum je Einzellieferung zählt). - M14 Großhändler — 100.000 Rohstoffe INSGESAMT verschickt (Summe über alle Lieferungen — nicht auf einmal, bewusst andere Semantik als M13). - M15 Gescheiterte Landung — 100 gescheiterte Kolonisationsversuche erlebt.
Dailies-Pool auf 15 Aufgaben erweitert (balancing.DAILIES["pool"]); pro Tag
werden weiterhin 3 davon angeboten, deterministisch per HMAC(Server-Seed,
Datum) ausgewählt — für alle Spieler am selben Tag garantiert identisch.
Fortschritt je Quest: jeder Eintrag in
GET /quests (tutorial/meilensteine/dailies) trägt zusätzlich
fortschritt: {aktuell, ziel, prozent} — prozent ist bereits auf 100
gedeckelt und für die Anzeige gerundet (1 Nachkommastelle), erfuellt bleibt
weiterhin die maßgebliche Quelle für "abholbar" (nicht prozent >= 100, auch
wenn beides deckungsgleich ist). Bei kombinierten Bedingungen (gebaeude_stufen,
forschung_max — mehrere Anforderungen auf EINER Insel) ist ziel die
Anzahl der Teilbedingungen und aktuell die Summe der je Teilbedingung
anteilig erfüllten Fortschritte (nicht alles-oder-nichts) der besten Insel.
Interne Admin-/Testhilfe-Endpunkte (nur is_admin) — bewusst NICHT Teil
dieser öffentlichen API-Dokumentation und aus der automatischen OpenAPI/
Swagger-Doku (/docs) ausgeschlossen.
WS /ws → Benachrichtigungen (Auftrag fertig, Flotte
angekommen, Angriff im Anflug, neue Nachricht)
Auth beim WS-Handshake via ?token= (JWT) oder ?api_key=. Es gibt KEINEN
Live-Kampf-Playback-Kanal mehr (WS /ws/battles/{id}, entfernt 2026-07-19)
— der Kampfausgang UND der volle Bericht stehen sofort bei Ankunft der
Flotte fest,
keine Berichtssperre mehr (§8, §14a.3).
Viele Antworten sehen je nach Rolle/Status/Admin-Recht unterschiedlich aus —
Felder werden nicht nur mit null befüllt, sondern weggelassen. Diese
Referenz listet ALLE Varianten, damit ein Bot nicht raten muss, welche Felder
auftauchen können.
Message.payload (Postfach, §11) — je nach NachrichtentypKampfbericht — Angreifer, Sieg:
{
"battle_id": 42, "rolle": "angreifer", "sieg": true,
"insel_name": "Nebelinsel", "insel_koordinaten": "12:34:5",
"eingesetzt": {"angreifer": {"units": {}, "ships": {}},
"verteidiger": {"units": {}, "ships": {}}},
"verluste": {"angreifer": {}, "verteidiger": {}},
"razzia": {"goldmine": 1},
"stein_rueckgewinnung": 35,
"holz_rueckgewinnung": 14,
"loot": {"gold": 120, "stein": 40},
"erobert": false,
"aufklaerung": null,
"aufklaerung_status": "kein_spaehschiff",
"rng": {"eroberung": {"wurf": 0.71, "schwelle": 0.5}},
"text_voll": "…"
}
insel_name/insel_koordinaten stehen in JEDER
Kampfbericht-Variante (Angreifer Sieg/Niederlage, Verteidiger) sowie in
GET /battles/{id} — Inselnamen sind ohnehin überall öffentlich sichtbar
(GET /map/island), keine zusätzliche Info-Preisgabe.
text_voll ist identisch zu Message.body (§11) — sofort ab Erstellung
verfügbar, keine Verzögerung/Freischaltung mehr (bis 2026-07-19 gab es hier
noch einen neutralen Platzhalter-Body bis Kampfende, s. 14a.3).
aufklaerung ist entweder null oder ein Schnappschuss (siehe 14a.2).
aufklaerung_status: "kein_spaehschiff" (kein Spähschiff dabei) |
"erfolgreich" | "fehlgeschlagen" (Würfel nicht getroffen). rng-Keys
variieren je nachdem, welche Zufallsentscheidungen im Kampf gefallen sind
(angriffsaufklaerung, eroberung, …), jeweils {wurf, schwelle}.
aufklaerung.rohstoffe wird NACH der Plünderung erfasst — zeigt bei
erfolgreicher Zusatz-Aufklärung also den Bestand ABZÜGLICH loot, nicht den
Stand vor dem Angriff.
stein_rueckgewinnung/holz_rueckgewinnung sind die Stein-/Holz-Mengen, die
der VERTEIDIGER (nicht der Angreifer) aus den razzierten Gebäuderesten
zurückbekommt (8 % bzw. 5 % des jeweiligen Baukostenanteils der zerstörten
Stufen; Gold ist verloren, kein gold_rueckgewinnung-Feld) — stehen in
BEIDEN Rollen-Payloads (Angreifer-Sieg und Verteidiger) zur Info,
gutgeschrieben wird nur der Zielinsel. Beide 0, wenn keine Stufe zerstört
wurde oder das Gebäude vorher schon Stufe 0 war.
razzia kann seit 2026-07-24 ZWEI Einträge enthalten (statt nur einem):
war razzia_ziel die Steinmauer und wurde diese vollständig zerstört, geht
überschüssige Angriffskraft im selben Angriff auf das Haupthaus über, z. B.
{"steinmauer": 1, "haupthaus": 2} — stein_rueckgewinnung/
holz_rueckgewinnung sind dann bereits über beide Gebäude aufsummiert.
erobert: true: die gelandeten Truppen des
Angreifers fahren, soweit Kämpfer-Kapazität der überlebenden Kriegsschiffe
reicht, mit der Flotte zurück — nur der Überhang bleibt als neue Garnison auf
der eroberten Insel. Die zurückfahrende Flotte kann also weniger Einheiten
enthalten als am Ende des Landkampfs überlebt hatten.
Kampfbericht — Angreifer, Niederlage (eingesetzt/verluste beider
Seiten sind AUCH bei Niederlage sichtbar, nicht nur bei Sieg):
{
"battle_id": 42, "rolle": "angreifer", "sieg": false,
"insel_name": "Nebelinsel", "insel_koordinaten": "12:34:5",
"eingesetzt": {"angreifer": {"units": {}, "ships": {}},
"verteidiger": {"units": {}, "ships": {}}},
"verluste": {"angreifer": {}, "verteidiger": {}},
"verteidiger_name": "SpielerName",
"text_voll": "…"
}
Kein razzia/loot/aufklaerung (die gibt's nur bei Sieg) — die
Zusatz-Aufklärung (Gebäude/Rohstoffe/Schiffsbestand der Insel) bleibt
weiterhin exklusiv an Spähschiff + Sieg gebunden, NUR der reine
Kampfverlauf (wer mit wieviel eingesetzt/verloren hat) ist jetzt immer
sichtbar. verteidiger_name ist null, wenn das Ziel unbewohnt war.
Kampfbericht — Verteidiger (immer vollständig, unabhängig vom Ausgang):
{
"battle_id": 42, "rolle": "verteidiger", "sieg": true,
"insel_name": "Nebelinsel", "insel_koordinaten": "12:34:5",
"eingesetzt": {"angreifer": {}, "verteidiger": {}},
"verluste": {"angreifer": {}, "verteidiger": {}},
"razzia": {}, "stein_rueckgewinnung": 0, "holz_rueckgewinnung": 0, "loot": {},
"angreifer": "SpielerName", "text_voll": "…"
}
Spionage — Ergebnis versenkt:
{"typ": "spionage", "ergebnis": "versenkt", "ziel": "12:34:5",
"insel_name": "Nebelinsel",
"rng": {"chance": 0.3, "wurf": 0.81, "entdeckt": true, "versenkt": true}}
insel_name steht in allen drei
typ: "spionage"-Varianten (versenkt/erfolg/fehlschlag) — NICHT in den
spionage_abwehr-Varianten (die betreffen die eigene Insel des Empfängers,
der kennt sie bereits).
Spionage-Abwehr — Ziel wurde ausspioniert, Späher versenkt:
{"typ": "spionage_abwehr", "ergebnis": "versenkt", "angreifer": "SpielerName"}
Spionage — Erfolg:
{"typ": "spionage", "ergebnis": "erfolg", "ziel": "12:34:5",
"bericht": {"info_tiefe": 3, "rohstoffe": {"gold": 900, "stein": 400, "holz": 200},
"gebaeude": {"haupthaus": 5}, "truppen": {"steinewerfer": 20}},
"rng": {"chance": 0.6, "wurf": 0.22}}
bericht ist nach info_tiefe gestaffelt — Felder kommen ERST ab der
jeweiligen Tiefe dazu (niedrigere Tiefe = weniger Felder, nicht null):
info_tiefe 1: nur rohstoffe. ≥2: zusätzlich gebaeude (ALLE Gebäudetypen
inkl. noch nicht gebauter mit Stufe 0 — deaktivierte Gebäude wie
Labor/Marktplatz sind ausgenommen). ≥3: zusätzlich
truppen. ≥4: zusätzlich schiffe, forschung, punkte
(int), produktion_pro_h ({gold, stein, holz} je Std.), lagerkapazitaet
(int, je Rohstoff), pluenderschutz (int, je Rohstoff) und bauauftraege
(Liste aller laufenden/wartenden Bauaufträge der Zielinsel, je Eintrag
{gebaeude, ziel_stufe, status: "aktiv"|"wartend", finish_at} — finish_at
ISO-8601 oder null solange der Auftrag noch wartet).
Spionage — Fehlschlag (kein bericht-Feld):
{"typ": "spionage", "ergebnis": "fehlschlag", "ziel": "12:34:5", "rng": {...}}
Spionage-Abwehr — nur gesichtet (Angreifer bleibt anonym, kein angreifer-Feld):
{"typ": "spionage_abwehr", "ergebnis": "gesichtet"}
Allianz-Einladung: {"alliance_id": 3, "invite": true}
Allianz-Rundbrief: {"alliance_id": 3, "rundbrief": true}
Neue Insel nach Verlust der letzten Insel — geht an einen Spieler, der
durch eine Eroberung GAR KEINE Insel mehr besitzt; er bekommt automatisch (noch im selben resolve_attack()-Lauf)
eine neue Startinsel inkl. Anfängerschutz, analog zur Registrierung
(services/islands.py create_start_island()):
{"typ": "insel_verloren", "neue_insel": "53:48:20", "neue_insel_id": 163}
folder="combat", wie Kampf-/Kolonisationsberichte. Besitzt der Spieler nach
der Eroberung noch andere Inseln, entfällt diese Nachricht komplett.
aufklaerung und Spionage-bericht verwendet){"rohstoffe": {"gold": 900, "stein": 400, "holz": 200},
"gebaeude": {"haupthaus": 5, "goldmine": 3},
"truppen": {"steinewerfer": 20}, "schiffe": {"spaehschiff": 1},
"forschung": {"segel": 2}}
Welche Top-Level-Keys vorhanden sind, hängt vom Kontext ab (Aufklärung durch Spähschiff im Angriff vs. gestufter Spionagebericht, siehe oben).
Es gab hier früher eine 3-minütige Sperre (davor Live-Playback, davor 15 Min)
zwischen Flotten-Ankunft und vollem Kampfbericht — GET /battles/{id} und
GET /messages/GET /messages/{id} lieferten bis dahin nur einen reduzierten
Platzhalter (fertig: false, hinweis/Fortschrittsbalken-Felder). Komplett
entfernt 2026-07-19 — Kampfausgang UND voller Bericht stehen jetzt im selben
Moment fest, battle.end_at == battle.start_at, es gibt kein fertig-Feld
und keinen Platzhalter mehr.
GET /battles/{id} — volle Ansicht, sofort ab Ankunft (§8)Alle Varianten haben zusätzlich insel_name/insel_koordinaten (§14a.1).
Verteidiger (immer voll): id, start_at, end_at, rolle:"verteidiger",
ergebnis, timeline, eingesetzt, verluste, razzia, stein_rueckgewinnung,
holz_rueckgewinnung, loot, conquered, angreifer_name.
Angreifer + Sieg: id, start_at, end_at, rolle:"angreifer", ergebnis,
timeline, eingesetzt, verluste, razzia, stein_rueckgewinnung,
holz_rueckgewinnung, loot, conquered, aufklaerung, aufklaerung_status, rng,
verteidiger_name.
Angreifer + Niederlage (nicht rudimentär — nur die Zusatz-Aufklärung
fehlt): id, start_at, end_at,
rolle:"angreifer", ergebnis, timeline, eingesetzt, verluste,
verteidiger_name, hinweis. Kein razzia/loot/conquered/aufklaerung
(die gibt's nur bei Sieg). verteidiger_name ist null bei unbewohntem Ziel.
timeline-Einträge (phase: "see"|"land") tragen seit 2026-07-09 zusätzlich
angriffsstaerke/verteidigungsstaerke (die tatsächlich verglichenen Werte,
die über Sieg/Niederlage entscheiden — VOR den Verlusten). Der "land"-
Eintrag trägt außerdem basis_inselverteidigung (0 oder KAMPF
["basis_inselverteidigung"]=50 — 50 gilt bereits ab 1 verteidigender
Einheit, 0 nur bei einer komplett leeren Zielinsel). Ältere, vor diesem
Datum gespeicherte Battles haben diese Felder nicht (fehlen einfach, kein
null).
Der "see"-Eintrag trägt seit 2026-07-24 zusätzlich rest_angriffsstaerke
— die Angriffsstärke der ÜBERLEBENDEN
Kriegsschiffe des Angreifers, 0, wenn der Angreifer das Seegefecht verliert
(auch bei Teil-Überleben). Dieser Wert geht, sofern der Angreifer am Ende
auch gewinnt, zusätzlich zur Landtruppen-Rest-Angriffsstärke in die Belagerung
ein — s. u.
WS /ws — genau 4 Event-Typen (kein weiterer){"typ": "verbunden"}
{"typ": "nachricht", "id": 7, "folder": "inbox", "subject": "Hallo"}
{"typ": "flotte_ankunft", "fleet_id": 12, "state": "outbound|returning|done"}
{"typ": "angriff_im_anflug", "ziel": "12:34:5", "mission": "attack",
"arrive_at": "2026-07-06T14:00:00Z"}
WS /ws/battles/{id} Live-Playback-FramesEs gab hier früher einen Live-Playback-Kanal, der die Kampf-Timeline frame-weise über WebSocket "abspielte", plus eine kurze Berichtssperre danach. Beides 2026-07-19 komplett entfernt — Kampfausgang UND voller Bericht stehen jetzt im selben Moment fest, keine Live-Daten, kein Platzhalter, keine Wartezeit mehr (§14a.3).
Der timeline-Eintrag der belagerung-Phase in battle.timeline (§14a.4)
enthält weiterhin stein_rueckgewinnung und holz_rueckgewinnung (beide
int) — identisch zu den Top-Level-Feldern im Kampfbericht (14a.1). Seit
2026-07-24 zusätzlich angriffsstaerke (die tatsächlich für die Razzia
verbrauchte Gesamt-Rest-Angriffsstärke) sowie die Aufschlüsselung
angriffsstaerke_land/angriffsstaerke_schiffe — wie viel davon von
überlebenden Landtruppen bzw. überlebenden Kriegsschiffen stammt.
GET /alliances/{id} (§9): kasse, rolle und kasse_unterwegs
erscheinen NUR, wenn der Aufrufer selbst Mitglied ist. Nicht-Mitglieder
sehen nur id, name, tag, beschreibung, gegruendet, punkte, mitglieder[],
diplomatie[], flagge.GET /forum/threads (§11a): gemeldet: bool je Thema NUR für Admins.GET /forum/threads/{id} (§11a): melde_anzahl: int je Beitrag NUR
für Admins.Angriff starten: (Typ-Schlüssel sind IMMER Deutsch, siehe balancing.SCHIFFE/
EINHEITEN — z. B. kleines_kriegsschiff, nicht small_warship)
POST /api/v1/fleets
X-API-Key: sk_live_...
{
"origin_island_id": 12,
"mission_type": "attack",
"target": { "x": 20, "y": 8, "z": 20 },
"ships": { "kleines_kriegsschiff": 10, "spaehschiff": 1, "kleines_handelsschiff": 5 },
"units": { "bogenschuetze": 80, "steinewerfer": 40 },
"razzia_ziel": "goldmine",
"razzia_prioritaet": "gold"
}
→ 201 {
"id": 88, "mission": "attack", "origin_island_id": 12,
"target": { "x": 20, "y": 8, "z": 20 },
"ships": { "kleines_kriegsschiff": 10, "spaehschiff": 1, "kleines_handelsschiff": 5 },
"units": { "bogenschuetze": 80, "steinewerfer": 40 }, "resources": {}, "loot": {},
"state": "outbound", "speed_knots": 10, "distanz_felder": 42,
"depart_at": "2026-07-02T18:20:00Z", "arrive_at": "2026-07-02T18:40:00Z",
"return_at": null, "recallable_until": "2026-07-02T18:30:00Z"
}
mission im Response ist die vom Server aus der Fracht ABGELEITETE konkrete
Mission (hier attack, da weder nur Spähschiffe noch ein Kolonisationsschiff
an Bord sind) — siehe §7.