Design wechseln

Mini-Spiel-Zustandsmaschine: Vom Startbildschirm über Kampf bis zur Abrechnung

Easton editorial illustration: central gameplay state rotor linking home, battle, pause, and result states with a nested combat ring

"80 % der Bugs bei Kartenspielen stammen aus schlechtem Zustandsdesign; Zustandsmaschinen lösen das Problem verstreuter Zustandsprüfungen effektiv."

- Sohu-Artikel zur Kartenspiel-Architektur

"Verschachtelte Zustandsmaschinen sind zum Mainstream-Designmuster komplexer Spiele geworden; Battle State kann BeginBattle, HeroTurn, EnemyTurn, EndBattle als vier Unterzustände enthalten."

Ihr Mini-Spiel ist live – und Spieler melden: Die Abrechnungsansicht hängt. Das Spiel ist vorbei, die Oberfläche zeigt noch den Kampf. Sie öffnen den Code und sehen überall if (isPlaying && !isPaused && !isGameOver && hasPlayerLeft). Wer hat das geschrieben? Ach ja – Sie selbst.

Bei einem Kartenspiel bin ich in dieselbe Falle getappt. Zustandsprüfungen als Boolean-Variablen, verstreut über dutzende Dateien – eine Funktion ändern hieß ein Dutzend Files anfassen. Im Mehrspieler war es am schlimmsten: Der Server war in der Abrechnung, der Client spielte noch Kampfanimationen ab – Zustandsdrift auf beiden Seiten. Nach einem Refactoring mit Zustandsmaschine wurde der Code deutlich übersichtlicher.

Dieser Artikel zeigt, wie Sie eine Mini-Spiel-Zustandsmaschine entwerfen. Vom Startbildschirm über Kampf bis zur Abrechnung – aufgeteilt in drei Ebenen: Spielfluss, Kampffluss und Kampf-Details. Dazu Praxiserfahrung: Zustandssperren, serverseitige Validierung, Timeout-Mechanismen – das sind die Dinge, die im Alltag wirklich zählen.

Warum brauchen Mini-Spiele eine Zustandsmaschine?

Kurz gesagt: Ein Spiel ist ein ständiges Hin und Her zwischen Zuständen.

Startbildschirm, Kampf, Abrechnung, Pause, Zuschauen – hinter jeder Ansicht steckt ein Zustand. Der Spieler tippt auf „Spiel starten“, der Startbildschirm verschwindet, die Kampfoberfläche erscheint. Kampfende, Abrechnung poppt auf. Klingt simpel – im Code wird es schnell unübersichtlich.

Die drei Sünden der if-else-Kaskade

Typischer Code, den Sie sicher schon gesehen haben:

// Zustandsprüfungen verstreut überall
if (isPlaying && !isPaused && !isGameOver) {
  // Spieler kann handeln
}

if (isGameOver && !hasShownResult) {
  // Abrechnungsansicht anzeigen
}

if (isPlaying && currentPlayer === 'player1') {
  // Spieler 1 ist dran
}

Drei Probleme:

Schwer lesbar: Um den aktuellen Zustand zu erkennen, müssen Sie die ganze Datei nach Boolean-Variablen durchsuchen. isPlaying, isPaused, isGameOver – verstreut wie Puzzleteile, die Sie selbst zusammensetzen müssen.

Schwer änderbar: Einen „Pause“-Zustand hinzufügen? Dutzende if-Prüfungen anpassen. Eine vergessen – Bug. Ich habe einmal einen „Zuschauen“-Zustand ergänzt, zwei Tage Code geändert – und nach dem Release lief eine Animation weiter, obwohl pausiert war.

Schwer debuggbar: Wechselt die Reihenfolge durcheinander, finden Sie die Ursache nicht. isGameOver ist true, hasShownResult noch false – welche Prüfung fehlt?

Ein Sohu-Artikel nennt die Zahl: 80 % der Bugs bei Kartenspielen kommen von schlechtem Zustandsdesign. Klingt drastisch – wer es erlebt hat, weiß: stimmt.

Wie löst die Zustandsmaschine das?

Sie bündelt die verstreuten Prüfungen an einem Ort.

Jeder Zustand hat eigenes Verhalten: Was passiert beim Eintritt, während des Laufs, beim Verlassen. Wechselregeln sind klar: Vom Startbildschirm nur zum Kampf – nicht direkt zur Abrechnung.

