package snake import ( "time" ) const ( targetFPS = 60 tickInterval = time.Second / targetFPS countdownSecondsMultiplayer = 10 countdownSecondsSinglePlayer = 3 inactivityLimit = 60 * time.Second ) func (si *SnakeGameInstance) startOrResetCountdownLocked() { si.game.Status = StatusCountdown countdown := countdownSecondsMultiplayer if si.game.Mode == ModeSinglePlayer { countdown = countdownSecondsSinglePlayer } si.game.CountdownEnd = time.Now().Add(time.Duration(countdown) * time.Second) si.loopOnce.Do(func() { si.stopCh = make(chan struct{}) go si.runLoop() }) } func (si *SnakeGameInstance) runLoop() { si.countdownPhase() si.gameMu.RLock() stopped := si.game.Status != StatusInProgress si.gameMu.RUnlock() if stopped { return } si.gamePhase() } func (si *SnakeGameInstance) countdownPhase() { ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case <-si.stopCh: return case <-ticker.C: si.gameMu.Lock() if si.game.Status != StatusCountdown { si.gameMu.Unlock() return } remaining := time.Until(si.game.CountdownEnd) if remaining <= 0 { si.initGame() si.game.Status = StatusInProgress if si.queries != nil { si.saveSnakeGame(si.game) } si.gameMu.Unlock() si.notify() return } if si.queries != nil { si.saveSnakeGame(si.game) } si.gameMu.Unlock() si.notify() } } } // initGame sets up snakes and food for the start of a game. func (si *SnakeGameInstance) initGame() { // Collect active player slots var activeSlots []int for i, p := range si.game.Players { if p != nil { activeSlots = append(activeSlots, i) } } state := si.game.State state.Snakes = SpawnSnakes(activeSlots, state.Width, state.Height) SpawnFood(state, len(activeSlots), si.game.Mode) } func (si *SnakeGameInstance) gamePhase() { ticker := time.NewTicker(tickInterval) defer ticker.Stop() lastInput := time.Now() var moveAccum time.Duration for { select { case <-si.stopCh: return case <-ticker.C: si.gameMu.Lock() if si.game.Status != StatusInProgress { si.gameMu.Unlock() return } // Track input activity for inactivity timeout for i := 0; i < 8; i++ { if len(si.pendingDirQueue[i]) > 0 { lastInput = time.Now() break } } // Inactivity timeout if time.Since(lastInput) > inactivityLimit { si.game.Status = StatusFinished if si.queries != nil { si.saveSnakeGame(si.game) } si.gameMu.Unlock() si.notify() return } // Compute move interval from per-game speed speed := si.game.Speed if speed <= 0 { speed = DefaultSpeed } moveInterval := time.Second / time.Duration(speed) // Only advance game state at game speed moveAccum += tickInterval if moveAccum < moveInterval { si.gameMu.Unlock() continue } moveAccum -= moveInterval // Pop one direction from queue per movement frame for i := 0; i < 8; i++ { if len(si.pendingDirQueue[i]) > 0 && i < len(si.game.State.Snakes) && si.game.State.Snakes[i] != nil { si.game.State.Snakes[i].Dir = si.pendingDirQueue[i][0] si.pendingDirQueue[i] = si.pendingDirQueue[i][1:] } } state := si.game.State // Advance snakes Tick(state) // Check collisions first (before food, so dead snakes don't eat) dead := CheckCollisions(state) MarkDead(state, dead) // Check food eaten (only by surviving snakes) eaten := CheckFood(state) RemoveFood(state, eaten) SpawnFood(state, si.game.PlayerCount(), si.game.Mode) // Track score for single player si.game.Score += len(eaten) // Check game over alive := AliveCount(state) gameOver := false if si.game.Mode == ModeSinglePlayer { // Single player ends when the player dies (alive == 0) if alive == 0 { gameOver = true // No winner in single player - just final score } } else { // Multiplayer ends when 1 or fewer alive if alive <= 1 { gameOver = true winnerIdx := LastAlive(state) if winnerIdx >= 0 && winnerIdx < len(si.game.Players) { si.game.Winner = si.game.Players[winnerIdx] } } } if gameOver { si.game.Status = StatusFinished } if si.queries != nil { si.saveSnakeGame(si.game) } si.gameMu.Unlock() si.notify() if gameOver { return } } } }