feat: cartes Événement (pioche partagée de 18 cartes, 6 effets, synergies, ciblage)
- Nouveau server/src/game/events.js : 6 événements (Récolte exceptionnelle, Épidémie, Levée des troupes, Accord royal, Complot découvert, Grande fête au château), deck PARTAGÉ de 18 cartes (3 exemplaires de chacun) mélangé au démarrage de la partie. - Room.js : buyEventDraw() (coût fixe 1 point dans chacune des 4 jauges, effet immédiat contrairement aux objets/bâtiments — pas de conflit de stock possible), playEventCard() (choix de jouer une carte de sa main, max 1 par tour, avec ciblage optionnel), _resolvePendingEventPlays() (résolution de TOUS les événements du tour AVANT le calcul des gains de cartes personnage, défausse systématique, protection "Complot découvert" pour soi-même uniquement). Les 6 effets incluent leurs synergies avec les objets/bâtiments existants (Charrue, Trousse+Église pour Épidémie, Trébuchet, Sceau Royal, Château Royal → pioche bonus). - IMPORTANT : le ciblage de joueur (Accord royal, variante Trousse d'Épidémie) transite par teamIndex (identifiant public) et non socketId (jamais exposé au client via getPlayersInfo/getStateFor) — le serveur résout teamIndex -> joueur réel au moment de la résolution. - Nouvel event socket 'buy-event-draw' (effet immédiat) et 'play-event' (résolution différée), eventResults inclus dans round-result. - Client : events.js (miroir), EventCardObject.js (carte épurée façon CardObject, badge rouge "!" pour les événements à effet négatif). BootScene génère les textures plein cadre (méthode _generateClippedTexture généralisée, réutilisée avec les personnages). - GameScene : bouton de pioche à droite du bouton de confirmation (état grisé selon jauges/main pleine/pioche déjà tentée), seconde main de cartes événement (max 2) au-dessus de ce bouton, sélection d'une carte pour la jouer avec séquence de ciblage interactive (surbrillance des slots adversaires cliquables) pour Accord royal et la variante Trousse d'Épidémie. - 61 nouveaux tests unitaires serveur (events.js + Room.js : pioche, choix de jouer, résolution des 6 effets avec/sans synergie, Complot découvert, ordre événements-avant-personnages, deck partagé). - Nouveaux helpers et tests d'intégration E2E Playwright (e2e/tests/events.spec.js) pour la pioche, la limite de main à 2, et les 6 effets — certains scénarios restent probabilistes (pioche aléatoire dans le deck partagé) et peuvent nécessiter une stabilisation supplémentaire du timing.
This commit is contained in:
1 parent
9139989fa7
commit
1726f8f96b
17 files changed
+1952
-16
No files matched your search
@@ -0,0 +1,525 @@
|
||||
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, [])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
const { test, describe } = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const { EVENTS, getEventById, buildEventDeck, DRAW_COST } = require('../../src/game/events')
|
||||
|
||||
describe('events.js', () => {
|
||||
test('expose exactement 6 événements', () => {
|
||||
assert.equal(EVENTS.length, 6)
|
||||
})
|
||||
|
||||
test('chaque événement a un id unique', () => {
|
||||
const ids = EVENTS.map(e => e.id)
|
||||
assert.equal(new Set(ids).size, ids.length)
|
||||
})
|
||||
|
||||
test('DRAW_COST demande 1 point dans chacune des 4 jauges', () => {
|
||||
assert.deepEqual(DRAW_COST, { food: 1, war: 1, health: 1, diplo: 1 })
|
||||
})
|
||||
|
||||
test('chaque événement utilise DRAW_COST comme coût de pioche', () => {
|
||||
for (const e of EVENTS) {
|
||||
assert.deepEqual(e.cost, DRAW_COST)
|
||||
}
|
||||
})
|
||||
|
||||
describe('getEventById', () => {
|
||||
test('retourne l\'événement correspondant à un id existant', () => {
|
||||
const epidemic = getEventById('epidemic')
|
||||
assert.ok(epidemic)
|
||||
assert.equal(epidemic.name, 'Épidémie')
|
||||
})
|
||||
|
||||
test('retourne null pour un id inconnu', () => {
|
||||
assert.equal(getEventById('does-not-exist'), null)
|
||||
})
|
||||
|
||||
test('retourne null pour undefined/null', () => {
|
||||
assert.equal(getEventById(undefined), null)
|
||||
assert.equal(getEventById(null), null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildEventDeck', () => {
|
||||
test('contient exactement 18 cartes (3 exemplaires de chacun des 6 événements)', () => {
|
||||
const deck = buildEventDeck()
|
||||
assert.equal(deck.length, 18)
|
||||
})
|
||||
|
||||
test('contient exactement 3 exemplaires de chaque id', () => {
|
||||
const deck = buildEventDeck()
|
||||
const counts = {}
|
||||
deck.forEach(id => { counts[id] = (counts[id] ?? 0) + 1 })
|
||||
EVENTS.forEach(e => assert.equal(counts[e.id], 3, `${e.id} devrait apparaître 3 fois`))
|
||||
})
|
||||
|
||||
test('mélange le deck (l\'ordre n\'est pas systématiquement identique entre deux appels)', () => {
|
||||
const decks = Array.from({ length: 5 }, () => buildEventDeck().join(','))
|
||||
assert.ok(new Set(decks).size > 1, 'les decks mélangés devraient différer entre les appels')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user