Skip to content

Animate individual PvB and PvP moves in lobby live viewer#731

Open
benckx with Copilot wants to merge 15 commits into
masterfrom
copilot/animate-individual-moves
Open

Animate individual PvB and PvP moves in lobby live viewer#731
benckx with Copilot wants to merge 15 commits into
masterfrom
copilot/animate-individual-moves

Conversation

Copilot AI commented May 28, 2026

Copy link
Copy Markdown
Contributor

PvB and PvP thumbnails in the lobby live viewer always jumped 2 positions at once — a player move and bot/opponent response both arrive between updates, so loadFen animated both piece changes simultaneously.

Instead of diffing FENs, the server now returns the individual new moves and the client animates them sequentially via registerOpponentMove. The updates are delivered over a WebSocket session (managed in LobbyService) rather than HTTP polling.

Server

  • LatestGamesUpdateRequest: added moveIndexes: Map<String, Int> (built server-side per session; shared across PvP and PvB)
  • LatestGamesUpdateResponse.Entry: added moveIndex: Int? and newMoves: List<String>
  • PlayerVsPlayerGameDaoService.fetchCurrentStatusAndFen: now also selects CURRENT_HALF_MOVE_INDEX
  • PlayerVsPlayerGameDaoService: added fetchNewMovesForGamesAndIndexes mirroring the PvB equivalent
  • PlayerVsBotGameDaoService.fetchCurrentStatusAndFen: now also selects CURRENT_HALF_MOVE_INDEX
  • GameDataService.fetchLatestGamesUpdate: calls fetchNewMovesForGamesAndIndexes for both PvP and PvB when moveIndexes is non-empty; populates newMoves/moveIndex in all entries
  • LiveGamesSubscription: new WebSocket input DTO — the client only declares which game ids it watches; the last move index per game is tracked server-side
  • LiveGamesBatchUpdate: batched update handed to every session, carrying the shared LatestGamesUpdateResponse plus the per-game minimum move index used for the fetch
  • LiveGamesWebSocketSession: tracks per-game move index and last status, and from the batched update slices out only the games it watches and the moves it still needs (drop(trackedIndex - batchMinIndex)); pushes only entries that changed (new moves or status change)
  • LobbyService: now manages the live-games sessions with a 1s refresher job (mirroring PlayerVsBotGameService/PlayerVsPlayerGameService); each refresh collects the union of all watched game ids and the lowest tracked move index per game across sessions, then performs a single GameDataService.fetchLatestGamesUpdate for that batch instead of one fetch per session
  • ShutdownHandler: now injects LobbyService and calls lobbyService.cancel() during the shutdown sequence so the live-games refresher job is stopped gracefully
  • Routing: added the ws/lobby/live-games WebSocket route (registered in ApiServiceRoutingModule); removed the HTTP POST /api/lobby/latest-games-update endpoint

Client

  • GameThumb: tracks currentMoveIndex; on refresh, enqueues newMoves and drains via #processMoveQueue() — each move animated with registerOpponentMove(move, false, cb), 300 ms between moves; falls back to loadFen on the first update (before move index is known)
  • LiveGamesWebSocketSession (js/ws/live-games.js): new client built on openReconnectingWebSocket; connects for everyone (including guests), sends the watched game ids on open/change, and dispatches pushed updates
  • LiveGamesViewer: subscribes over the WebSocket and applies pushed updates; reloads the latest games every 60 s to discover new ones and re-subscribes (skipping when mostly-live to avoid interrupting animations); lobby mini boards created with playSounds: false to suppress click sounds during background animations
  • LobbyClient: removed fetchLatestGamesUpdate (replaced by the WebSocket session)

Copilot AI changed the title [WIP] Animate individual moves for PVB games in live viewer Animate individual PvB moves in lobby live viewer May 28, 2026
Copilot finished work on behalf of benckx May 28, 2026 08:07
Copilot AI requested a review from benckx May 28, 2026 08:07

@benckx benckx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot We will do the same logic for PvP too, so fetch the latest moves instead of the latest FEN.

@benckx

benckx commented May 29, 2026

Copy link
Copy Markdown
Owner

@copilot I don't quite understand why we need pvbMoveIndexes if we keep track of the latest index per game already in the update.

Copilot AI commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

@copilot We will do the same logic for PvP too, so fetch the latest moves instead of the latest FEN.