Zum Vergleich:

// Zustandsmaschinen-Ansatz
class HomeState {
  enter() { showHomeUI(); }
  exit() { hideHomeUI(); }
  handleEvent(event) {
    if (event === 'START_BATTLE') {
      manager.changeState(new BattleState());
    }
  }
}

class BattleState {
  enter() { showBattleUI(); startBattle(); }
  exit() { hideBattleUI(); cleanupBattle(); }
}

Zustände sind gekapselt. HomeState ändern – BattleState bleibt unberührt. Neuer Zustand: neue State-Klasse plus eine Wechselregel.

Better Programming schreibt: Verschachtelte Zustandsmaschinen sind zum Mainstream bei komplexen Spielen geworden. Mini-Spiele ausgenommen? Nein – klare Zustände, klarer Code.

Dreischichtige Zustandsmaschinen-Architektur

Mini-Spiel-Zustandsmaschinen sollten geschichtet sein. Ohne Schichten landet Kampflogik in der Spielfluss-Ebene – der Code wuchert.

Ich teile in drei Ebenen: Spielfluss, Kampffluss, Kampf-Details.

Erste Ebene: Spielfluss

Das Rückgrat vom Start bis zur Abrechnung:

BootState: Beim Start – Ressourcen laden, Engine initialisieren. Cocos Creators Boot-Szene macht genau das: erst Boot, dann Home.

HomeState: Startmenü. Level wählen, Charakter wählen, Bestenliste. Relativ einfach: UI anzeigen, Button-Klicks verarbeiten.

BattleState: Kampfszene. Kein Endpunkt, sondern Container – innen läuft eine Unter-Zustandsmaschine.

SettlementState: Abrechnung. Sieg/Niederlage, Statistik, Kampf-Ressourcen aufräumen.

Ablauf: Boot → Home → Battle → Settlement → zurück zu Home (oder Beenden).

Skizze:

Spielstart → BootState
          ↓ (Laden fertig)
         HomeState
          ↓ (Start tippen)
         BattleState ← Kampf-Unterzustandsmaschine hier
          ↓ (Kampfende)
      SettlementState
          ↓ (Zurück tippen)
         HomeState

Zweite Ebene: Kampffluss

In BattleState läuft eine eigene Unter-Zustandsmaschine. Better Programming: Battle State kann BeginBattle, HeroTurn, EnemyTurn, EndBattle als vier Unterzustände enthalten.

BeginBattleState: Kampf initialisieren. Level-Parameter, Charakterdaten, Spieler synchronisieren. Im Mehrspieler hier sperren – kein Beitritt/Austritt mitten drin.

PlayerTurnState: Spielerzug. Karten legen, angreifen, verteidigen – alles in diesem Zustand. Zugende → nächster Zug oder Abrechnung.

EnemyTurnState (optional): KI-Aktion. Einzelspieler kann weglassen; Mehrspieler = Gegnerzug.

EndBattleState: Abrechnungslogik. Sieg/Niederlage, Belohnungen, Abrechnungsanimation.

Ablauf:

BattleState betreten → BeginBattleState (Lock, Sync)
                    ↓ (Bereit)
                PlayerTurnState
                    ↓ (Zugende)
                EnemyTurnState (optional)
                    ↓ (Kampfende)
                EndBattleState
                    ↓ (Abrechnung fertig)
         BattleState verlassen → SettlementState

Dritte Ebene: Kampf-Details

Manche Zustände brauchen weitere Unterteilung. In PlayerTurnState zum Beispiel:

Animations-Zustandsmaschine: Angriff → Angriffsanimation, Treffer → Trefferanimation. Unity und Cocos Creator haben eingebaute Animations-Zustandsmaschinen – hier nur kurz erwähnt.

Runden-Zustandsmaschine: Karte legen → angreifen → verteidigen → Zug beenden. Jede Aktion ein Unterzustand.

Beast Card Clash kapselt Setup, Scoring und Ergebnisbildschirm in eigene State-Klassen. Vorteil: Logik einer Phase ändern, ohne andere Phasen anzufassen.

Dreischichtig heißt klare Verantwortung. Startbildschirm ändern – Kampflogik bleibt intakt. Rundendetails ändern – Abrechnungsfluss nicht. Teamarbeit wird einfacher: eine Person Kampffluss, eine andere Details.

