Add spectator mode for late-joining players

- Server: Late joiners are added as spectators instead of players
- Server: Send forced-spectator event only to joining spectator (not broadcast)
- Server: Track spectators separately and move them to players after game ends
- Client: Handle forced-spectator event to show all player boards
- Client: Spectators see all boards equally without main/spectator highlighting
- Client: Mobile view shows scrollable vertical list of all boards for spectators
- Fix: All cleared lines are sent as garbage to each opponent (not randomized)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 18:26:48 +00:00
parent 75cd5b0e44
commit a0ab4ff5cd
7 changed files with 281 additions and 21 deletions
+43 -2
View File
@@ -83,10 +83,45 @@ function setupNetworkListeners() {
network.setListener('game-over', (data) => {
endGame(data.states);
});
// Forced spectator - game already in progress
network.setListener('forced-spectator', ({ states, players }) => {
ui.showScreen('game');
ui.showSpectatorMode();
// Clear old boards
renderer.clearAll();
// Create boards for all players (spectator can see everyone)
states.forEach((state) => {
const player = network.getPlayer(state.playerId);
renderer.createPlayerBoard(state.playerId, player.name);
});
// Set up battle grid layout
updateBattleGridLayout(players.length);
// Start game loop for viewing
lastTime = performance.now();
requestAnimationFrame(gameLoop);
});
// Spectator joined
network.setListener('spectator-joined', (spectator) => {
ui.updateSpectatorList(network.getAllSpectators());
});
// Spectator left
network.setListener('spectator-left', (spectatorId) => {
ui.updateSpectatorList(network.getAllSpectators());
});
}
function setupKeyboardControls() {
document.addEventListener('keydown', (e) => {
// Spectators cannot control the game
if (network.isSpectator) return;
if (ui.screens.game.classList.contains('active') && localGame) {
switch (e.key) {
case 'ArrowLeft':
@@ -129,6 +164,8 @@ function setupKeyboardControls() {
const handleTouch = (e, action) => {
e.preventDefault();
e.stopPropagation();
// Spectators cannot control the game
if (network.isSpectator) return;
if (ui.screens.game.classList.contains('active')) {
action();
}
@@ -185,6 +222,10 @@ function endGame(states) {
}
});
// Reset spectator mode for next round
network.isSpectator = false;
ui.hideSpectatorMode();
ui.showGameOver(winner, scores);
}
@@ -207,8 +248,8 @@ function gameLoop(currentTime) {
renderer.renderPlayer(state.playerId, state);
});
// Set active player highlight
renderer.setActivePlayer(network.currentPlayerId);
// Set active player highlight (or null for spectators to show all boards equally)
renderer.setActivePlayer(network.isSpectator ? null : network.currentPlayerId);
requestAnimationFrame(gameLoop);
}