Done in the latest commit. The same individual-move animation logic now applies to PvP:

  • PlayerVsPlayerGameDaoService: fetchCurrentStatusAndFen now also selects CURRENT_HALF_MOVE_INDEX; added fetchNewMovesForGamesAndIndexes mirroring the PvB equivalent
  • LatestGamesUpdateRequest: added pvpMoveIndexes: Map<String, Int>
  • GameDataService: fetches new PvP moves when pvpMoveIndexes is non-empty; PvP entries now include moveIndex and newMoves
  • lobby-client.js / live-games-viewer.js: collect and send pvpMoveIndexes alongside pvbMoveIndexes

GameThumb.refresh already handled newMoves generically, so no changes were needed there.

Copilot AI changed the title Animate individual PvB moves in lobby live viewer Animate individual PvB and PvP moves in lobby live viewer May 29, 2026
Copilot finished work on behalf of benckx May 29, 2026 14:46
Copilot AI requested a review from benckx May 29, 2026 14:46

Copilot AI commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

@copilot I don't quite understand why we need pvbMoveIndexes if we keep track of the latest index per game already in the update.

Good point — there was no reason to split by type. Replaced pvpMoveIndexes/pvbMoveIndexes with a single moveIndexes: Map<String, Int>. The server already knows each game's type from the gameIds list, so it can route correctly without a separate map per type. Done in the latest commit.

Copilot finished work on behalf of benckx May 29, 2026 14:53

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot not your change but rename gameID into gameId

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — renamed gameID to gameId in the groupBy lambda (commit Rename gameID to gameId in GameDataService).

let liveGames = 0;

this.#client.fetchLatestGamesUpdate(gameIdsToUpdate, (updates) => {
const moveIndexes = {};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Line 136-139: can it be done in a single assign? can it be inlined with var creation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — collapsed to a single Object.fromEntries assignment inline with the const declaration.

@benckx

benckx commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Looks fairly correct overall. I'm wondering if we should move this to WS though.

Copilot AI commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot While we are at it, we will move this whole thing to a WebSocket session. We'll add the session in LobbyService (check in PlayerVsPlayerService how sessions are managed).

Done — moved the lobby live games update to a WebSocket session (commit Move lobby live games update from HTTP polling to WebSocket session).

Server:

  • LobbyService now manages the sessions (mirroring how PlayerVsPlayerGameService/PlayerVsBotGameService do it): a 1s refresher job pushes per-session updates, reusing GameDataService.fetchLatestGamesUpdate.
  • New LiveGamesWebSocketSession tracks the last move index + status per game server-side, so the client no longer sends move indexes — it only declares which games it watches via the new LiveGamesSubscription input. The session only pushes entries that actually changed (new moves or status change).
  • New route ws/lobby/live-games; the HTTP POST /api/lobby/latest-games-update endpoint was removed.

Client:

  • New js/ws/live-games.js (LiveGamesWebSocketSession) built on openReconnectingWebSocket.
  • live-games-viewer.js subscribes over the socket and applies pushed updates; it still reloads the latest games every 60s to discover new ones and re-subscribes. fetchLatestGamesUpdate was removed from lobby-client.js.

Copilot finished work on behalf of benckx June 2, 2026 07:04
action = { refreshLiveGamesSessions() }
)

fun cancel() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Update the ShutdownHandler (or something) to handle this then

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — ShutdownHandler now injects LobbyService and calls lobbyService.cancel() during the shutdown sequence (commit Cancel LobbyService refresher job during shutdown).

