package snake import ( "encoding/json" "time" "github.com/ryanhamamura/games/player" ) // SubjectPrefix is the NATS subject namespace for snake games. const SubjectPrefix = "snake" // GameSubject returns the NATS subject for game state updates. func GameSubject(gameID string) string { return SubjectPrefix + "." + gameID } // ChatSubject returns the NATS subject for chat messages. func ChatSubject(gameID string) string { return SubjectPrefix + ".chat." + gameID } 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 Player struct { ID player.ID 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 }