import InfoBox from '../components/InfoBox.astro';
import PullQuote from '../components/PullQuote.astro';

When a 1v1 real-time word match concluded in [pvp-duel](/posts/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.

## Token-chained authorization without new links

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.

```typescript
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.

<InfoBox variant="note" title="Presence without database writes">
Player presence is tracked live in memory via a role-aware event bus rather than persisted in database tables, ensuring that disconnection cleanups fire only when a player's final active connection drops.
</InfoBox>

## 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.

<PullQuote attribution="Endgame UI design">
Keeping action button positions locked in place across every rematch state eliminates layout jitter during rapid post-game taps.
</PullQuote>

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.

```typescript
// 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.