Use via's embedded NATS server to notify players of state changes instead of a 100ms polling ticker. Each player subscribes to "game.<id>" on page load; via auto-cleans subscriptions on disconnect, eliminating the need for manual player tracking and RegisterSync.
244 lines
4.3 KiB
Go
244 lines
4.3 KiB
Go
package game
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"sync"
|
|
)
|
|
|
|
type PubSub interface {
|
|
Publish(subject string, data []byte) error
|
|
}
|
|
|
|
type PlayerSession struct {
|
|
Player *Player
|
|
}
|
|
|
|
type Persister interface {
|
|
SaveGame(g *Game) error
|
|
LoadGame(id string) (*Game, error)
|
|
SaveGamePlayer(gameID string, player *Player, slot int) error
|
|
LoadGamePlayers(gameID string) ([]*Player, error)
|
|
DeleteGame(id string) error
|
|
}
|
|
|
|
type GameStore struct {
|
|
games map[string]*GameInstance
|
|
gamesMu sync.RWMutex
|
|
persister Persister
|
|
pubsub PubSub
|
|
}
|
|
|
|
func NewGameStore() *GameStore {
|
|
return &GameStore{
|
|
games: make(map[string]*GameInstance),
|
|
}
|
|
}
|
|
|
|
func (gs *GameStore) SetPersister(p Persister) {
|
|
gs.persister = p
|
|
}
|
|
|
|
func (gs *GameStore) SetPubSub(ps PubSub) {
|
|
gs.pubsub = ps
|
|
}
|
|
|
|
func (gs *GameStore) makeNotify(gameID string) func() {
|
|
return func() {
|
|
if gs.pubsub != nil {
|
|
gs.pubsub.Publish("game."+gameID, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (gs *GameStore) Create() *GameInstance {
|
|
id := GenerateID(4)
|
|
gi := NewGameInstance(id)
|
|
gi.persister = gs.persister
|
|
gi.notify = gs.makeNotify(id)
|
|
gs.gamesMu.Lock()
|
|
gs.games[id] = gi
|
|
gs.gamesMu.Unlock()
|
|
|
|
if gs.persister != nil {
|
|
gs.persister.SaveGame(gi.game)
|
|
}
|
|
|
|
return gi
|
|
}
|
|
|
|
func (gs *GameStore) Get(id string) (*GameInstance, bool) {
|
|
gs.gamesMu.RLock()
|
|
gi, ok := gs.games[id]
|
|
gs.gamesMu.RUnlock()
|
|
|
|
if ok {
|
|
return gi, true
|
|
}
|
|
|
|
if gs.persister == nil {
|
|
return nil, false
|
|
}
|
|
|
|
game, err := gs.persister.LoadGame(id)
|
|
if err != nil || game == nil {
|
|
return nil, false
|
|
}
|
|
|
|
players, _ := gs.persister.LoadGamePlayers(id)
|
|
for _, p := range players {
|
|
if p.Color == 1 {
|
|
game.Players[0] = p
|
|
} else if p.Color == 2 {
|
|
game.Players[1] = p
|
|
}
|
|
}
|
|
|
|
gi = &GameInstance{
|
|
game: game,
|
|
persister: gs.persister,
|
|
notify: gs.makeNotify(id),
|
|
}
|
|
|
|
gs.gamesMu.Lock()
|
|
gs.games[id] = gi
|
|
gs.gamesMu.Unlock()
|
|
|
|
return gi, true
|
|
}
|
|
|
|
func (gs *GameStore) Delete(id string) error {
|
|
gs.gamesMu.Lock()
|
|
delete(gs.games, id)
|
|
gs.gamesMu.Unlock()
|
|
|
|
if gs.persister != nil {
|
|
return gs.persister.DeleteGame(id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GenerateID(size int) string {
|
|
b := make([]byte, size)
|
|
rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
type GameInstance struct {
|
|
game *Game
|
|
gameMu sync.RWMutex
|
|
notify func()
|
|
persister Persister
|
|
}
|
|
|
|
func NewGameInstance(id string) *GameInstance {
|
|
return &GameInstance{
|
|
game: NewGame(id),
|
|
notify: func() {},
|
|
}
|
|
}
|
|
|
|
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()
|
|
|
|
var slot int
|
|
if gi.game.Players[0] == nil {
|
|
ps.Player.Color = 1
|
|
gi.game.Players[0] = ps.Player
|
|
slot = 0
|
|
} else if gi.game.Players[1] == nil {
|
|
ps.Player.Color = 2
|
|
gi.game.Players[1] = ps.Player
|
|
gi.game.Status = StatusInProgress
|
|
slot = 1
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
if gi.persister != nil {
|
|
gi.persister.SaveGamePlayer(gi.game.ID, ps.Player, slot)
|
|
gi.persister.SaveGame(gi.game)
|
|
}
|
|
|
|
gi.notify()
|
|
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) CreateRematch(gs *GameStore) *GameInstance {
|
|
gi.gameMu.Lock()
|
|
defer gi.gameMu.Unlock()
|
|
|
|
if !gi.game.IsFinished() || gi.game.RematchGameID != nil {
|
|
return nil
|
|
}
|
|
|
|
newGI := gs.Create()
|
|
newID := newGI.ID()
|
|
gi.game.RematchGameID = &newID
|
|
|
|
if gi.persister != nil {
|
|
if err := gi.persister.SaveGame(gi.game); err != nil {
|
|
gs.Delete(newID)
|
|
gi.game.RematchGameID = nil
|
|
return nil
|
|
}
|
|
}
|
|
|
|
gi.notify()
|
|
return newGI
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
if gi.persister != nil {
|
|
gi.persister.SaveGame(gi.game)
|
|
}
|
|
|
|
gi.notify()
|
|
return true
|
|
}
|