fix: deep-copy GameState in GetState to prevent data race
Replace shallow struct copy with full deep copy of Players, Monsters, Floor/Rooms, Inventory, Relics, ShopItems, and CombatLog slices so concurrent readers via GetState never alias the combatLoop's live data. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -155,7 +155,58 @@ func (s *GameSession) StartFloor() {
|
||||
func (s *GameSession) GetState() GameState {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.state
|
||||
|
||||
// Deep copy players
|
||||
players := make([]*entity.Player, len(s.state.Players))
|
||||
for i, p := range s.state.Players {
|
||||
cp := *p
|
||||
cp.Inventory = make([]entity.Item, len(p.Inventory))
|
||||
copy(cp.Inventory, p.Inventory)
|
||||
cp.Relics = make([]entity.Relic, len(p.Relics))
|
||||
copy(cp.Relics, p.Relics)
|
||||
players[i] = &cp
|
||||
}
|
||||
|
||||
// Deep copy monsters
|
||||
monsters := make([]*entity.Monster, len(s.state.Monsters))
|
||||
for i, m := range s.state.Monsters {
|
||||
cm := *m
|
||||
monsters[i] = &cm
|
||||
}
|
||||
|
||||
// Deep copy floor
|
||||
var floorCopy *dungeon.Floor
|
||||
if s.state.Floor != nil {
|
||||
fc := *s.state.Floor
|
||||
fc.Rooms = make([]*dungeon.Room, len(s.state.Floor.Rooms))
|
||||
for i, r := range s.state.Floor.Rooms {
|
||||
rc := *r
|
||||
rc.Neighbors = make([]int, len(r.Neighbors))
|
||||
copy(rc.Neighbors, r.Neighbors)
|
||||
fc.Rooms[i] = &rc
|
||||
}
|
||||
floorCopy = &fc
|
||||
}
|
||||
|
||||
// Copy combat log
|
||||
logCopy := make([]string, len(s.state.CombatLog))
|
||||
copy(logCopy, s.state.CombatLog)
|
||||
|
||||
return GameState{
|
||||
Floor: floorCopy,
|
||||
Players: players,
|
||||
Monsters: monsters,
|
||||
Phase: s.state.Phase,
|
||||
FloorNum: s.state.FloorNum,
|
||||
TurnNum: s.state.TurnNum,
|
||||
CombatTurn: s.state.CombatTurn,
|
||||
SoloMode: s.state.SoloMode,
|
||||
GameOver: s.state.GameOver,
|
||||
Victory: s.state.Victory,
|
||||
ShopItems: append([]entity.Item{}, s.state.ShopItems...),
|
||||
CombatLog: logCopy,
|
||||
TurnDeadline: s.state.TurnDeadline,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameSession) SubmitAction(playerName string, action PlayerAction) {
|
||||
|
||||
@@ -7,6 +7,37 @@ import (
|
||||
"github.com/tolelom/catacombs/entity"
|
||||
)
|
||||
|
||||
func TestGetStateNoRace(t *testing.T) {
|
||||
s := NewGameSession()
|
||||
p := entity.NewPlayer("Racer", entity.ClassWarrior)
|
||||
s.AddPlayer(p)
|
||||
s.StartGame()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for i := 0; i < 100; i++ {
|
||||
st := s.GetState()
|
||||
for _, p := range st.Players {
|
||||
_ = p.HP
|
||||
_ = p.Gold
|
||||
}
|
||||
for _, m := range st.Monsters {
|
||||
_ = m.HP
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
select {
|
||||
case s.actionCh <- playerActionMsg{PlayerName: "Racer", Action: PlayerAction{Type: ActionWait}}:
|
||||
default:
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestSessionTurnTimeout(t *testing.T) {
|
||||
s := NewGameSession()
|
||||
p := entity.NewPlayer("test", entity.ClassWarrior)
|
||||
|
||||
Reference in New Issue
Block a user