Kern-Schnittstellen der Zustandsmaschine

Architektur erklärt – jetzt Code.

Ein Zhihu-Artikel zu Unity nennt die einheitliche Schnittstelle: OnEnter, OnUpdate, OnExit, OnHandleEvent. Davon abgeleitet, hier TypeScript:

IGameState-Schnittstelle

Jeder Zustand implementiert:

interface IGameState {
  name: string;                    // Zustandsname – hilfreich beim Debuggen
  
  enter(params?: any): void;       // Initialisierung beim Eintritt
  update(dt: number): void;        // Frame-Update (optional)
  exit(): void;                    // Aufräumen beim Verlassen
  handleEvent(event: GameEvent): void;  // Events verarbeiten, Zustandswechsel auslösen
}

Die fünf Methoden:

name: Debugging. Logs zeigen „HomeState“ oder „BattleState“ – klarer als Booleans.

enter: Beim Eintritt. HomeState: Start-UI anzeigen, Spielerdaten laden. BattleState: Kampfparameter, Unter-Zustandsmaschine starten.

update: Pro Frame. Im Kampf oft – Countdown, Eingabe prüfen. Startbildschirm meist unnötig.

exit: Beim Verlassen. Ressourcen freigeben, UI verbergen, Listener entfernen.

handleEvent: Event-getriebener Wechsel. „Spiel starten“ → START_BATTLE → HomeState wechselt zu BattleState.

GameStateManager

Die Zustandsmaschine braucht einen Manager für Wechsel. Stack Exchange beschreibt CGameEngine mit Init, Cleanup, ChangeState, PushState, PopState.

Vereinfacht:

class GameStateManager {
  private currentState: IGameState | null = null;
  private stateStack: IGameState[] = [];  // Überlagernde Zustände (Pause über Kampf)
  
  // Initialisierung
  init(firstState: IGameState) {
    this.currentState = firstState;
    this.currentState.enter();
  }
  
  // Zustand wechseln
  changeState(newState: IGameState, params?: any) {
    if (this.currentState) {
      this.currentState.exit();
    }
    this.currentState = newState;
    this.currentState.enter(params);
  }
  
  // Überlagernden Zustand pushen (Pause, Dialog)
  pushState(state: IGameState) {
    if (this.currentState) {
      // Aktueller Zustand bleibt, wird nur pausiert
      this.stateStack.push(this.currentState);
    }
    this.currentState = state;
    state.enter();
  }
  
  // Überlagernden Zustand poppen
  popState() {
    if (this.currentState) {
      this.currentState.exit();
    }
    this.currentState = this.stateStack.pop();
    // Kein erneutes enter – vorher nur pausiert
  }
  
  // Frame-Update
  update(dt: number) {
    if (this.currentState) {
      this.currentState.update(dt);
    }
  }
  
  // Event verarbeiten
  handleEvent(event: GameEvent) {
    if (this.currentState) {
      this.currentState.handleEvent(event);
    }
  }
}

changeState: Wechsel – alter Zustand exit, neuer enter. Start → Kampf.

pushState / popState: Überlagerung. Pause während Kampf: PauseState auf den Stack. Pause beenden: pop, BattleState läuft weiter.

Wichtig für Pause, Dialoge, Bestätigungsboxen – überlagern ohne den darunterliegenden Zustand zu zerstören.

Praxis-Fallstricke und Lösungen

Theorie reicht – jetzt typische Probleme. Ein Sohu-Artikel fasst Kartenspiel-Erfahrungen zusammen; hier im Detail:

Zustandsdrift: Server und Client nicht synchron

Im Mehrspieler der häufigste Fall.

Szenario: Server in Abrechnung, ein Client spielt noch Kampfanimation – Netzwerkverzögerung, Broadcast verpasst. Anzeige „Im Kampf“, obwohl schon vorbei.

Ursache: Wechsel-Nachricht verloren oder zu spät.

Lösung:

  1. Synchronisation: Server broadcastet bei jedem Wechsel; Client synchronisiert sofort.

  2. Heartbeat: Client sendet regelmäßig aktuellen Zustand; bei Abweichung erzwingt der Server Sync.

// Client-Heartbeat
class BattleState {
  enter() {
    this.startHeartbeat();
  }
  
