Rematching with the same opponent in real-time PvP

Starting another match against the same opponent previously required sharing a new room link. Token-chained authorization and a six-state UI matrix bring instant same-opponent rematches.

When a 1v1 real-time word match concluded in pvp-duel, playing another round required closing the game, generating a new invite link or QR code, and sending it to the same player over an external chat channel. This post-game friction interrupted the competitive flow between friends. Bringing instant same-opponent rematches directly into the endgame card required extending server-side game state, establishing live presence detection, and handling UI transitions cleanly.

Instead of requiring players to generate a new room link, the rematch system authorizes a fresh match room by chaining the session tokens of the completed game. Three state fields were added to the primary match database record to track which player requested a rematch, whether a request was declined, and the identifier of the new match room once created.

export interface PvpRematchState {
  requestedBy: 'host' | 'guest' | null;
  declined: boolean;
  newGameId: string | null;
  opponentPresent: boolean;
}

When both players tap rematch, the server creates a new game record using atomic compare-and-set guards, linking the previous session’s tokens to the host and guest slots of the new match automatically.

Zero-layout-shift UI matrix#

The endgame recap card supports a six-state interaction matrix covering idle, request pending, opponent requested, both accepted, opponent declined, and opponent departed.

Keeping action button positions locked in place across every rematch state eliminates layout jitter during rapid post-game taps.

Endgame UI design

To prevent UI elements from jumping as states transition, the action button row remains structurally fixed in place. Labels and disabled states update inline, while status explanations render inside an absolutely positioned callout box above the controls.

Catching connection cleanup edge cases#

An automated code review pass caught a subtle event bus bug during development. The initial Server-Sent Events (SSE) disconnect handler was clearing pending rematch requests on any connection teardown. In practice, routine network hiccups or browser tab switching cause transient SSE auto-reconnects, which erroneously cancelled active rematch requests.

// Ensure cleanup fires only when all active connections for a role drop
if (eventBus.getRoleConnectionCount(gameId, role) === 0) {
  await pvpService.clearRematchOnDeparture(gameId, role);
}

Restricting departure cleanup to fire only when a role’s connection count hits zero ensures that transient network reconnects do not disrupt pending rematch offers.

With same-opponent rematches in place, players can flow seamlessly into consecutive duels without leaving the game screen.