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] } describe('Room - gestion des joueurs', () => { test('addPlayer ajoute un joueur avec jauges à 0 et teamIndex incrémental', () => { const room = new Room('R1') assert.equal(room.addPlayer('s1', 'Alice'), true) assert.equal(room.addPlayer('s2', 'Bob'), true) assert.equal(room.players.length, 2) assert.equal(room.players[0].teamIndex, 0) assert.equal(room.players[1].teamIndex, 1) assert.deepEqual(room.players[0].gauges, { food: 0, war: 0, health: 0, diplo: 0 }) assert.deepEqual(room.players[0].items, []) }) test('addPlayer refuse un 5e joueur (MAX_PLAYERS = 4)', () => { const room = new Room('R2') for (let i = 0; i < 4; i++) assert.equal(room.addPlayer(`s${i}`, `P${i}`), true) assert.equal(room.addPlayer('s4', 'Trop'), false) assert.equal(room.players.length, 4) }) test('addPlayer est idempotent pour un socketId déjà présent (reconnexion)', () => { const room = new Room('R3') room.addPlayer('s1', 'Alice') const result = room.addPlayer('s1', 'Alice') assert.equal(result, true) assert.equal(room.players.length, 1) }) test('removePlayer retire le joueur de la liste', () => { const room = new Room('R4') room.addPlayer('s1', 'Alice') room.addPlayer('s2', 'Bob') room.removePlayer('s1') assert.equal(room.players.length, 1) assert.equal(room.players[0].socketId, 's2') }) test('removePlayer nettoie les achats/cartes en attente du joueur parti', () => { const room = makeStartedRoom(3) room.players[0].gauges.food = 10 room.buyItem('s0', 'plow') room.playCard('s1', room.players[1].hand[0].id) room.removePlayer('s0') assert.equal(room.pendingBuys.has('s0'), false) // s1 avait joué une carte, removePlayer ne doit pas y toucher (ce n'est pas lui qui part) assert.equal(room.pendingPlays.has('s1'), true) }) test('removePlayer conserve l\'identité du "premier joueur" après un décalage d\'index', () => { const room = makeStartedRoom(4) // firstPlayerIndex pointe sur s0 ; on retire s0's voisin s1 (index 1, avant s0 dans la liste) room.firstPlayerIndex = 2 // pointe sur s2 room.removePlayer('s1') // s1 est retiré, s2 glisse à l'index 1 const firstPlayer = room.players[room.firstPlayerIndex] assert.equal(firstPlayer.socketId, 's2', 'le premier joueur doit rester s2 malgré le décalage') }) test('removePlayer garde firstPlayerIndex borné si c\'est le premier joueur lui-même qui part', () => { const room = makeStartedRoom(3) room.firstPlayerIndex = 2 // dernier joueur (s2) room.removePlayer('s2') assert.ok(room.firstPlayerIndex < room.players.length, 'index doit rester dans les bornes') }) test('removePlayer ramène firstPlayerIndex à 0 si tous les joueurs partent', () => { const room = makeStartedRoom(2) room.removePlayer('s0') room.removePlayer('s1') assert.equal(room.players.length, 0) assert.equal(room.firstPlayerIndex, 0) }) }) describe('Room - startGame', () => { test('distribue une main de 4 cartes et remet les jauges/items à zéro', () => { const room = makeStartedRoom(3) assert.equal(room.currentTurn, 1) assert.equal(room.phase, 'selecting') for (const p of room.players) { assert.equal(p.hand.length, 4) assert.equal(p.deck.length, 8) assert.deepEqual(p.gauges, { food: 0, war: 0, health: 0, diplo: 0 }) assert.deepEqual(p.items, []) } }) test('réinitialise le stock d\'objets disponibles et la priorité d\'achat', () => { const room = makeStartedRoom(2) assert.equal(room.availableItems.length, 4) assert.equal(room.firstPlayerIndex, 0) }) }) describe('Room - playCard', () => { test('retire la carte de la main et l\'enregistre en pendingPlays', () => { const room = makeStartedRoom(2) const card = room.players[0].hand[0] const result = room.playCard('s0', card.id) assert.equal(result.ok, true) assert.equal(room.players[0].hand.length, 3) assert.equal(room.pendingPlays.get('s0').card.id, card.id) }) test('refuse de jouer hors phase selecting', () => { const room = makeStartedRoom(2) room.phase = 'resolve' const result = room.playCard('s0', room.players[0].hand[0].id) assert.equal(result.ok, false) }) test('refuse un second coup dans le même tour', () => { const room = makeStartedRoom(2) const [c1, c2] = room.players[0].hand room.playCard('s0', c1.id) const result = room.playCard('s0', c2.id) assert.equal(result.ok, false) }) test('refuse une carte absente de la main', () => { const room = makeStartedRoom(2) const result = room.playCard('s0', 'carte-inexistante') assert.equal(result.ok, false) }) test('refuse un joueur inconnu', () => { const room = makeStartedRoom(2) const result = room.playCard('ghost', 'whatever') assert.equal(result.ok, false) }) test('allPlayersPlayed reflète le nombre de coups joués', () => { const room = makeStartedRoom(2) assert.equal(room.allPlayersPlayed(), false) room.playCard('s0', room.players[0].hand[0].id) assert.equal(room.allPlayersPlayed(), false) room.playCard('s1', room.players[1].hand[0].id) assert.equal(room.allPlayersPlayed(), true) }) }) describe('Room - buyItem (validations)', () => { test('accepte un achat si la jauge est suffisante', () => { const room = makeStartedRoom(2) room.players[0].gauges.war = 5 const result = room.buyItem('s0', 'trebuchet') assert.equal(result.ok, true) assert.equal(room.pendingBuys.get('s0'), 'trebuchet') }) test('refuse si jauge insuffisante', () => { const room = makeStartedRoom(2) room.players[0].gauges.war = 1 const result = room.buyItem('s0', 'trebuchet') assert.equal(result.ok, false) assert.match(result.error, /Pas assez de points/) }) test('refuse hors phase selecting', () => { const room = makeStartedRoom(2) room.phase = 'resolve' const result = room.buyItem('s0', 'trebuchet') assert.equal(result.ok, false) }) test('refuse un objet inconnu', () => { const room = makeStartedRoom(2) const result = room.buyItem('s0', 'objet-inexistant') assert.equal(result.ok, false) assert.match(result.error, /inconnu/) }) test('refuse un objet déjà retiré du stock (availableItems)', () => { const room = makeStartedRoom(2) room.availableItems = room.availableItems.filter(id => id !== 'trebuchet') room.players[0].gauges.war = 10 const result = room.buyItem('s0', 'trebuchet') assert.equal(result.ok, false) assert.match(result.error, /déjà été acheté/) }) test('refuse un objet déjà possédé par ce joueur', () => { const room = makeStartedRoom(2) room.players[0].items.push('trebuchet') room.players[0].gauges.war = 10 const result = room.buyItem('s0', 'trebuchet') assert.equal(result.ok, false) assert.match(result.error, /possèdes déjà/) }) test('refuse une seconde tentative d\'achat le même tour', () => { const room = makeStartedRoom(2) room.players[0].gauges.war = 10 room.players[0].gauges.food = 10 room.buyItem('s0', 'trebuchet') const result = room.buyItem('s0', 'plow') assert.equal(result.ok, false) assert.match(result.error, /déjà tenté/) }) test('refuse un joueur inconnu', () => { const room = makeStartedRoom(2) const result = room.buyItem('ghost', 'trebuchet') assert.equal(result.ok, false) }) }) describe('Room - resolveRound : calcul des gains de base', () => { test('le gain = max(0, ma_valeur - valeur_voisin_gauche)', () => { const room = makeStartedRoom(2) // Index 0 : voisin de gauche = index 1 (n=2, (0-1+2)%2 = 1) setHandCardValues(room, 's0', { war: 5 }) setHandCardValues(room, 's1', { war: 2 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() // s0 vs voisin gauche s1 : max(0, 5-2) = 3 assert.equal(result.gains[0].war, 3) // s1 vs voisin gauche s0 : max(0, 2-5) = 0 assert.equal(result.gains[1].war, 0) }) test('un gain négatif est clampé à 0, jamais retiré de la jauge', () => { const room = makeStartedRoom(2) setHandCardValues(room, 's0', { food: 1 }) setHandCardValues(room, 's1', { food: 6 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) room.resolveRound() assert.equal(room.players[0].gauges.food, 0) }) test('applique bien les gains cumulés aux jauges des joueurs', () => { const room = makeStartedRoom(2) setHandCardValues(room, 's0', { health: 4 }) setHandCardValues(room, 's1', { health: 1 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) room.resolveRound() assert.equal(room.players[0].gauges.health, 3) }) test('vide pendingPlays et passe en phase resolve', () => { const room = makeStartedRoom(2) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) room.resolveRound() assert.equal(room.pendingPlays.size, 0) assert.equal(room.phase, 'resolve') }) test('gère un joueur n\'ayant pas joué de carte (card=null) sans planter', () => { const room = makeStartedRoom(2) room.playCard('s0', room.players[0].hand[0].id) // s1 ne joue rien const result = room.resolveRound() assert.equal(result.playedCards[1].cardId, undefined) assert.equal(result.gains[1].food, 0) }) }) describe('Room - resolveRound : effets permanents des objets', () => { test('double_gain (Trébuchet) double le gain de guerre', () => { const room = makeStartedRoom(2) room.players[0].items.push('trebuchet') setHandCardValues(room, 's0', { war: 4 }) setHandCardValues(room, 's1', { war: 1 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() // gain de base = 4-1 = 3, doublé par Trébuchet = 6 assert.equal(result.gains[0].war, 6) }) test('guaranteed_gain (Trousse) garantit un minimum même si le gain de base est plus faible', () => { const room = makeStartedRoom(2) room.players[0].items.push('medkit') setHandCardValues(room, 's0', { health: 1 }) setHandCardValues(room, 's1', { health: 6 }) // gain de base = 0 room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() const medkit = require('../../src/game/items').getItemById('medkit') assert.equal(result.gains[0].health, medkit.effectValue) }) test('guaranteed_gain n\'écrase pas un gain de base déjà supérieur au minimum', () => { const room = makeStartedRoom(2) room.players[0].items.push('medkit') setHandCardValues(room, 's0', { health: 6 }) setHandCardValues(room, 's1', { health: 1 }) // gain de base = 5, > effectValue (2) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.equal(result.gains[0].health, 5) }) test('copy_best_in_game (Sceau Royal) copie le meilleur gain diplo de tous les autres joueurs', () => { const room = makeStartedRoom(3) room.players[0].items.push('royal_seal') setHandCardValues(room, 's0', { diplo: 0 }) setHandCardValues(room, 's1', { diplo: 0 }) setHandCardValues(room, 's2', { diplo: 0 }) // Forcer un gros gain diplo pour s1 en jouant une carte à forte diplo // contre le voisin de gauche de s1 (s0, diplo=0 car sa carte a diplo=0) room.players[1].hand[0].diplo = 6 room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) room.playCard('s2', room.players[2].hand[0].id) const result = room.resolveRound() // s1 vs voisin gauche s0 (diplo 0) : gain de base = 6 assert.equal(result.gains[1].diplo, 6) // s0 possède le Sceau : copie le meilleur gain diplo parmi s1/s2 = 6 assert.equal(result.gains[0].diplo, 6) }) test('copy_best_in_game ne dégrade jamais le gain de base du porteur', () => { const room = makeStartedRoom(3) room.players[0].items.push('royal_seal') setHandCardValues(room, 's0', { diplo: 6 }) setHandCardValues(room, 's1', { diplo: 0 }) setHandCardValues(room, 's2', { diplo: 0 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) room.playCard('s2', room.players[2].hand[0].id) const result = room.resolveRound() // s0 a déjà le meilleur gain de base possible, le Sceau ne doit pas le réduire assert.ok(result.gains[0].diplo >= 6) }) }) describe('Room - résolution des achats en attente (priorité, stock unique)', () => { test('un achat en attente est débité de la jauge et ajouté aux items du joueur', () => { const room = makeStartedRoom(2) room.players[0].gauges.war = 5 room.buyItem('s0', 'trebuchet') room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.equal(room.players[0].items.includes('trebuchet'), true) assert.equal(room.availableItems.includes('trebuchet'), false) assert.equal(result.boughtItems.length, 1) assert.equal(result.boughtItems[0].itemId, 'trebuchet') }) test('déduit uniquement le coût de l\'objet, pas plus', () => { const room = makeStartedRoom(2) room.players[0].gauges.war = 10 room.buyItem('s0', 'trebuchet') setHandCardValues(room, 's0', { war: 0 }) setHandCardValues(room, 's1', { war: 0 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) room.resolveRound() const trebuchet = require('../../src/game/items').getItemById('trebuchet') assert.equal(room.players[0].gauges.war, 10 - trebuchet.cost) }) test('en cas de conflit sur le même objet, le "premier joueur" du tour gagne', () => { const room = makeStartedRoom(2) room.players[0].gauges.food = 10 room.players[1].gauges.food = 10 room.firstPlayerIndex = 0 // s0 est prioritaire room.buyItem('s0', 'plow') room.buyItem('s1', 'plow') // Neutraliser l'attribut testé (food) sur les cartes jouées pour isoler // le coût d'achat du gain de carte normal (sinon la comparaison de gain // voisin-de-gauche ajouterait aussi des points à cette même jauge). setHandCardValues(room, 's0', { food: 0 }) setHandCardValues(room, 's1', { food: 0 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.equal(result.boughtItems.length, 1) assert.equal(result.boughtItems[0].playerName, 'P0') assert.equal(room.players[0].items.includes('plow'), true) assert.equal(room.players[1].items.includes('plow'), false) // Le perdant ne paie rien assert.equal(room.players[1].gauges.food, 10) }) test('en cas de conflit, la priorité suit firstPlayerIndex même si ce n\'est pas le joueur 0', () => { const room = makeStartedRoom(3) room.players[0].gauges.food = 10 room.players[1].gauges.food = 10 room.players[2].gauges.food = 10 room.firstPlayerIndex = 2 // s2 est prioritaire ce tour room.buyItem('s0', 'plow') room.buyItem('s1', 'plow') room.buyItem('s2', 'plow') room.players.forEach(p => room.playCard(p.socketId, p.hand[0].id)) const result = room.resolveRound() assert.equal(result.boughtItems[0].playerName, 'P2') }) test('chaque objet ne peut être acheté qu\'une seule fois pour toute la partie', () => { const room = makeStartedRoom(2) room.players[0].gauges.war = 10 room.buyItem('s0', 'trebuchet') room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) room.resolveRound() room.nextTurn() room.players[1].gauges.war = 10 const secondAttempt = room.buyItem('s1', 'trebuchet') assert.equal(secondAttempt.ok, false) }) test('pas d\'achat en attente -> boughtItems vide, aucune jauge modifiée', () => { const room = makeStartedRoom(2) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.deepEqual(result.boughtItems, []) }) }) describe('Room - nextTurn', () => { test('chaque joueur pioche une carte de son deck', () => { const room = makeStartedRoom(2) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) room.resolveRound() const deckSizeBefore = room.players[0].deck.length const handSizeBefore = room.players[0].hand.length room.nextTurn() assert.equal(room.players[0].deck.length, deckSizeBefore - 1) assert.equal(room.players[0].hand.length, handSizeBefore + 1) }) test('fait tourner firstPlayerIndex de façon circulaire', () => { const room = makeStartedRoom(3) assert.equal(room.firstPlayerIndex, 0) room.nextTurn() assert.equal(room.firstPlayerIndex, 1) room.nextTurn() assert.equal(room.firstPlayerIndex, 2) room.nextTurn() assert.equal(room.firstPlayerIndex, 0) }) test('incrémente currentTurn et repasse en phase selecting', () => { const room = makeStartedRoom(2) room.nextTurn() assert.equal(room.currentTurn, 2) assert.equal(room.phase, 'selecting') }) test('vide pendingBuys au tour suivant', () => { const room = makeStartedRoom(2) room.players[0].gauges.war = 1 // insuffisant, mais peu importe pour ce test room.pendingBuys.set('s0', 'trebuchet') room.nextTurn() assert.equal(room.pendingBuys.size, 0) }) test('passe en phase end quand currentTurn dépasse maxTurns', () => { const room = makeStartedRoom(2) room.currentTurn = room.maxTurns room.nextTurn() assert.equal(room.phase, 'end') }) test('isGameOver devient vrai quand mains ET decks sont vides', () => { const room = makeStartedRoom(2) room.players.forEach(p => { p.hand = []; p.deck = [] }) assert.equal(room.isGameOver(), true) }) test('isGameOver est faux tant qu\'il reste des cartes en main ou en deck', () => { const room = makeStartedRoom(2) assert.equal(room.isGameOver(), false) }) }) describe('Room - données publiques (getPlayersInfo / getStateFor)', () => { test('getPlayersInfo calcule le score comme la somme des 4 jauges', () => { const room = makeStartedRoom(1) room.players[0].gauges = { food: 1, war: 2, health: 3, diplo: 4 } const info = room.getPlayersInfo() assert.equal(info[0].score, 10) }) test('getPlayersInfo retourne des copies (pas de référence directe aux gauges/items internes)', () => { const room = makeStartedRoom(1) const info = room.getPlayersInfo() info[0].gauges.food = 999 info[0].items.push('hack') assert.equal(room.players[0].gauges.food, 0) assert.deepEqual(room.players[0].items, []) }) test('getStateFor renvoie phase="waiting" pour un joueur ayant déjà joué, "selecting" pour les autres', () => { const room = makeStartedRoom(2) room.playCard('s0', room.players[0].hand[0].id) assert.equal(room.getStateFor('s0').phase, 'waiting') assert.equal(room.getStateFor('s1').phase, 'selecting') }) test('getStateFor expose pendingBuyItemId propre à chaque joueur', () => { const room = makeStartedRoom(2) room.players[0].gauges.war = 5 room.buyItem('s0', 'trebuchet') assert.equal(room.getStateFor('s0').pendingBuyItemId, 'trebuchet') assert.equal(room.getStateFor('s1').pendingBuyItemId, null) }) test('getStateFor expose firstPlayerName correspondant à firstPlayerIndex', () => { const room = makeStartedRoom(3) room.firstPlayerIndex = 1 assert.equal(room.getStateFor('s0').firstPlayerName, 'P1') }) test('getStateFor gère un socketId inconnu sans planter (spectateur/déconnecté)', () => { const state = makeStartedRoom(2).getStateFor('ghost') assert.deepEqual(state.hand, []) assert.deepEqual(state.myItems, []) assert.equal(state.pendingBuyItemId, null) }) test('getStateFor expose aussi les champs liés aux bâtiments', () => { const room = makeStartedRoom(2) room.players[0].gauges.health = 5 room.players[0].gauges.diplo = 5 room.buyBuilding('s0', 'church') const state0 = room.getStateFor('s0') assert.equal(state0.pendingBuyBuildingId, 'church') assert.deepEqual(state0.myBuildings, []) assert.ok(state0.availableBuildings.includes('church')) const state1 = room.getStateFor('s1') assert.equal(state1.pendingBuyBuildingId, null) }) }) describe('Room - buyBuilding (validations)', () => { test('accepte un achat si les DEUX jauges requises sont suffisantes', () => { const room = makeStartedRoom(2) room.players[0].gauges.health = 5 room.players[0].gauges.diplo = 5 const result = room.buyBuilding('s0', 'church') assert.equal(result.ok, true) assert.equal(room.pendingBuildingBuys.get('s0'), 'church') }) test('refuse si UNE SEULE des deux jauges requises est insuffisante', () => { const room = makeStartedRoom(2) room.players[0].gauges.health = 5 room.players[0].gauges.diplo = 0 // insuffisant (Église coûte 2 diplo) const result = room.buyBuilding('s0', 'church') assert.equal(result.ok, false) assert.match(result.error, /Pas assez de points/) }) test('refuse hors phase selecting', () => { const room = makeStartedRoom(2) room.phase = 'resolve' const result = room.buyBuilding('s0', 'church') assert.equal(result.ok, false) }) test('refuse un bâtiment inconnu', () => { const room = makeStartedRoom(2) const result = room.buyBuilding('s0', 'batiment-inexistant') assert.equal(result.ok, false) assert.match(result.error, /inconnu/) }) test('refuse un bâtiment déjà retiré du stock', () => { const room = makeStartedRoom(2) room.availableBuildings = room.availableBuildings.filter(id => id !== 'church') room.players[0].gauges.health = 10 room.players[0].gauges.diplo = 10 const result = room.buyBuilding('s0', 'church') assert.equal(result.ok, false) assert.match(result.error, /déjà été acheté/) }) test('refuse un bâtiment déjà possédé par ce joueur', () => { const room = makeStartedRoom(2) room.players[0].buildings.push('church') room.players[0].gauges.health = 10 room.players[0].gauges.diplo = 10 const result = room.buyBuilding('s0', 'church') assert.equal(result.ok, false) assert.match(result.error, /possèdes déjà/) }) test('refuse une seconde tentative d\'achat de bâtiment le même tour', () => { const room = makeStartedRoom(2) room.players[0].gauges.health = 10 room.players[0].gauges.diplo = 10 room.players[0].gauges.food = 10 room.players[0].gauges.war = 10 room.buyBuilding('s0', 'church') const result = room.buyBuilding('s0', 'castle') assert.equal(result.ok, false) assert.match(result.error, /déjà tenté/) }) test('refuse un joueur inconnu', () => { const room = makeStartedRoom(2) const result = room.buyBuilding('ghost', 'church') assert.equal(result.ok, false) }) }) describe('Room - résolution des achats de bâtiments (coût multi-jauges, priorité, stock unique)', () => { test('un achat de bâtiment déduit le coût sur les DEUX jauges concernées', () => { const room = makeStartedRoom(2) room.players[0].gauges.health = 10 room.players[0].gauges.diplo = 10 room.buyBuilding('s0', 'church') setHandCardValues(room, 's0', { health: 0, diplo: 0 }) setHandCardValues(room, 's1', { health: 0, diplo: 0 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.equal(room.players[0].buildings.includes('church'), true) assert.equal(room.availableBuildings.includes('church'), false) assert.equal(result.boughtBuildings.length, 1) assert.equal(result.boughtBuildings[0].buildingId, 'church') // Église coûte 3 health + 2 diplo ; le +1 garanti de base s'applique // DÉJÀ ce tour (même règle que les objets), donc jauge finale = 10 - coût + garanti assert.equal(room.players[0].gauges.health, 10 - 3 + 1) assert.equal(room.players[0].gauges.diplo, 10 - 2 + 1) }) test('en cas de conflit sur le même bâtiment, le "premier joueur" du tour gagne', () => { const room = makeStartedRoom(2) room.players[0].gauges.food = 10 room.players[0].gauges.war = 10 room.players[1].gauges.food = 10 room.players[1].gauges.war = 10 room.firstPlayerIndex = 0 // s0 prioritaire room.buyBuilding('s0', 'castle') room.buyBuilding('s1', 'castle') setHandCardValues(room, 's0', { food: 0, war: 0 }) setHandCardValues(room, 's1', { food: 0, war: 0 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.equal(result.boughtBuildings.length, 1) assert.equal(result.boughtBuildings[0].playerName, 'P0') assert.equal(room.players[0].buildings.includes('castle'), true) assert.equal(room.players[1].buildings.includes('castle'), false) // Le perdant ne paie rien (jauges inchangées, hors gain de carte neutralisé) assert.equal(room.players[1].gauges.food, 10) assert.equal(room.players[1].gauges.war, 10) }) test('chaque bâtiment ne peut être acheté qu\'une seule fois pour toute la partie', () => { const room = makeStartedRoom(2) room.players[0].gauges.food = 10 room.players[0].gauges.war = 10 room.buyBuilding('s0', 'castle') room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) room.resolveRound() room.nextTurn() room.players[1].gauges.food = 10 room.players[1].gauges.war = 10 const secondAttempt = room.buyBuilding('s1', 'castle') assert.equal(secondAttempt.ok, false) }) }) describe('Room - resolveRound : effets permanents des bâtiments (+ synergie objets)', () => { test('un bâtiment garantit +1 sur CHACUN de ses deux attributs, même sans synergie', () => { const room = makeStartedRoom(2) room.players[0].buildings.push('church') // possession directe, sans passer par un achat setHandCardValues(room, 's0', { health: 0, diplo: 0 }) setHandCardValues(room, 's1', { health: 6, diplo: 6 }) // gain de base = 0 pour s0 room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.equal(result.gains[0].health, 1) assert.equal(result.gains[0].diplo, 1) }) test('le bâtiment ne réduit jamais un gain de base déjà supérieur au minimum garanti', () => { const room = makeStartedRoom(2) room.players[0].buildings.push('church') setHandCardValues(room, 's0', { health: 6, diplo: 6 }) setHandCardValues(room, 's1', { health: 0, diplo: 0 }) // gain de base = 6 pour s0 room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.equal(result.gains[0].health, 6) assert.equal(result.gains[0].diplo, 6) }) test('synergie : +1 supplémentaire sur Santé si Église ET Trousse du Médecin sont possédées', () => { const room = makeStartedRoom(2) room.players[0].buildings.push('church') room.players[0].items.push('medkit') // synergie sur l'attribut 'health' setHandCardValues(room, 's0', { health: 0, diplo: 0 }) setHandCardValues(room, 's1', { health: 6, diplo: 0 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() // baseGuaranteed(1) + synergie(1) = 2, ET la Trousse elle-même garantit // déjà 2 (effectValue) sur health via _applyItemEffects → le max des deux // effets doit donner au moins 2. assert.ok(result.gains[0].health >= 2, `attendu >= 2, obtenu ${result.gains[0].health}`) // Sans synergie sur diplo (pas de royal_seal), seul le +1 de base s'applique assert.equal(result.gains[0].diplo, 1) }) test('synergie : +1 supplémentaire sur Guerre si Château ET Trébuchet sont possédés', () => { const room = makeStartedRoom(2) room.players[0].buildings.push('castle') room.players[0].items.push('trebuchet') // synergie sur l'attribut 'war' setHandCardValues(room, 's0', { food: 0, war: 2 }) setHandCardValues(room, 's1', { food: 0, war: 0 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() // gain de base war = 2-0 = 2, doublé par Trébuchet = 4, le Château garantit // seulement max(4, 1+1=2) = 4 → le doublement de l'objet reste dominant ici assert.equal(result.gains[0].war, 4) // Sur food (pas de synergie, pas de Charrue), seul le +1 garanti du Château s'applique assert.equal(result.gains[0].food, 1) }) test('sans l\'objet de synergie, le bâtiment seul ne garantit que baseGuaranteed (pas de bonus)', () => { const room = makeStartedRoom(2) room.players[0].buildings.push('castle') // pas de Trébuchet/Charrue possédés setHandCardValues(room, 's0', { food: 0, war: 0 }) setHandCardValues(room, 's1', { food: 6, war: 6 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.equal(result.gains[0].food, 1) assert.equal(result.gains[0].war, 1) }) test('possession simultanée des 2 bâtiments : chacun garantit son propre plancher indépendamment', () => { const room = makeStartedRoom(2) room.players[0].buildings.push('church', 'castle') setHandCardValues(room, 's0', { food: 0, war: 0, health: 0, diplo: 0 }) setHandCardValues(room, 's1', { food: 6, war: 6, health: 6, diplo: 6 }) room.playCard('s0', room.players[0].hand[0].id) room.playCard('s1', room.players[1].hand[0].id) const result = room.resolveRound() assert.equal(result.gains[0].food, 1) assert.equal(result.gains[0].war, 1) assert.equal(result.gains[0].health, 1) assert.equal(result.gains[0].diplo, 1) }) })