if (this.#totalRefresh % 60 === 0 && !mustSkipCompleteRefresh) {
this.#loadLatestPvpGames();
this.#loadLatestPvbGames();
this.#wsSession.subscribe(gameIds);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot does this create a new WS session?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. subscribe() reuses the single WebSocket opened once in the LiveGamesWebSocketSession constructor — it just stores the new game ids and sends an updated subscription message over the existing socket (#sendSubscriptionsocket.send(...)). The viewer creates only one LiveGamesWebSocketSession, so re-subscribing never opens a new connection.

@benckx
benckx marked this pull request as ready for review June 2, 2026 09:32
// information is fetched once per session instead of being batched across sessions
liveGamesSessions.forEach { session ->
val request = session.currentRequest()
if (request.gameIds.isNotEmpty()) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot collect all gameIds you need, min index and fetching all the missing moves, send the missing moves to sessions that should get them

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (commit Batch live games fetch across lobby sessions). refreshLiveGamesSessions now collects the union of all watched game ids and, per game, the lowest tracked move index across sessions, then does a single fetchLatestGamesUpdate for that batch. Each session receives the same batched response and slices out only the moves it still needs (drop(trackedIndex - batchMinIndex)), so the DB work is no longer duplicated per session.

}

// remove the sessions that are not active anymore
liveGamesSessions.removeIf { session ->

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this should be at the start of the process

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (commit Remove closed live games sessions at start of refresh). The closed-session cleanup now runs at the very start of refreshLiveGamesSessions, before collecting game ids and fetching, so no work is done for sessions that are already gone.

Copilot finished work on behalf of benckx June 3, 2026 11:19
@benckx

benckx commented Jun 7, 2026

Copy link
Copy Markdown
Owner

@copilot I see timestamp getting updated in the lobby but I don't see the moves getting animated and also a lot of errors:

api.js:465 [games-to-play] WebSocket connected
api.js:465 [live-games] WebSocket connected
xiangqi.js:863 Uncaught TypeError: Cannot read properties of undefined (reading 'x')
    at Board.getPieceAt (xiangqi.js:863:39)
    at Board.registerMove (xiangqi.js:971:26)
    at #animateMoveViaDiff (board-gui.js:706:21)
    at BoardGui.registerOpponentMove (board-gui.js:474:33)
    at #processMoveQueue (game-thumb.js:417:27)
    at GameThumb.refresh (game-thumb.js:388:39)
    at live-games-viewer.js:140:23
    at Array.forEach (<anonymous>)
    at #applyUpdates (live-games-viewer.js:137:17)
    at LiveGamesWebSocketSession.<anonymous> (live-games-viewer.js:48:88)
lobby.js:387 Unrecognized feature: 'web-share'.
#renderYouTubeEmbed @ lobby.js:387
content.js:2 [maze-utils/fetcher] attaching: init message sent
www.youtube.com/youtubei/v1/log_event?alt=json:1  Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
www.youtube.com/youtubei/v1/log_event?alt=json:1  Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
content.js:2 [maze-utils/fetcher] no response after 500ms, triggering election
content.js:2 [maze-utils/fetcher] election status, winning: true
www.youtube.com/youtubei/v1/log_event?alt=json:1  Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
www.youtube.com/youtubei/v1/log_event?alt=json:1  Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
www.youtube.com/generate_204?Y6cvhw:1  Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
content.js:2 [maze-utils/fetcher] we won the election, broadcasting result
www.youtube.com/youtubei/v1/log_event?alt=json:1  Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
nApZihrdQGo:1 Uncaught (in promise) TIMEOUT waiting for ()=>(t=document.getElementById("player-control-container"),null!==t): Error
    at chrome-extension://mnjggcdmjocbbbhaepdhchncahnbgone/js/content.js:2:162712
content.js:2 Uncaught (in promise) TIMEOUT waiting for ()=>(0,c.getYouTubeTitleNode)(): Error
    at chrome-extension://mnjggcdmjocbbbhaepdhchncahnbgone/js/content.js:2:162712
www.youtube.com/youtubei/v1/log_event?alt=json:1  Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
www.youtube.com/youtubei/v1/log_event?alt=json:1  Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
xiangqi.js:863 Uncaught TypeError: Cannot read properties of undefined (reading 'x')
    at Board.getPieceAt (xiangqi.js:863:39)
    at Board.registerMove (xiangqi.js:971:26)
    at #animateMoveViaDiff (board-gui.js:706:21)
    at BoardGui.registerOpponentMove (board-gui.js:474:33)
    at #processMoveQueue (game-thumb.js:417:27)
    at GameThumb.refresh (game-thumb.js:388:39)
    at live-games-viewer.js:140:23
    at Array.forEach (<anonymous>)
    at #applyUpdates (live-games-viewer.js:137:17)
    at LiveGamesWebSocketSession.<anonymous> (live-games-viewer.js:48:88)
www.youtube.com/youtubei/v1/log_event?alt=json:1  Failed to load resource: net::ERR_BLOCKED_BY_CLIENT

Copilot AI commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot I see timestamp getting updated in the lobby but I don't see the moves getting animated and also a lot of errors:

...

The error came from passing raw UCI strings to registerOpponentMove, which expects HalfMove objects — move.from was undefined, so getPieceAt crashed and no animation ran. GameThumb.refresh now parses each newMoves entry with HalfMove.parseUci(uci) before queuing (matching BotGameSpectatorUpdateDto). Fixed in the latest commit.

Copilot finished work on behalf of benckx June 7, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

in the live viewer, PVB games always update 2 moves at once which looks a bit funny, maybe we should animate individual moves

2 participants