Files
games/snake/types.go
Ryan Hamamura 7e78664534 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).
2026-02-02 07:26:28 -10:00

149 lines
2.9 KiB
Go

package snake
import (
"encoding/json"
"time"
)
type Direction int
const (
DirUp Direction = iota
DirDown
DirLeft
DirRight
)
// 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
}
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
}
// Grid presets
type GridPreset struct {
Name string
Width int
Height int
}
var GridPresets = []GridPreset{
{Name: "Small", Width: 20, Height: 20},
{Name: "Medium", Width: 30, Height: 20},
{Name: "Large", Width: 40, Height: 20},
}
// 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)
return &cp
}
// Snake colors (hex values for CSS)
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
}