Files
games/snake/types.go
Ryan Hamamura 063b03ce25 refactor: extract shared player.ID type and GenerateID to player package
Both game and snake packages had identical PlayerID types and the snake
package imported game.GenerateID. Now both use player.ID and
player.GenerateID from the shared player package.
2026-03-02 19:09:01 -10:00

177 lines
3.7 KiB
Go

package snake
import (
"encoding/json"
"time"
"github.com/ryanhamamura/c4/player"
)
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
}