  startHeartbeat() {
    setInterval(() => {
      socket.send({
        type: 'HEARTBEAT',
        state: this.manager.currentState.name,
        roomId: this.roomId
      });
    }, 3000);  // alle 3 Sekunden
  }
}

Parallelitätskonflikt: Gleichzeitige Aktionen

Am Tisch legen zwei Spieler gleichzeitig Karten – Wechselreihenfolge durcheinander.

Ursache: Kein Lock, parallele Operationen ohne Warteschlange.

Lösung: Lock plus „aktueller Bediener“-Mechanismus.

class DealingState implements IGameState {
  private lock: boolean = true;
  private currentOperator: string = '';
  
  enter() {
    this.lock = true;  // Kartenausgabe gesperrt
    // Beitritt/Austritt verboten
    // Initiale Daten für alle synchronisieren
    
    setTimeout(() => this.unlock(), 3000);  // nach 3 s entsperren
  }
  
  unlock() {
    this.lock = false;
    this.currentOperator = this.getFirstPlayerId();
    this.manager.changeState(new PlayerTurnState());
  }
  
  handleEvent(event: GameEvent) {
    if (this.lock) {
      // Gesperrt – Aktion ablehnen
      return;
    }
    
    // Nur aktueller Bediener darf Events auslösen
    if (event.playerId === this.currentOperator) {
      // Aktion verarbeiten
    }
  }
}

Locks sichern: In kritischen Phasen (Kartenausgabe, Abrechnung) läuft nur ein Ablauf – keine Unterbrechung durch Spielereingaben.

Abrechnungssicherheit: Gefälschte Punkte vom Client

Client sendet Punkte und Sieg/Niederlage – ein Crack ändert die Werte. Wie erkennt der Server Betrug?

Ursache: Server vertraut Client-Daten.

Lösung: Server rechnet unabhängig.

// Server-Abrechnungslogik
class EndBattleState {
  enter() {
    // Keine Client-Punkte akzeptieren
    // Server berechnet aus Kampfprotokoll
    const result = this.calculateResultFromLog(battleLog);
    
    // Ergebnis an alle Clients
    this.broadcastResult(result);
  }
  
  calculateResultFromLog(log: BattleLog) {
    // Aus Kampfprotokoll (Kartenreihenfolge, Angriffsdaten) berechnen
    // Client-Logs können gefälscht sein – kritische Daten serverseitig prüfen
  }
}

Kernprinzip: Server vertraut dem Client nicht. Client zeigt an – Server entscheidet.

Timeout-Deadlock: Zustand ohne Ausstieg

Ein Zustand hängt – Wechsel zum nächsten kommt nie.

Ursache: Kein Timeout, erwartetes Event tritt nicht ein.

Lösung: Timeout pro Zustand.

class PlayerTurnState implements IGameState {
  private timeoutTimer: number;
  
  enter() {
    this.timeoutTimer = setTimeout(() => {
      // Timeout – automatisch überspringen
      this.manager.handleEvent({
        type: 'TIMEOUT_SKIP',
        playerId: this.currentPlayer
      });
    }, 30000);  // 30 Sekunden
  }
  
  exit() {
    clearTimeout(this.timeoutTimer);  // Timeout beim Verlassen aufräumen
  }
}

Wichtig bei Disconnect oder Netzwerk-Hänger – der Zustand darf nicht ewig warten.

Vier Fallstricke – Praxis schlägt Theorie. Zustandsdrift kostete mich zwei Tage Debug-Zeit. Heartbeat danach – Problem weg.

Cocos Creator in der Praxis

Theorie, Schnittstellen, Fallstricke – jetzt Cocos Creator.

Im vorherigen Artikel „Cocos Creator: Mini-Spiel-Projektstruktur“ ging es um Boot, Szenen, Abrechnungsseite. Hier: Wie die Zustandsmaschine mit Layern zusammenspielt.

Zustandsmaschine bei Ein-Szene-Architektur

Cocos Creator empfiehlt für Mini-Spiele: eine Szene, mehrere Layer ein-/ausblenden. Passt zur Zustandsmaschine.

BootLayerHomeLayerBattleLayerSettlementLayer

Jeder Layer ein Zustand:

import { director, Node } from 'cc';

// HomeState
class HomeState implements IGameState {
  name = 'Home';
  private homeLayer: Node | null = null;
  
