const express = require('express') const http = require('http') const { Server } = require('socket.io') const cors = require('cors') const { RoomManager } = require('./game/RoomManager') const app = express() app.use(cors()) app.use(express.json()) // Route de santé minimale : utilisée notamment par le healthcheck HTTP de // Playwright (config e2e/playwright.config.js, webServer.url) pour détecter // qu'un serveur est déjà démarré et éviter un conflit de port (EADDRINUSE) // en local. Sans route HTTP définie, toute requête renvoyait 404, que // Playwright ne considère pas comme "serveur prêt". app.get('/health', (req, res) => res.status(200).json({ status: 'ok' })) const server = http.createServer(app) const io = new Server(server, { cors: { origin: '*', methods: ['GET', 'POST'] } }) const rooms = new RoomManager() io.on('connection', (socket) => { console.log(`[+] ${socket.id}`) // ─── Rejoindre une salle ───────────────────────────────────────────────────── socket.on('join-room', ({ roomId, playerName }) => { const room = rooms.getOrCreate(roomId) const joined = room.addPlayer(socket.id, playerName) if (!joined) { socket.emit('error', { message: 'Salle pleine (4 joueurs max)' }) return } socket.join(roomId) socket.data.roomId = roomId socket.data.playerName = playerName io.to(roomId).emit('room-update', { players: room.getPlayersInfo() }) console.log(`[${roomId}] ${playerName} rejoint (${room.playerCount}/4)`) }) // ─── Démarrer la partie ────────────────────────────────────────────────────── socket.on('start-game', ({ roomId }) => { const room = rooms.get(roomId) if (!room || room.playerCount < 2) return room.startGame() io.to(roomId).emit('game-start') // Envoyer la main personnalisée à chaque joueur room.players.forEach(player => { io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId)) }) console.log(`[${roomId}] Partie démarrée`) }) // ─── Demander l'état courant ───────────────────────────────────────────────── socket.on('request-state', ({ roomId }) => { const room = rooms.get(roomId) if (!room) return socket.emit('game-state', room.getStateFor(socket.id)) }) // ─── Acheter un objet ──────────────────────────────────────────────────────── socket.on('buy-item', ({ roomId, itemId }) => { const room = rooms.get(roomId) if (!room) return const result = room.buyItem(socket.id, itemId) if (!result.ok) { socket.emit('error', { message: result.error }) return } // L'achat est différé (résolu avec le tour) : on renvoie juste l'état à // jour à chaque joueur pour refléter la tentative en attente. room.players.forEach(player => { io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId)) }) }) // ─── Acheter un bâtiment ───────────────────────────────────────────────────── socket.on('buy-building', ({ roomId, buildingId }) => { const room = rooms.get(roomId) if (!room) return const result = room.buyBuilding(socket.id, buildingId) if (!result.ok) { socket.emit('error', { message: result.error }) return } room.players.forEach(player => { io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId)) }) }) // ─── Piocher une carte Événement ───────────────────────────────────────────── socket.on('buy-event-draw', ({ roomId }) => { const room = rooms.get(roomId) if (!room) return const result = room.buyEventDraw(socket.id) if (!result.ok) { socket.emit('error', { message: result.error }) return } // Contrairement aux objets/bâtiments, la pioche est immédiate (pas de // conflit de stock possible) : chaque joueur reçoit son état à jour tout // de suite (seul le joueur ayant pioché voit la nouvelle carte dans // myEventHand, les autres voient juste ses jauges diminuer). room.players.forEach(player => { io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId)) }) }) // ─── Choisir de jouer une carte Événement ce tour ──────────────────────────── socket.on('play-event', ({ roomId, eventId, targets }) => { const room = rooms.get(roomId) if (!room) return const result = room.playEventCard(socket.id, eventId, targets) if (!result.ok) { socket.emit('error', { message: result.error }) return } room.players.forEach(player => { io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId)) }) }) // ─── Jouer une carte ───────────────────────────────────────────────────────── socket.on('play-card', ({ roomId, cardId }) => { const room = rooms.get(roomId) if (!room) return const result = room.playCard(socket.id, cardId) if (!result.ok) { socket.emit('error', { message: result.error }) return } // Broadcaster le compteur de joueurs prêts à tous io.to(roomId).emit('players-ready', { ready: room.pendingPlays.size, total: room.playerCount }) // Envoyer à chaque joueur son état personnalisé : // - celui qui vient de jouer reçoit phase='waiting' // - les autres restent en phase='selecting' room.players.forEach(player => { io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId)) }) // Tous les joueurs ont joué → résolution automatique if (room.allPlayersPlayed()) { const roundResult = room.resolveRound() console.log(`[${roomId}] Tour ${room.currentTurn} résolu`) // Révéler les cartes jouées, les gains par attribut, les objets/bâtiments // achetés, et les résultats des cartes Événement jouées ce tour. io.to(roomId).emit('round-result', { playedCards: roundResult.playedCards, gains: roundResult.gains, boughtItems: roundResult.boughtItems, boughtBuildings: roundResult.boughtBuildings, eventResults: roundResult.eventResults, message: roundResult.message }) // Mise à jour scores (état personnalisé par joueur : items, achat en // attente, objets encore disponibles, etc.) room.players.forEach(player => { io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId)) }) // 3 secondes d'affichage puis tour suivant / fin setTimeout(() => { room.nextTurn() if (room.isGameOver()) { io.to(roomId).emit('game-over', room.getPlayersInfo()) console.log(`[${roomId}] Partie terminée`) } else { // Distribuer la nouvelle main à chaque joueur room.players.forEach(player => { io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId)) }) } }, 3500) } }) // ─── Quitter une salle explicitement (bouton retour) ──────────────────────── socket.on('leave-room', ({ roomId }) => { const room = rooms.get(roomId) if (room) { room.removePlayer(socket.id) socket.leave(roomId) io.to(roomId).emit('room-update', { players: room.getPlayersInfo() }) if (room.playerCount === 0) rooms.delete(roomId) console.log(`[${roomId}] ${socket.data.playerName ?? socket.id} a quitté`) } socket.data.roomId = null }) // ─── Déconnexion ───────────────────────────────────────────────────────────── socket.on('disconnect', () => { const roomId = socket.data.roomId if (roomId) { const room = rooms.get(roomId) if (room) { room.removePlayer(socket.id) io.to(roomId).emit('room-update', { players: room.getPlayersInfo() }) if (room.playerCount === 0) rooms.delete(roomId) } } console.log(`[-] ${socket.id}`) }) }) const PORT = process.env.PORT || 3000 server.listen(PORT, () => { console.log(`Regnum server → http://localhost:${PORT}`) })