diff --git a/cmd/server/main.go b/cmd/server/main.go index 2f05dc2..f276851 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -21,11 +21,22 @@ var ( playerSessions = make(map[string]*server.Room) mu sync.RWMutex globalWorld *game.World + serverID string // Eindeutige Server-ID für diesen Pod ) func main() { log.Println("🚀 Escape From Teacher SERVER startet...") + // 0. Server-ID generieren (Hostname oder zufällig) + serverID = os.Getenv("HOSTNAME") + if serverID == "" { + serverID = os.Getenv("POD_NAME") + } + if serverID == "" { + serverID = "server-" + time.Now().Format("150405") + } + log.Printf("🆔 Server-ID: %s", serverID) + // 1. WELT & ASSETS LADEN globalWorld = game.NewWorld() loadServerAssets(globalWorld) @@ -100,10 +111,8 @@ func main() { } log.Printf("✅ Verbunden mit NATS: %s", natsURL) - // 3. HANDLER: GAME JOIN (mit Queue Group für Load Balancing) - sub, err := ec.QueueSubscribe("game.join", "game-servers", func(req *game.JoinRequest) { - log.Printf("📥 JOIN empfangen: Name=%s, RoomID=%s", req.Name, req.RoomID) - + // 3. HANDLER: GAME JOIN (broadcast - alle Pods hören, aber nur zuständiger erstellt) + sub, err := ec.Subscribe("game.join", func(req *game.JoinRequest) { playerID := req.Name if playerID == "" { playerID = "Unknown" @@ -114,13 +123,21 @@ func main() { roomID = "lobby" } + log.Printf("📥 JOIN empfangen: Name=%s, RoomID=%s", req.Name, roomID) + + // Prüfe ob dieser Pod für den Raum zuständig ist + if !isResponsibleForRoom(roomID) { + log.Printf("⏭️ Überspringe JOIN - nicht zuständig für Raum '%s'", roomID) + return + } + mu.Lock() defer mu.Unlock() // Raum finden oder erstellen room, exists := rooms[roomID] if !exists { - log.Printf("🆕 Erstelle neuen Raum: '%s'", roomID) + log.Printf("🆕 Erstelle neuen Raum: '%s' (zuständiger Pod)", roomID) room = server.NewRoom(roomID, nc, globalWorld) rooms[roomID] = room @@ -170,8 +187,8 @@ func main() { } }) - // 6. HANDLER: SCORE SUBMISSION - _, _ = ec.Subscribe("score.submit", func(submission *game.ScoreSubmission) { + // 6. HANDLER: SCORE SUBMISSION (Queue Group - nur ein Pod speichert) + _, _ = ec.QueueSubscribe("score.submit", "score-handlers", func(submission *game.ScoreSubmission) { // Verwende Team-Name wenn vorhanden (Coop-Mode), sonst Player-Name (Solo-Mode) displayName := submission.PlayerName if submission.TeamName != "" { @@ -234,6 +251,44 @@ func getEnv(key, defaultValue string) string { return defaultValue } +// isResponsibleForRoom prüft ob dieser Pod für den Raum zuständig ist +func isResponsibleForRoom(roomID string) bool { + // Wenn nur 1 Replica läuft, sind wir immer zuständig + totalReplicas := getEnv("TOTAL_REPLICAS", "1") + if totalReplicas == "1" { + return true + } + + // Hash-basierte Zuweisung: RoomID → Pod + // Einfacher Ansatz: erster Buchstabe von RoomID + // A-M → Pod 0, N-Z → Pod 1 + if len(roomID) == 0 { + return true + } + + firstChar := roomID[0] + + // Für 2 Replicas: A-M und N-Z splitten + if totalReplicas == "2" { + // Pod Namen sind meist: escape-game-0, escape-game-1, etc. + if firstChar <= 'M' || firstChar <= 'm' { + return serverID[len(serverID)-1] == '0' || !hasDigitSuffix() + } + return serverID[len(serverID)-1] == '1' || hasDigitSuffix() + } + + // Fallback: immer zuständig + return true +} + +func hasDigitSuffix() bool { + if len(serverID) == 0 { + return false + } + lastChar := serverID[len(serverID)-1] + return lastChar >= '0' && lastChar <= '9' +} + func loadServerAssets(w *game.World) { assetDir := "./cmd/client/web/assets" chunkDir := filepath.Join(assetDir, "chunks") diff --git a/k8s/app.yaml b/k8s/app.yaml index ea2d8f2..23f0f44 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -27,6 +27,12 @@ spec: value: "redis:6379" - name: NATS_URL value: "nats://nats:4222" + - name: TOTAL_REPLICAS + value: "2" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name livenessProbe: httpGet: path: /health