  enter() {
    // HomeLayer anzeigen
    const scene = director.getScene();
    this.homeLayer = scene?.getChildByName('HomeLayer') ?? null;
    if (this.homeLayer) {
      this.homeLayer.active = true;
      this.initHomeUI();
    }
  }
  
  exit() {
    // HomeLayer verbergen
    if (this.homeLayer) {
      this.homeLayer.active = false;
      this.cleanupHome();
    }
  }
  
  handleEvent(event: GameEvent) {
    if (event.type === 'START_BATTLE') {
      // zu BattleState wechseln
      this.manager.changeState(new BattleState(), event.params);
    }
  }
  
  initHomeUI() {
    // Spielerdaten laden, Button-Events setzen
  }
  
  cleanupHome() {
    // Start-Ressourcen freigeben
  }
}

// BattleState
class BattleState implements IGameState {
  name = 'Battle';
  private battleLayer: Node | null = null;
  
  enter(params?: any) {
    const scene = director.getScene();
    this.battleLayer = scene?.getChildByName('BattleLayer') ?? null;
    if (this.battleLayer) {
      this.battleLayer.active = true;
      this.startBattle(params);
    }
  }
  
  exit() {
    if (this.battleLayer) {
      this.battleLayer.active = false;
      this.cleanupBattle();
    }
  }
  
  startBattle(params: any) {
    // Kampf-Unterzustandsmaschine starten
    this.battleStateManager.init(new BeginBattleState(params));
  }
  
  cleanupBattle() {
    // Kampf-Ressourcen aufräumen
  }
}

Layer ein-/ausblenden = visueller Zustandswechsel. enter zeigt, exit verbirgt.

Datenübergabe beim Zustandswechsel

Parameter mitgeben – z. B. gewähltes Level an BattleState:

// HomeState löst Wechsel aus
handleEvent(event: GameEvent) {
  if (event.type === 'START_BATTLE') {
    // Level-Parameter übergeben
    this.manager.changeState(new BattleState(), {
      levelId: event.levelId,
      difficulty: event.difficulty
    });
  }
}

// BattleState empfängt Parameter
enter(params?: any) {
  const levelId = params?.levelId ?? 'default';
  const difficulty = params?.difficulty ?? 'normal';
  this.loadLevel(levelId, difficulty);
}

Fortschritt in localStorage – Kampf unterbrochen, beim nächsten Mal weiter:

// Fortschritt speichern
exit() {
  localStorage.setItem('battle_progress', JSON.stringify({
    levelId: this.levelId,
    round: this.currentRound,
    score: this.score
  }));
}

// Fortschritt wiederherstellen
enter() {
  const saved = localStorage.getItem('battle_progress');
  if (saved) {
    const progress = JSON.parse(saved);
    this.resumeFromProgress(progress);
  }
}

WeChat-Mini-Spiel: Besonderheiten

Ein CSDN-Artikel empfiehlt: Zustandsmaschine in einer globalen Datei – einfacher zu pflegen.

Paketgröße: WeChat-Hauptpaket 4 MB. Zustandsmaschinen-Code schlank halten.

Subpackages: Kern (Boot, Home, Battle) im Hauptpaket; Erweiterungen (Speziallevel, Events) in Subpackages.

Globale State-Datei: In game.js initialisieren, andere Module greifen global zu.

// game.js
import { GameStateManager } from './states/GameStateManager';
import { BootState } from './states/BootState';

// Globaler State-Manager
window.gameStateManager = new GameStateManager();
window.gameStateManager.init(new BootState());

WeChat-API über globale Variable:

// WeChat-Login-Callback
wx.login({
  success: (res) => {
    const state = window.gameStateManager.currentState;
    if (state.name === 'HomeState') {
      state.handleEvent({ type: 'LOGIN_SUCCESS', code: res.code });
    }
  }
});

Vorteil: WeChat-API braucht keinen Import der Zustandsmaschine – Zugriff über window.


Diesen Artikel zusammen mit „Cocos Creator: Mini-Spiel-Projektstruktur“ lesen – dann wird das Gesamtbild klar. Projektstruktur ist der Container, die Zustandsmaschine das Verhalten.

Fazit

Eine Zustandsmaschine zerlegt den Spielfluss in klare Knoten.

