package snake import ( "time" ) const ( targetFPS = 60 tickInterval = time.Second / targetFPS snakeSpeed = 7 // cells per second moveInterval = time.Second / snakeSpeed countdownSeconds = 10 inactivityLimit = 60 * time.Second ) func (si *SnakeGameInstance) startOrResetCountdownLocked() { si.game.Status = StatusCountdown si.game.CountdownEnd = time.Now().Add(countdownSeconds * 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.persister != nil { si.persister.SaveSnakeGame(si.game) } si.gameMu.Unlock() si.notify() return } if si.persister != nil { si.persister.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)) } 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 } // Apply pending directions every tick for responsive input inputReceived := false for i := 0; i < 8; i++ { if si.pendingDir[i] != nil && i < len(si.game.State.Snakes) && si.game.State.Snakes[i] != nil { si.game.State.Snakes[i].Dir = *si.pendingDir[i] si.pendingDir[i] = nil inputReceived = true } } if inputReceived { lastInput = time.Now() } // Inactivity timeout if time.Since(lastInput) > inactivityLimit { si.game.Status = StatusFinished if si.persister != nil { si.persister.SaveSnakeGame(si.game) } si.gameMu.Unlock() si.notify() return } // Only advance game state at snakeSpeed moveAccum += tickInterval if moveAccum < moveInterval { si.gameMu.Unlock() continue } moveAccum -= moveInterval 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()) // Check game over alive := AliveCount(state) if alive <= 1 { si.game.Status = StatusFinished winnerIdx := LastAlive(state) if winnerIdx >= 0 && winnerIdx < len(si.game.Players) { si.game.Winner = si.game.Players[winnerIdx] } } if si.persister != nil { si.persister.SaveSnakeGame(si.game) } si.gameMu.Unlock() si.notify() if alive <= 1 { return } } } }