- Add brotli compression (level 5) to long-lived SSE event streams (HandleGameEvents, HandleSnakeEvents) to reduce wire payload - Fix all errcheck violations with nolint annotations for best-effort calls - Fix goimports: separate stdlib, third-party, and local import groups - Fix staticcheck: add package comments, use tagged switch - Zero lint issues remaining
177 lines
3.7 KiB
Go
177 lines
3.7 KiB
Go
package snake
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
type Direction int
|
|
|
|
const (
|
|
DirUp Direction = iota
|
|
DirDown
|
|
DirLeft
|
|
DirRight
|
|
)
|
|
|
|
type GameMode int
|
|
|
|
const (
|
|
ModeMultiplayer GameMode = iota // Default (0) - backward compatible
|
|
ModeSinglePlayer // Single player survival mode
|
|
)
|
|
|
|
// Opposite returns true if a and b are 180-degree reversals.
|
|
func (d Direction) Opposite(other Direction) bool {
|
|
switch d {
|
|
case DirUp:
|
|
return other == DirDown
|
|
case DirDown:
|
|
return other == DirUp
|
|
case DirLeft:
|
|
return other == DirRight
|
|
case DirRight:
|
|
return other == DirLeft
|
|
}
|
|
return false
|
|
}
|
|
|
|
type Point struct {
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
}
|
|
|
|
type Snake struct {
|
|
Body []Point `json:"body"`
|
|
Dir Direction `json:"dir"`
|
|
Alive bool `json:"alive"`
|
|
Growing bool `json:"growing"`
|
|
Color int `json:"color"` // 1-8
|
|
}
|
|
|
|
type GameState struct {
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Snakes []*Snake `json:"snakes"`
|
|
Food []Point `json:"food"`
|
|
}
|
|
|
|
func (gs *GameState) ToJSON() string {
|
|
data, _ := json.Marshal(gs)
|
|
return string(data)
|
|
}
|
|
|
|
func GameStateFromJSON(data string) (*GameState, error) {
|
|
var gs GameState
|
|
if err := json.Unmarshal([]byte(data), &gs); err != nil {
|
|
return nil, err
|
|
}
|
|
return &gs, nil
|
|
}
|
|
|
|
type Status int
|
|
|
|
const (
|
|
StatusWaitingForPlayers Status = iota
|
|
StatusCountdown
|
|
StatusInProgress
|
|
StatusFinished
|
|
)
|
|
|
|
type PlayerID string
|
|
|
|
type Player struct {
|
|
ID PlayerID
|
|
UserID *string
|
|
Nickname string
|
|
Slot int // 0-7
|
|
}
|
|
|
|
type SnakeGame struct {
|
|
ID string
|
|
State *GameState
|
|
Players []*Player // up to 8
|
|
Status Status
|
|
Winner *Player // nil if draw
|
|
CountdownEnd time.Time // when countdown reaches 0
|
|
RematchGameID *string
|
|
Mode GameMode // ModeMultiplayer or ModeSinglePlayer
|
|
Score int // tracks food eaten in single player
|
|
Speed int // cells per second
|
|
}
|
|
|
|
// SpeedPreset defines a named speed option for the snake game.
|
|
type SpeedPreset struct {
|
|
Name string
|
|
Speed int
|
|
}
|
|
|
|
var SpeedPresets = []SpeedPreset{
|
|
{Name: "Slow", Speed: 5},
|
|
{Name: "Normal", Speed: 7},
|
|
{Name: "Fast", Speed: 10},
|
|
{Name: "Insane", Speed: 15},
|
|
}
|
|
|
|
const DefaultSpeed = 7
|
|
|
|
func (sg *SnakeGame) IsFinished() bool {
|
|
return sg.Status == StatusFinished
|
|
}
|
|
|
|
func (sg *SnakeGame) PlayerCount() int {
|
|
count := 0
|
|
for _, p := range sg.Players {
|
|
if p != nil {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// GridPreset defines a named grid size option for the snake game.
|
|
type GridPreset struct {
|
|
Name string
|
|
Width int
|
|
Height int
|
|
}
|
|
|
|
var GridPresets = []GridPreset{
|
|
{Name: "Tiny", Width: 15, Height: 15},
|
|
{Name: "Small", Width: 20, Height: 20},
|
|
{Name: "Medium", Width: 30, Height: 20},
|
|
{Name: "Large", Width: 40, Height: 20},
|
|
{Name: "XL", Width: 50, Height: 30},
|
|
}
|
|
|
|
// snapshot returns a shallow copy of the game safe for reading outside the lock.
|
|
// Slices and pointers are shared but the top-level struct is copied.
|
|
func (sg *SnakeGame) snapshot() *SnakeGame {
|
|
cp := *sg
|
|
if sg.State != nil {
|
|
stateCp := *sg.State
|
|
// Copy slices so the caller's iteration is safe
|
|
stateCp.Snakes = make([]*Snake, len(sg.State.Snakes))
|
|
copy(stateCp.Snakes, sg.State.Snakes)
|
|
stateCp.Food = make([]Point, len(sg.State.Food))
|
|
copy(stateCp.Food, sg.State.Food)
|
|
cp.State = &stateCp
|
|
}
|
|
cp.Players = make([]*Player, len(sg.Players))
|
|
copy(cp.Players, sg.Players)
|
|
// Mode and Score are value types, already copied by *sg
|
|
return &cp
|
|
}
|
|
|
|
// SnakeColors are hex color values for CSS, indexed by player slot.
|
|
var SnakeColors = []string{
|
|
"#00b894", // 1: Green
|
|
"#e17055", // 2: Orange
|
|
"#0984e3", // 3: Blue
|
|
"#6c5ce7", // 4: Purple
|
|
"#fd79a8", // 5: Pink
|
|
"#00cec9", // 6: Cyan
|
|
"#d63031", // 7: Red
|
|
"#fdcb6e", // 8: Yellow
|
|
}
|