const { test, describe } = require('node:test') const assert = require('node:assert/strict') const { Room } = require('../../src/game/Room') /** Aide : crée une Room avec n joueurs déjà ajoutés et la partie démarrée. */ function makeStartedRoom(n) { const room = new Room('T') for (let i = 0; i < n; i++) { room.addPlayer(`s${i}`, `P${i}`) } room.startGame() return room } /** Aide : force la carte en tête de main d'un joueur à des valeurs contrôlées. */ function setHandCardValues(room, socketId, values) { const player = room.players.find(p => p.socketId === socketId) Object.assign(player.hand[0], values) return player.hand[0] } /** Aide : joue les cartes en tête de main de tous les joueurs, avec attributs * neutralisés (0 partout) pour isoler les effets d'événements des gains * de carte personnage normaux. */ function playNeutralCardsAndResolve(room) { room.players.forEach(p => { setHandCardValues(room, p.socketId, { food: 0, war: 0, health: 0, diplo: 0 }) }) room.players.forEach(p => room.playCard(p.socketId, p.hand[0].id)) return room.resolveRound() } /** Aide : force le deck d'événement à ne contenir qu'un seul id, en tête. */ function forceNextDraw(room, eventId) { room.eventDeck.unshift(eventId) } describe('Room - buyEventDraw', () => { test('accepte une pioche si les 4 jauges ont au moins 1 point', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 1, war: 1, health: 1, diplo: 1 } const result = room.buyEventDraw('s0') assert.equal(result.ok, true) assert.ok(result.eventId) }) test('débite 1 point de chacune des 4 jauges', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 5, war: 5, health: 5, diplo: 5 } room.buyEventDraw('s0') assert.deepEqual(room.players[0].gauges, { food: 4, war: 4, health: 4, diplo: 4 }) }) test('ajoute la carte piochée à la main d\'événements du joueur', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 1, war: 1, health: 1, diplo: 1 } const result = room.buyEventDraw('s0') assert.deepEqual(room.players[0].eventHand, [result.eventId]) }) test('retire la carte piochée du deck partagé', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 1, war: 1, health: 1, diplo: 1 } const before = room.eventDeck.length room.buyEventDraw('s0') assert.equal(room.eventDeck.length, before - 1) }) test('refuse si une seule des 4 jauges est insuffisante', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 1, war: 1, health: 0, diplo: 1 } const result = room.buyEventDraw('s0') assert.equal(result.ok, false) }) test('refuse hors phase selecting', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 1, war: 1, health: 1, diplo: 1 } room.phase = 'resolve' const result = room.buyEventDraw('s0') assert.equal(result.ok, false) }) test('refuse une seconde tentative de pioche le même tour', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 5, war: 5, health: 5, diplo: 5 } room.buyEventDraw('s0') const second = room.buyEventDraw('s0') assert.equal(second.ok, false) assert.match(second.error, /déjà tenté/) }) test('refuse si la main d\'événements est déjà pleine (max 2)', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 10, war: 10, health: 10, diplo: 10 } room.players[0].eventHand = ['bountiful_harvest', 'epidemic'] // déjà 2 const result = room.buyEventDraw('s0') assert.equal(result.ok, false) assert.match(result.error, /pleine/) }) test('refuse si le deck partagé est épuisé', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 1, war: 1, health: 1, diplo: 1 } room.eventDeck = [] const result = room.buyEventDraw('s0') assert.equal(result.ok, false) assert.match(result.error, /épuisé/) }) test('refuse un joueur inconnu', () => { const room = makeStartedRoom(2) const result = room.buyEventDraw('ghost') assert.equal(result.ok, false) }) test('le deck est PARTAGÉ : une pioche par un joueur réduit ce que les autres peuvent piocher', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 1, war: 1, health: 1, diplo: 1 } room.players[1].gauges = { food: 1, war: 1, health: 1, diplo: 1 } const before = room.eventDeck.length room.buyEventDraw('s0') room.buyEventDraw('s1') assert.equal(room.eventDeck.length, before - 2) }) }) describe('Room - playEventCard', () => { test('accepte de jouer une carte présente dans la main d\'événements', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest'] const result = room.playEventCard('s0', 'bountiful_harvest') assert.equal(result.ok, true) }) test('refuse une carte absente de la main d\'événements', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['epidemic'] const result = room.playEventCard('s0', 'bountiful_harvest') assert.equal(result.ok, false) }) test('refuse une seconde carte événement jouée le même tour (max 1 par tour)', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest', 'epidemic'] room.playEventCard('s0', 'bountiful_harvest') const second = room.playEventCard('s0', 'epidemic') assert.equal(second.ok, false) assert.match(second.error, /déjà choisi/) }) test('refuse hors phase selecting', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest'] room.phase = 'resolve' const result = room.playEventCard('s0', 'bountiful_harvest') assert.equal(result.ok, false) }) test('refuse un joueur inconnu', () => { const room = makeStartedRoom(2) const result = room.playEventCard('ghost', 'bountiful_harvest') assert.equal(result.ok, false) }) }) describe('Room - résolution des événements : ordre et défausse', () => { test('les événements sont résolus AVANT le calcul des gains de cartes personnage', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest'] room.playEventCard('s0', 'bountiful_harvest') // gain de base de carte = 0 (cartes neutralisées), donc si food augmente // c'est uniquement grâce à l'événement, avant même le calcul des gains. const result = playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.food, 2) assert.equal(result.gains[0].food, 0) // le gain de carte lui-même reste 0 }) test('la carte jouée est retirée de la main d\'événements (toujours défaussée)', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest'] room.playEventCard('s0', 'bountiful_harvest') playNeutralCardsAndResolve(room) assert.deepEqual(room.players[0].eventHand, []) }) test('pendingEventPlays est vidé après résolution', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest'] room.playEventCard('s0', 'bountiful_harvest') playNeutralCardsAndResolve(room) assert.equal(room.pendingEventPlays.size, 0) }) test('un tour sans carte événement jouée ne modifie rien (eventResults vide)', () => { const room = makeStartedRoom(2) const result = playNeutralCardsAndResolve(room) assert.deepEqual(result.eventResults, []) }) }) describe('Room - effet: Récolte exceptionnelle', () => { test('gagne 2 Nourriture sans la Charrue', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest'] room.playEventCard('s0', 'bountiful_harvest') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.food, 2) }) test('gagne 3 Nourriture (2+1) avec la Charrue', () => { const room = makeStartedRoom(2) room.players[0].items.push('plow') room.players[0].eventHand = ['bountiful_harvest'] room.playEventCard('s0', 'bountiful_harvest') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.food, 3) }) test('n\'affecte pas les jauges des autres joueurs', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest'] room.playEventCard('s0', 'bountiful_harvest') playNeutralCardsAndResolve(room) assert.equal(room.players[1].gauges.food, 0) }) }) describe('Room - effet: Épidémie', () => { test('effet de base : TOUS les joueurs (y compris le lanceur) perdent 1 Santé', () => { const room = makeStartedRoom(3) room.players.forEach(p => { p.gauges.health = 5 }) room.players[0].eventHand = ['epidemic'] room.playEventCard('s0', 'epidemic') playNeutralCardsAndResolve(room) room.players.forEach(p => assert.equal(p.gauges.health, 4)) }) test('variante Trousse : un adversaire choisi perd 2 Santé, les autres ne perdent rien', () => { const room = makeStartedRoom(3) room.players.forEach(p => { p.gauges.health = 5 }) room.players[0].items.push('medkit') room.players[0].eventHand = ['epidemic'] room.playEventCard('s0', 'epidemic', { targetTeamIndex: 1 }) playNeutralCardsAndResolve(room) // Le lanceur n'est pas affecté par sa PROPRE variante Trousse de l'Épidémie, // mais possède la Trousse : son effet permanent (guaranteed_gain +2 Santé, // indépendant de l'événement) s'applique quand même normalement ce tour. assert.equal(room.players[0].gauges.health, 7) // 5 + 2 (effet permanent de la Trousse) assert.equal(room.players[1].gauges.health, 3) // cible : -2 assert.equal(room.players[2].gauges.health, 5) // non ciblé : inchangé }) test('variante Église : TOUS les adversaires perdent 2 Santé, le lanceur n\'est pas affecté', () => { const room = makeStartedRoom(3) room.players.forEach(p => { p.gauges.health = 5 }) room.players[0].buildings.push('church') room.players[0].eventHand = ['epidemic'] room.playEventCard('s0', 'epidemic') playNeutralCardsAndResolve(room) // Le lanceur possède l'Église : son effet permanent (+1 Santé garanti, // indépendant de l'événement) s'applique aussi normalement ce tour. assert.equal(room.players[0].gauges.health, 6) // 5 + 1 (effet permanent de l'Église) assert.equal(room.players[1].gauges.health, 3) assert.equal(room.players[2].gauges.health, 3) }) test('la variante Église est prioritaire sur la variante Trousse si les deux sont possédées', () => { const room = makeStartedRoom(3) room.players.forEach(p => { p.gauges.health = 5 }) room.players[0].items.push('medkit') room.players[0].buildings.push('church') room.players[0].eventHand = ['epidemic'] room.playEventCard('s0', 'epidemic', { targetTeamIndex: 1 }) playNeutralCardsAndResolve(room) // Église s'applique : tous les adversaires -2, pas seulement s1 assert.equal(room.players[1].gauges.health, 3) assert.equal(room.players[2].gauges.health, 3) }) test('la Santé ne devient jamais négative (clampée à 0)', () => { const room = makeStartedRoom(2) room.players.forEach(p => { p.gauges.health = 0 }) room.players[0].eventHand = ['epidemic'] room.playEventCard('s0', 'epidemic') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.health, 0) assert.equal(room.players[1].gauges.health, 0) }) test('Complot découvert protège son porteur de l\'effet de base d\'Épidémie jouée par un autre', () => { const room = makeStartedRoom(2) room.players.forEach(p => { p.gauges.health = 5 }) room.players[0].eventHand = ['epidemic'] room.players[1].eventHand = ['plot_uncovered'] room.playEventCard('s0', 'epidemic') room.playEventCard('s1', 'plot_uncovered') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.health, 4) // lanceur d'Épidémie, non protégé assert.equal(room.players[1].gauges.health, 5) // protégé par son propre Complot découvert }) }) describe('Room - effet: Levée des troupes', () => { test('gagne 2 Guerre et perd 1 Diplomatie sans le Trébuchet', () => { const room = makeStartedRoom(2) room.players[0].gauges.diplo = 5 room.players[0].eventHand = ['troop_levy'] room.playEventCard('s0', 'troop_levy') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.war, 2) assert.equal(room.players[0].gauges.diplo, 4) }) test('gagne 4 Guerre (2+2) avec le Trébuchet, perd toujours 1 Diplomatie', () => { const room = makeStartedRoom(2) room.players[0].items.push('trebuchet') room.players[0].gauges.diplo = 5 room.players[0].eventHand = ['troop_levy'] room.playEventCard('s0', 'troop_levy') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.war, 4) assert.equal(room.players[0].gauges.diplo, 4) }) test('la Diplomatie ne devient jamais négative (clampée à 0)', () => { const room = makeStartedRoom(2) room.players[0].gauges.diplo = 0 room.players[0].eventHand = ['troop_levy'] room.playEventCard('s0', 'troop_levy') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.diplo, 0) }) }) describe('Room - effet: Accord royal', () => { test('gagne 1 Diplomatie sans le Sceau Royal', () => { const room = makeStartedRoom(3) room.players[0].eventHand = ['royal_accord'] room.playEventCard('s0', 'royal_accord') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.diplo, 1) }) test('gagne 2 Diplomatie (1+1) avec le Sceau Royal', () => { const room = makeStartedRoom(3) room.players[0].items.push('royal_seal') room.players[0].eventHand = ['royal_accord'] room.playEventCard('s0', 'royal_accord') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.diplo, 2) }) test('le joueur ciblé en gain reçoit +1 Diplomatie', () => { const room = makeStartedRoom(3) room.players[0].eventHand = ['royal_accord'] room.playEventCard('s0', 'royal_accord', { gainTargetTeamIndex: 1 }) playNeutralCardsAndResolve(room) assert.equal(room.players[1].gauges.diplo, 1) }) test('le joueur ciblé en perte reçoit -1 Diplomatie', () => { const room = makeStartedRoom(3) room.players[2].gauges.diplo = 3 room.players[0].eventHand = ['royal_accord'] room.playEventCard('s0', 'royal_accord', { loseTargetTeamIndex: 2 }) playNeutralCardsAndResolve(room) assert.equal(room.players[2].gauges.diplo, 2) }) test('les deux cibles (gain/perte) peuvent être différentes simultanément', () => { const room = makeStartedRoom(3) room.players[2].gauges.diplo = 3 room.players[0].eventHand = ['royal_accord'] room.playEventCard('s0', 'royal_accord', { gainTargetTeamIndex: 1, loseTargetTeamIndex: 2 }) playNeutralCardsAndResolve(room) assert.equal(room.players[1].gauges.diplo, 1) assert.equal(room.players[2].gauges.diplo, 2) }) test('un joueur ne peut pas se cibler lui-même (targets ignorées si == lanceur)', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['royal_accord'] room.playEventCard('s0', 'royal_accord', { gainTargetTeamIndex: 0, loseTargetTeamIndex: 0 }) playNeutralCardsAndResolve(room) // Seul le +1 de base doit s'appliquer, pas de double comptage via l'auto-ciblage assert.equal(room.players[0].gauges.diplo, 1) }) test('Complot découvert protège son porteur de la perte de Diplomatie ciblée', () => { const room = makeStartedRoom(3) room.players[1].gauges.diplo = 3 room.players[0].eventHand = ['royal_accord'] room.players[1].eventHand = ['plot_uncovered'] room.playEventCard('s0', 'royal_accord', { loseTargetTeamIndex: 1 }) room.playEventCard('s1', 'plot_uncovered') playNeutralCardsAndResolve(room) assert.equal(room.players[1].gauges.diplo, 3) // protégé, pas de -1 }) }) describe('Room - effet: Complot découvert', () => { test('ne modifie aucune jauge par lui-même', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['plot_uncovered'] room.playEventCard('s0', 'plot_uncovered') const before = { ...room.players[0].gauges } playNeutralCardsAndResolve(room) assert.deepEqual(room.players[0].gauges, before) }) }) describe('Room - effet: Grande fête au château', () => { test('gagne 1 Nourriture et 1 Diplomatie sans le Château Royal', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['castle_feast'] room.playEventCard('s0', 'castle_feast') playNeutralCardsAndResolve(room) assert.equal(room.players[0].gauges.food, 1) assert.equal(room.players[0].gauges.diplo, 1) assert.equal(room.players[0].eventHand.length, 0) // pas de pioche bonus sans le Château }) test('pioche 1 carte événement supplémentaire avec le Château Royal', () => { const room = makeStartedRoom(2) room.players[0].buildings.push('castle') room.players[0].eventHand = ['castle_feast'] const deckBefore = room.eventDeck.length room.playEventCard('s0', 'castle_feast') playNeutralCardsAndResolve(room) assert.equal(room.players[0].eventHand.length, 1) // 1 carte piochée en bonus assert.equal(room.eventDeck.length, deckBefore - 1) }) test('ne pioche rien si le deck partagé est épuisé, même avec le Château', () => { const room = makeStartedRoom(2) room.players[0].buildings.push('castle') room.players[0].eventHand = ['castle_feast'] room.eventDeck = [] room.playEventCard('s0', 'castle_feast') playNeutralCardsAndResolve(room) assert.equal(room.players[0].eventHand.length, 0) }) }) describe('Room - eventResults (résumé retourné par resolveRound)', () => { test('contient une entrée par carte événement jouée, avec playerName et eventName', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest'] room.playEventCard('s0', 'bountiful_harvest') const result = playNeutralCardsAndResolve(room) assert.equal(result.eventResults.length, 1) assert.equal(result.eventResults[0].playerName, 'P0') assert.equal(result.eventResults[0].eventName, 'Récolte exceptionnelle') assert.ok(result.eventResults[0].summary.length > 0) }) test('contient une entrée par joueur si plusieurs jouent un événement le même tour', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['bountiful_harvest'] room.players[1].eventHand = ['castle_feast'] room.playEventCard('s0', 'bountiful_harvest') room.playEventCard('s1', 'castle_feast') const result = playNeutralCardsAndResolve(room) assert.equal(result.eventResults.length, 2) }) }) describe('Room - getStateFor : champs liés aux événements', () => { test('expose myEventHand propre à chaque joueur', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['epidemic'] assert.deepEqual(room.getStateFor('s0').myEventHand, ['epidemic']) assert.deepEqual(room.getStateFor('s1').myEventHand, []) }) test('expose eventDeckSize (taille du deck partagé)', () => { const room = makeStartedRoom(2) assert.equal(room.getStateFor('s0').eventDeckSize, 18) }) test('expose hasPendingEventDraw après une pioche ce tour', () => { const room = makeStartedRoom(2) room.players[0].gauges = { food: 1, war: 1, health: 1, diplo: 1 } room.buyEventDraw('s0') assert.equal(room.getStateFor('s0').hasPendingEventDraw, true) assert.equal(room.getStateFor('s1').hasPendingEventDraw, false) }) test('expose pendingEventPlayId après avoir choisi de jouer une carte', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['epidemic'] room.playEventCard('s0', 'epidemic') assert.equal(room.getStateFor('s0').pendingEventPlayId, 'epidemic') assert.equal(room.getStateFor('s1').pendingEventPlayId, null) }) }) describe('Room - removePlayer nettoie l\'état événements', () => { test('nettoie pendingEventDraws et pendingEventPlays du joueur qui part', () => { const room = makeStartedRoom(3) room.players[0].gauges = { food: 1, war: 1, health: 1, diplo: 1 } room.buyEventDraw('s0') room.players[1].eventHand = ['epidemic'] room.playEventCard('s1', 'epidemic') room.removePlayer('s0') assert.equal(room.pendingEventDraws.has('s0'), false) // s1 n'est pas parti, son event play doit rester intact assert.equal(room.pendingEventPlays.has('s1'), true) }) }) describe('Room - startGame réinitialise l\'état événements', () => { test('reconstruit un deck de 18 cartes et vide les mains d\'événements', () => { const room = makeStartedRoom(2) room.players[0].eventHand = ['epidemic'] room.eventDeck = ['bountiful_harvest'] room.startGame() assert.equal(room.eventDeck.length, 18) assert.deepEqual(room.players[0].eventHand, []) }) })