Files
games/game/store.go
2026-01-14 15:41:43 -10:00

183 lines
3.2 KiB
Go

package game
import (
"crypto/rand"
"encoding/hex"
"sync"
"time"
)
type Syncable interface {
Sync()
}
type PlayerSession struct {
Player *Player
Sync Syncable
}
type GameStore struct {
games map[string]*GameInstance
gamesMu sync.RWMutex
}
func NewGameStore() *GameStore {
return &GameStore{
games: make(map[string]*GameInstance),
}
}
func (gs *GameStore) Create() *GameInstance {
id := GenerateID(4)
gi := NewGameInstance(id)
gs.gamesMu.Lock()
gs.games[id] = gi
gs.gamesMu.Unlock()
go gi.run()
return gi
}
func (gs *GameStore) Get(id string) (*GameInstance, bool) {
gs.gamesMu.RLock()
defer gs.gamesMu.RUnlock()
gi, ok := gs.games[id]
return gi, ok
}
func GenerateID(size int) string {
b := make([]byte, size)
rand.Read(b)
return hex.EncodeToString(b)
}
type GameInstance struct {
game *Game
gameMu sync.RWMutex
players map[PlayerID]Syncable
playersMu sync.RWMutex
leave chan PlayerID
done chan struct{}
dirty bool
}
func NewGameInstance(id string) *GameInstance {
return &GameInstance{
game: NewGame(id),
players: make(map[PlayerID]Syncable),
leave: make(chan PlayerID, 5),
done: make(chan struct{}),
}
}
func (gi *GameInstance) ID() string {
gi.gameMu.RLock()
defer gi.gameMu.RUnlock()
return gi.game.ID
}
func (gi *GameInstance) Join(ps *PlayerSession) bool {
gi.gameMu.Lock()
defer gi.gameMu.Unlock()
// Assign player to an open slot
if gi.game.Players[0] == nil {
ps.Player.Color = 1 // Red
gi.game.Players[0] = ps.Player
} else if gi.game.Players[1] == nil {
ps.Player.Color = 2 // Yellow
gi.game.Players[1] = ps.Player
gi.game.Status = StatusInProgress
} else {
return false // Game is full
}
gi.playersMu.Lock()
gi.players[ps.Player.ID] = ps.Sync
gi.playersMu.Unlock()
gi.dirty = true
return true
}
func (gi *GameInstance) GetGame() *Game {
gi.gameMu.RLock()
defer gi.gameMu.RUnlock()
return gi.game
}
func (gi *GameInstance) GetPlayerColor(pid PlayerID) int {
gi.gameMu.RLock()
defer gi.gameMu.RUnlock()
for _, p := range gi.game.Players {
if p != nil && p.ID == pid {
return p.Color
}
}
return 0
}
func (gi *GameInstance) DropPiece(col int, playerColor int) bool {
gi.gameMu.Lock()
defer gi.gameMu.Unlock()
row, ok := gi.game.DropPiece(col, playerColor)
if !ok {
return false
}
if gi.game.CheckWin(row, col) {
for _, p := range gi.game.Players {
if p != nil && p.Color == playerColor {
gi.game.Winner = p
break
}
}
} else if gi.game.CheckDraw() {
// Status already set by CheckDraw
} else {
gi.game.SwitchTurn()
}
gi.dirty = true
return true
}
func (gi *GameInstance) run() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case pid := <-gi.leave:
gi.playersMu.Lock()
delete(gi.players, pid)
gi.playersMu.Unlock()
case <-ticker.C:
gi.publish()
case <-gi.done:
return
}
}
}
func (gi *GameInstance) publish() {
gi.gameMu.Lock()
if !gi.dirty {
gi.gameMu.Unlock()
return
}
gi.dirty = false
gi.playersMu.RLock()
syncers := make([]Syncable, 0, len(gi.players))
for _, sync := range gi.players {
syncers = append(syncers, sync)
}
gi.playersMu.RUnlock()
gi.gameMu.Unlock()
for _, sync := range syncers {
sync.Sync()
}
}