feat: add single player snake mode

Add solo mode where players survive as long as possible while tracking
score (food eaten). Single player games start with a shorter 3-second
countdown vs 10 seconds for multiplayer, maintain exactly 1 food item
for classic snake feel, and end when the player dies rather than when
one player remains.

- Add GameMode type (ModeMultiplayer/ModeSinglePlayer) and Score field
- Filter single player games from "Join a Game" lobby list
- Show "Ready?" and "Score: X" UI for single player mode
- Hide invite link for single player games
- Preserve game mode on rematch
This commit is contained in:
Ryan Hamamura
2026-02-04 07:33:02 -10:00
parent 7faf94fa6d
commit f454e0d220
14 changed files with 205 additions and 78 deletions

View File

@@ -5,17 +5,23 @@ import (
)
const (
targetFPS = 60
tickInterval = time.Second / targetFPS
snakeSpeed = 7 // cells per second
moveInterval = time.Second / snakeSpeed
countdownSeconds = 10
inactivityLimit = 60 * time.Second
targetFPS = 60
tickInterval = time.Second / targetFPS
snakeSpeed = 7 // cells per second
moveInterval = time.Second / snakeSpeed
countdownSecondsMultiplayer = 10
countdownSecondsSinglePlayer = 3
inactivityLimit = 60 * time.Second
)
func (si *SnakeGameInstance) startOrResetCountdownLocked() {
si.game.Status = StatusCountdown
si.game.CountdownEnd = time.Now().Add(countdownSeconds * time.Second)
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{})
@@ -86,7 +92,7 @@ func (si *SnakeGameInstance) initGame() {
state := si.game.State
state.Snakes = SpawnSnakes(activeSlots, state.Width, state.Height)
SpawnFood(state, len(activeSlots))
SpawnFood(state, len(activeSlots), si.game.Mode)
}
func (si *SnakeGameInstance) gamePhase() {
@@ -152,16 +158,33 @@ func (si *SnakeGameInstance) gamePhase() {
// Check food eaten (only by surviving snakes)
eaten := CheckFood(state)
RemoveFood(state, eaten)
SpawnFood(state, si.game.PlayerCount())
SpawnFood(state, si.game.PlayerCount(), si.game.Mode)
// Track score for single player
si.game.Score += len(eaten)
// 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]
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.persister != nil {
@@ -171,7 +194,7 @@ func (si *SnakeGameInstance) gamePhase() {
si.gameMu.Unlock()
si.notify()
if alive <= 1 {
if gameOver {
return
}
}