Replace polling loop with NATS pub/sub for game updates
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.
This commit is contained in:
108
game/store.go
108
game/store.go
@@ -4,16 +4,14 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Syncable interface {
|
||||
Sync()
|
||||
type PubSub interface {
|
||||
Publish(subject string, data []byte) error
|
||||
}
|
||||
|
||||
type PlayerSession struct {
|
||||
Player *Player
|
||||
Sync Syncable
|
||||
}
|
||||
|
||||
type Persister interface {
|
||||
@@ -28,6 +26,7 @@ type GameStore struct {
|
||||
games map[string]*GameInstance
|
||||
gamesMu sync.RWMutex
|
||||
persister Persister
|
||||
pubsub PubSub
|
||||
}
|
||||
|
||||
func NewGameStore() *GameStore {
|
||||
@@ -40,10 +39,23 @@ 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()
|
||||
@@ -52,7 +64,6 @@ func (gs *GameStore) Create() *GameInstance {
|
||||
gs.persister.SaveGame(gi.game)
|
||||
}
|
||||
|
||||
go gi.run()
|
||||
return gi
|
||||
}
|
||||
|
||||
@@ -65,7 +76,6 @@ func (gs *GameStore) Get(id string) (*GameInstance, bool) {
|
||||
return gi, true
|
||||
}
|
||||
|
||||
// Try to load from database
|
||||
if gs.persister == nil {
|
||||
return nil, false
|
||||
}
|
||||
@@ -86,27 +96,20 @@ func (gs *GameStore) Get(id string) (*GameInstance, bool) {
|
||||
|
||||
gi = &GameInstance{
|
||||
game: game,
|
||||
players: make(map[PlayerID]Syncable),
|
||||
leave: make(chan PlayerID, 5),
|
||||
done: make(chan struct{}),
|
||||
persister: gs.persister,
|
||||
notify: gs.makeNotify(id),
|
||||
}
|
||||
|
||||
gs.gamesMu.Lock()
|
||||
gs.games[id] = gi
|
||||
gs.gamesMu.Unlock()
|
||||
|
||||
go gi.run()
|
||||
return gi, true
|
||||
}
|
||||
|
||||
func (gs *GameStore) Delete(id string) error {
|
||||
gs.gamesMu.Lock()
|
||||
gi, ok := gs.games[id]
|
||||
if ok {
|
||||
delete(gs.games, id)
|
||||
close(gi.done)
|
||||
}
|
||||
delete(gs.games, id)
|
||||
gs.gamesMu.Unlock()
|
||||
|
||||
if gs.persister != nil {
|
||||
@@ -124,20 +127,14 @@ func GenerateID(size int) string {
|
||||
type GameInstance struct {
|
||||
game *Game
|
||||
gameMu sync.RWMutex
|
||||
players map[PlayerID]Syncable
|
||||
playersMu sync.RWMutex
|
||||
leave chan PlayerID
|
||||
done chan struct{}
|
||||
dirty bool
|
||||
notify func()
|
||||
persister Persister
|
||||
}
|
||||
|
||||
func NewGameInstance(id string) *GameInstance {
|
||||
return &GameInstance{
|
||||
game: NewGame(id),
|
||||
players: make(map[PlayerID]Syncable),
|
||||
leave: make(chan PlayerID, 5),
|
||||
done: make(chan struct{}),
|
||||
game: NewGame(id),
|
||||
notify: func() {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,30 +149,25 @@ func (gi *GameInstance) Join(ps *PlayerSession) bool {
|
||||
defer gi.gameMu.Unlock()
|
||||
|
||||
var slot int
|
||||
// Assign player to an open slot
|
||||
if gi.game.Players[0] == nil {
|
||||
ps.Player.Color = 1 // Red
|
||||
ps.Player.Color = 1
|
||||
gi.game.Players[0] = ps.Player
|
||||
slot = 0
|
||||
} else if gi.game.Players[1] == nil {
|
||||
ps.Player.Color = 2 // Yellow
|
||||
ps.Player.Color = 2
|
||||
gi.game.Players[1] = ps.Player
|
||||
gi.game.Status = StatusInProgress
|
||||
slot = 1
|
||||
} else {
|
||||
return false // Game is full
|
||||
return false
|
||||
}
|
||||
|
||||
gi.playersMu.Lock()
|
||||
gi.players[ps.Player.ID] = ps.Sync
|
||||
gi.playersMu.Unlock()
|
||||
|
||||
if gi.persister != nil {
|
||||
gi.persister.SaveGamePlayer(gi.game.ID, ps.Player, slot)
|
||||
gi.persister.SaveGame(gi.game)
|
||||
}
|
||||
|
||||
gi.dirty = true
|
||||
gi.notify()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -196,13 +188,6 @@ func (gi *GameInstance) GetPlayerColor(pid PlayerID) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (gi *GameInstance) RegisterSync(playerID PlayerID, sync Syncable) {
|
||||
gi.playersMu.Lock()
|
||||
gi.players[playerID] = sync
|
||||
gi.playersMu.Unlock()
|
||||
gi.dirty = true
|
||||
}
|
||||
|
||||
func (gi *GameInstance) CreateRematch(gs *GameStore) *GameInstance {
|
||||
gi.gameMu.Lock()
|
||||
defer gi.gameMu.Unlock()
|
||||
@@ -223,7 +208,7 @@ func (gi *GameInstance) CreateRematch(gs *GameStore) *GameInstance {
|
||||
}
|
||||
}
|
||||
|
||||
gi.dirty = true
|
||||
gi.notify()
|
||||
return newGI
|
||||
}
|
||||
|
||||
@@ -253,45 +238,6 @@ func (gi *GameInstance) DropPiece(col int, playerColor int) bool {
|
||||
gi.persister.SaveGame(gi.game)
|
||||
}
|
||||
|
||||
gi.dirty = true
|
||||
gi.notify()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user