// Package snake implements snake game logic, state management, and persistence. package snake import "math/rand" // SpawnSnakes creates snakes for the given active slot indices, evenly spaced // around the grid perimeter facing inward. Returns a length-8 slice where // only active slots have non-nil entries. func SpawnSnakes(activeSlots []int, width, height int) []*Snake { snakes := make([]*Snake, 8) n := len(activeSlots) if n == 0 { return snakes } perimeter := 2*(width+height) - 4 spacing := perimeter / n for i, slot := range activeSlots { pos := (i * spacing) + spacing/2 sp := perimeterToSpawn(pos, width, height) body := make([]Point, 3) dx, dy := dirDelta(sp.dir) for j := 0; j < 3; j++ { bx := sp.x - dx*j by := sp.y - dy*j // Clamp to grid bounds if bx < 0 { bx = 0 } else if bx >= width { bx = width - 1 } if by < 0 { by = 0 } else if by >= height { by = height - 1 } body[j] = Point{X: bx, Y: by} } snakes[slot] = &Snake{ Body: body, Dir: sp.dir, Alive: true, Color: slot + 1, } } return snakes } type spawnInfo struct { x, y int dir Direction } // perimeterToSpawn converts a linear position along the perimeter to coordinates // and an inward-facing direction. func perimeterToSpawn(pos, width, height int) spawnInfo { // Top edge: left to right if pos < width { return spawnInfo{pos, 0, DirDown} } pos -= width // Right edge: top to bottom if pos < height-1 { return spawnInfo{width - 1, pos + 1, DirLeft} } pos -= height - 1 // Bottom edge: right to left if pos < width-1 { return spawnInfo{width - 2 - pos, height - 1, DirUp} } pos -= width - 1 // Left edge: bottom to top return spawnInfo{0, height - 2 - pos, DirRight} } func dirDelta(d Direction) (dx, dy int) { switch d { case DirUp: return 0, -1 case DirDown: return 0, 1 case DirLeft: return -1, 0 case DirRight: return 1, 0 } return 0, 0 } // Tick advances all alive snakes one cell in their direction. func Tick(state *GameState) { for _, s := range state.Snakes { if s == nil || !s.Alive { continue } dx, dy := dirDelta(s.Dir) head := s.Body[0] newHead := Point{X: head.X + dx, Y: head.Y + dy} s.Body = append([]Point{newHead}, s.Body...) if s.Growing { s.Growing = false } else { s.Body = s.Body[:len(s.Body)-1] } } } // CheckFood checks if any snake head is on food. Returns indices of eaten food. func CheckFood(state *GameState) []int { eaten := make(map[int]bool) for _, s := range state.Snakes { if s == nil || !s.Alive { continue } head := s.Body[0] for fi, f := range state.Food { if !eaten[fi] && head.X == f.X && head.Y == f.Y { s.Growing = true eaten[fi] = true break } } } var indices []int for fi := range eaten { indices = append(indices, fi) } return indices } // RemoveFood removes food items at the given indices. func RemoveFood(state *GameState, indices []int) { if len(indices) == 0 { return } remove := make(map[int]bool, len(indices)) for _, i := range indices { remove[i] = true } var remaining []Point for i, f := range state.Food { if !remove[i] { remaining = append(remaining, f) } } state.Food = remaining } // SpawnFood adds food items to maintain the target count. // Single player always maintains exactly 1 food for classic snake feel. func SpawnFood(state *GameState, playerCount int, mode GameMode) { target := playerCount/2 + 1 if mode == ModeSinglePlayer { target = 1 } for len(state.Food) < target { p := randomEmptyCell(state) if p == nil { break } state.Food = append(state.Food, *p) } } func randomEmptyCell(state *GameState) *Point { occupied := make(map[Point]bool) for _, s := range state.Snakes { if s == nil { continue } for _, p := range s.Body { occupied[p] = true } } for _, f := range state.Food { occupied[f] = true } var empty []Point for y := 0; y < state.Height; y++ { for x := 0; x < state.Width; x++ { p := Point{X: x, Y: y} if !occupied[p] { empty = append(empty, p) } } } if len(empty) == 0 { return nil } choice := empty[rand.Intn(len(empty))] return &choice } // CheckCollisions checks for wall, self, and other-snake collisions. // Returns the set of snake indices that died this tick. func CheckCollisions(state *GameState) map[int]bool { dead := make(map[int]bool) for i, s := range state.Snakes { if s == nil || !s.Alive { continue } head := s.Body[0] // Wall collision if head.X < 0 || head.X >= state.Width || head.Y < 0 || head.Y >= state.Height { dead[i] = true continue } // Self-body collision (skip head at index 0) for _, bp := range s.Body[1:] { if head.X == bp.X && head.Y == bp.Y { dead[i] = true break } } if dead[i] { continue } // Other snake body collision for j, other := range state.Snakes { if i == j || other == nil || !other.Alive { continue } for _, bp := range other.Body[1:] { if head.X == bp.X && head.Y == bp.Y { dead[i] = true break } } if dead[i] { break } } } // Head-to-head collision for i, s := range state.Snakes { if s == nil || !s.Alive || dead[i] { continue } for j := i + 1; j < len(state.Snakes); j++ { other := state.Snakes[j] if other == nil || !other.Alive || dead[j] { continue } if s.Body[0].X == other.Body[0].X && s.Body[0].Y == other.Body[0].Y { dead[i] = true dead[j] = true } } } return dead } // MarkDead sets Alive=false for snakes in the dead set. func MarkDead(state *GameState, dead map[int]bool) { for i := range dead { state.Snakes[i].Alive = false } } // AliveCount returns the number of living snakes. func AliveCount(state *GameState) int { count := 0 for _, s := range state.Snakes { if s != nil && s.Alive { count++ } } return count } // LastAlive returns the index of the last alive snake, or -1 if none or multiple. func LastAlive(state *GameState) int { idx := -1 for i, s := range state.Snakes { if s != nil && s.Alive { if idx != -1 { return -1 } idx = i } } return idx } // ValidateDirection returns true if the new direction is not a 180 reversal. func ValidateDirection(current, proposed Direction) bool { return !current.Opposite(proposed) }