Skip to content

Online implementation#169

Merged
losadgm merged 25 commits intomasterfrom
online
Apr 26, 2026
Merged

Online implementation#169
losadgm merged 25 commits intomasterfrom
online

Conversation

@Clubserg
Copy link
Copy Markdown
Collaborator

♟️ Online Multiplayer (pvp-online)

This PR implements real-time online multiplayer for Game Y, including matchmaking, WebSocket communication, game history for both players, and connection resilience.


✨ Features

Matchmaking & Lobby

  • Online lobby page with live queue status and player count
  • Automatic pairing of the two longest-waiting players
  • Match confirmation screen showing the opponent's name before redirecting to the game

Real-time gameplay via WebSocket

  • Move relay between both players through the WebSocket server
  • Optimistic board updates on the sender's side for instant feedback
  • Surrender support over WebSocket
  • join_game / leave_game messages so the server knows which room each client is in

Disconnection & Reconnection

  • 30-second grace period when a player drops — opponent sees a banner instead of an instant forfeit
  • Automatic WS reconnect with exponential backoff (up to 5 attempts)
  • On reconnect, join_game is re-sent automatically so the server can resume relaying moves
  • leave_game is only sent on intentional navigation away, not on page reload
  • Lobby cleanup correctly distinguishes between leaving the queue and being matched

Game History

  • GET /api/games now returns games for both player1Id and player2Id, merged and sorted by updatedAt DESC
  • Both players can access the full move-by-move replay after a match ends

Stats & Ranking

  • Match results are recorded for both players via the internal stats endpoint at game end
  • pvp-online mode feeds into the ranking leaderboard

🔧 Backend changes

File Change
WebSocketManager.ts Message routing: join_queue, leave_queue, join_game, leave_game, move, surrender; disconnect grace period with auto-forfeit
MatchmakingService.ts In-memory FIFO queue; join, leave, tryMatch, contains, size, clear
GameService.getUserGames Queries both player1Id and player2Id, deduplicates, sorts, trims to 50
GameService.setPlayer2Id Persists player 2 identity after matchmaking
GameController.getGames Requires auth; returns GameSummary[] without board/moves/timer
gameRoutes Added GET /api/games (authenticated)

🖥️ Frontend changes

File Change
websocketService.ts Auto-reconnect with backoff; emits internal reconnected event; disconnect() cancels reconnect
useOnlineLobbyController.ts Manages connecting → queuing → matched flow; skips leave_queue on unmount when already matched
useGameYController.ts WS listeners for game_update, opponent_disconnected, opponent_reconnected; re-sends join_game on WS reconnect; handleGoHome sends leave_game explicitly
GameYPage.tsx Passes handleGoHome to overlay; opponent disconnected banner
OnlineLobbyPage.tsx Renders connecting / queuing / matched / error states
GameHistoryPage.tsx Replay button per game; guest upsell
useGameHistoryController.ts Fetches GET /api/games with auth token
gameyService.ts getUserGames(token) calls the list endpoint

Comment thread webapp/src/i18n/es.ts Fixed
@losadgm
Copy link
Copy Markdown
Contributor

losadgm commented Apr 26, 2026

Changes added on top of this PR

Private rooms

Added a room-based flow as an alternative to the automatic matchmaking queue. Players can now create a private game and share a 6-character code with a specific opponent instead of being paired randomly.

Backend (game)

  • WebSocketManager: added RoomEntry interface and a rooms: Map<string, RoomEntry> store; generateRoomCode() produces a 6-char alphanumeric code (unambiguous charset); handleCreateRoom() registers the room and sends room_created { code } back to the host; handleJoinRoom() looks up the room, creates the game, sends matched to both players, and removes the room entry. Host disconnect cleans up any open room.
  • game/src/websocket/types.ts: added create_room and join_room to ClientMessage; room_created to ServerMessage; playerColor?: 'player1' | 'player2' field to OnlineGameConfig.

Frontend (webapp)

  • OnlineRoomPage (/games/y/online): landing page for online play; two cards — "Create Room" (goes to config) and "Join Room" (code input). Non-idle states use the same centered layout as OnlineLobbyPage.
  • OnlineHostLobbyPage (/games/y/online/host): shown after config is submitted; connects the WebSocket, sends create_room, displays the room code in large monospace font with a copy-to-clipboard button, and navigates to the game on matched.
  • useOnlineRoomController: manages idle / connecting / waiting / matched / error states for the join flow.
  • useOnlineHostLobbyController: manages the host flow (connecting → waiting → matched).
  • onlineConfig.ts (webapp/src/utils/): extracted loadOnlineConfig() from useOnlineLobbyController into a shared utility so all three online controllers (lobby, host, room) read config the same way.
  • Navigation wiring: useGameModeController now routes pvp-online to /games/y/online; useGameConfigController routes the config submit to /games/y/online/host; routes added in App.tsx.

Host color selection

The room creator can now choose whether to play as player 1 (first move) or player 2 in the game config screen. When cfg.playerColor === 'player2', handleJoinRoom swaps the player assignments so the joiner becomes player 1 and the host becomes player 2. The matched messages sent to each client reflect the actual colors.

The "Your color" selector in GameConfigPage is now shown for both pve and pvp-online modes (previously pve-only). The submit button label is "Create Room" for pvp-online.

Game history result fix

GameHistoryPage was showing the wrong win/loss result for the non-host player in online games. Root cause: user.id arrives as a JavaScript number at runtime despite the TypeScript declaration being string, causing "5" === 5 to always be false. Fixed by coercing both sides: String(game.players.player1.id) === String(currentUserId). The same pattern was already used in useGameYController. Helpers humanColor(), resultForGame(), and opponentName() were updated accordingly and currentUserId is now passed down to the GameRow component.

i18n

Added online.* keys (title, subtitle, createRoom, createRoomDescription, joinRoom, joinRoomDescription, roomCode, roomCodePlaceholder, join, joiningRoom, waitingForOpponent, waitingForOpponentHint, copyCode, codeCopied, cancelRoom, guestCannotJoin) and gameConfig.createRoom to both en.ts and es.ts.

Production nginx

Resolved a merge conflict in webapp/nginx/nginx.prod.conf (lines 59-66). Kept the $cors_origin map variable approach from master, which correctly handles both micrati.com and www.micrati.com rather than hardcoding a single origin.

@Clubserg
Copy link
Copy Markdown
Collaborator Author

Quality Gate Failed Quality Gate failed

Failed conditions 80.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

que nadie se ria

losadgm
losadgm previously approved these changes Apr 26, 2026
Copy link
Copy Markdown
Contributor

@losadgm losadgm left a comment

Choose a reason for hiding this comment

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

LGTM

@sonarqubecloud
Copy link
Copy Markdown

@losadgm losadgm merged commit 0efa29a into master Apr 26, 2026
3 checks passed
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.

3 participants