WIP: Add multiplayer Snake game
N-player (2-8) real-time Snake game alongside Connect 4. Lobby has tabs to switch between games. Players join via invite link with 10-second countdown. Game loop runs at tick-based intervals with NATS pub/sub for state sync. Keyboard input not yet working (Datastar keydown binding issue still under investigation).
This commit is contained in:
167
snake/loop.go
Normal file
167
snake/loop.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package snake
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
tickInterval = 500 * time.Millisecond
|
||||
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()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-si.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
si.gameMu.Lock()
|
||||
|
||||
if si.game.Status != StatusInProgress {
|
||||
si.gameMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Apply pending directions (iterate all 8 slots)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user