v0.10.0 fix to engine game state mgmt, client and web ui updates to match

This commit is contained in:
2026-07-23 14:03:09 -04:00
parent 882c83c9a4
commit c0fc2a9667
17 changed files with 741 additions and 549 deletions
+36 -8
View File
@@ -113,7 +113,7 @@ func (s *Service) ApplyMove(gameID, moveUCI, newFEN string) error {
g.AddSnapshot(newFEN, moveUCI, nextTurn)
// Notify waiting clients about the state change
s.waiter.NotifyGame(gameID, len(g.Moves()))
s.waiter.NotifyGame(gameID, len(g.Moves()), g.State())
// Persist if storage enabled
if s.store != nil {
@@ -132,6 +132,36 @@ func (s *Service) ApplyMove(gameID, moveUCI, newFEN string) error {
return nil
}
// ApplyMoveWithState atomically records a move, its resulting state, and move
// metadata, then notifies waiters exactly once with the settled state.
func (s *Service) ApplyMoveWithState(gameID, moveUCI, newFEN string, state core.State, result *game.MoveResult) error {
s.mu.Lock()
defer s.mu.Unlock()
g, ok := s.games[gameID]
if !ok {
return fmt.Errorf("game not found: %s", gameID)
}
currentTurn := g.NextTurnColor()
g.AddSnapshot(newFEN, moveUCI, core.OppositeColor(currentTurn))
g.SetState(state)
if result != nil {
g.SetLastResult(result)
}
s.waiter.NotifyGame(gameID, len(g.Moves()), state)
if s.store != nil {
s.store.RecordMove(storage.MoveRecord{
GameID: gameID, MoveNumber: len(g.Moves()), MoveUCI: moveUCI,
FENAfterMove: newFEN, PlayerColor: currentTurn.String(),
MoveTimeUTC: time.Now().UTC(),
})
}
return nil
}
// UpdateGameState sets the game's end state (checkmate, stalemate, etc)
func (s *Service) UpdateGameState(gameID string, state core.State) error {
s.mu.Lock()
@@ -143,11 +173,8 @@ func (s *Service) UpdateGameState(gameID string, state core.State) error {
}
g.SetState(state)
// Notify if game ended
if state != core.StateOngoing && state != core.StatePending {
s.waiter.NotifyGame(gameID, len(g.Moves()))
}
// Notify unconditionally; the registry decides.
s.waiter.NotifyGame(gameID, len(g.Moves()), state)
return nil
}
@@ -183,7 +210,7 @@ func (s *Service) UndoMoves(gameID string, count int) error {
}
// Notify waiting clients about the undo
s.waiter.NotifyGame(gameID, len(g.Moves()))
s.waiter.NotifyGame(gameID, len(g.Moves()), g.State())
// Delete undone moves from storage if enabled
if s.store != nil {
@@ -214,4 +241,5 @@ func (s *Service) DeleteGame(gameID string) error {
delete(s.games, gameID)
return nil
}
}
+4 -9
View File
@@ -1,6 +1,7 @@
package service
import (
"chess/internal/server/core"
"context"
"fmt"
"sync"
@@ -84,24 +85,19 @@ func (w *WaitRegistry) RegisterWait(gameID string, moveCount int, ctx context.Co
}
// NotifyGame notifies all clients waiting on a game about state change
func (w *WaitRegistry) NotifyGame(gameID string, currentMoveCount int) {
func (w *WaitRegistry) NotifyGame(gameID string, currentMoveCount int, state core.State) {
w.mu.RLock()
waitList := w.waiters[gameID]
w.mu.RUnlock()
if len(waitList) == 0 {
return
}
// Non-blocking notification to all waiters
settled := state != core.StateOngoing && state != core.StatePending
for _, req := range waitList {
// Only notify if move count changed
if req.MoveCount != currentMoveCount {
if settled || req.MoveCount != currentMoveCount {
select {
case req.Notify <- struct{}{}:
// Notification sent
default:
// Channel full or closed, skip slow client
}
}
}
@@ -175,4 +171,3 @@ func (w *WaitRegistry) removeWaiter(gameID string, req *WaitRequest) {
// Stop timer if still running
req.Timer.Stop()
}