Dreischichtig – Spielfluss, Kampffluss, Kampf-Details – skaliert vom Mini-Spiel bis zum großen Projekt. Eine Ebene ändern, andere bleiben stabil; Teamarbeit wird einfacher.

Praxis schlägt Theorie. Zustandsdrift, Parallelitätskonflikte, Abrechnungssicherheit, Timeout-Deadlocks – alle vier habe ich erlebt, Debug war mühsam. Locks, Heartbeat, serverseitige Validierung, Timeouts – danach war Ruhe. Richtiger Code, weniger Bugs.

Zum Ausprobieren: vollständiges Code-Beispiel (GitHub-Link). Zuerst „Cocos Creator: Mini-Spiel-Projektstruktur“ – Layer-Aufteilung verstehen, dann diesen Artikel – wie die Zustandsmaschine läuft. Beide zusammen, Architektur klar.

Als Nächstes: KI-gestütztes Testen der Zustandsmaschine – Testfälle für Wechselpfade generieren lassen. Automatisierte Tests senken die Fallstricke-Wahrscheinlichkeit.

Dreischichtige Mini-Spiel-Zustandsmaschine entwerfen

Vollständiger Implementierungsablauf für Zustandsverwaltung vom Startbildschirm über Kampf bis zur Abrechnung

⏱️ Estimated time: 60 min

  1. 1

    Step 1: Zustände der Spielfluss-Ebene definieren

    Haupt-Zustände auflisten: BootState, HomeState, BattleState, SettlementState. Jeder Zustand entspricht einem Layer oder einer Szene; Wechselregeln festlegen.
  2. 2

    Step 2: Unterzustände der Kampffluss-Ebene aufteilen

    In BattleState eine Unter-Zustandsmaschine: BeginBattleState (Initialisierung mit Lock), PlayerTurnState (Spielerzug), EnemyTurnState (optional KI-Zug), EndBattleState (Abrechnungsbewertung).
  3. 3

    Step 3: Zustands-Schnittstellen und Manager implementieren

    IGameState-Schnittstelle (name, enter, exit, update, handleEvent) erstellen, GameStateManager für changeState sowie pushState/popState implementieren.
  4. 4

    Step 4: Schutzmechanismen gegen typische Fallstricke

    An kritischen Zuständen (Kartenausgabe, Abrechnung) Locks gegen Parallelitätskonflikte, Timeouts gegen Deadlocks, Heartbeat gegen Zustandsdrift, serverseitige Berechnung gegen Abrechnungsbetrug.
  5. 5

    Step 5: An Cocos Creator Layer anbinden

    In enter den passenden Layer anzeigen und Daten initialisieren; in exit Layer verbergen und Ressourcen freigeben. Zustandsmaschine steuert UI-Wechsel – Logik und Darstellung entkoppelt.

FAQ

Brauchen Mini-Spiele unbedingt eine Zustandsmaschine?
Nicht zwingend – aber bei vielen Zuständen (Start, Kampf, Pause, Abrechnung usw.) wird if-else-Logik schnell chaotisch. Eine Zustandsmaschine bündelt die Logik und macht Änderungen sicherer.
Ist eine dreischichtige Zustandsmaschine Over-Engineering?
Einfache Mini-Spiele brauchen vielleicht nicht drei Ebenen – das Verständnis dieser Architektur hilft aber bei komplexen Szenarien. Zwei Ebenen für einfache Spiele, drei für komplexere.
Was ist der Unterschied zwischen Zustandsmaschine und Szenenwechsel?
Szenenwechsel ist Cocos' Ressourcen-Management-Konzept; die Zustandsmaschine ist ein Logik-Konzept. Eine Szene kann einem Zustand entsprechen – ein Zustand kann auch nur UI-Sichtbarkeit steuern.
Wie löst man Zustandsdrift im Mehrspieler?
Server broadcastet Zustandswechsel plus Client-Heartbeat. Alle 3 Sekunden ein Heartbeat; bei Inkonsistenz erzwingt der Server eine Synchronisation.
Wie schützt man Abrechnungsdaten vor Betrug?
Server rechnet unabhängig – Client-Ergebnisse werden nicht vertraut. Der Client zeigt nur an; die gesamte Bewertungslogik liegt serverseitig.

10 Min. Lesezeit · Veröffentlicht am: 19. Mai 2026 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog