import { test, expect } from '@playwright/test' import { installPhaserDebugHook, createRoom, joinRoom, startGame, playCard, playFullTurn, getMyGauges, getMyEventHand, clickEventDrawButton, drawEventUntil, playEventCard } from './helpers.js' /** * Tests d'intégration E2E : cartes Événement (pioche partagée de 18 cartes, * main limitée à 2, choix de jouer une carte, résolution avant les gains de * cartes personnage, synergies avec objets/bâtiments, ciblage de joueurs). */ test.describe('Cartes Événement', () => { test('le bouton de pioche est grisé/non cliquable tant que les jauges sont à 0', async ({ browser }) => { const ctx1 = await browser.newContext() const ctx2 = await browser.newContext() const p1 = await ctx1.newPage() const p2 = await ctx2.newPage() await installPhaserDebugHook(p1) await installPhaserDebugHook(p2) const roomId = await createRoom(p1, 'Alice') await joinRoom(p2, 'Bob', roomId) await startGame(p1) await p1.waitForTimeout(2000) const drew = await clickEventDrawButton(p1) expect(drew).toBe(false) await ctx1.close() await ctx2.close() }) test('acheter une pioche débite 1 point de chacune des 4 jauges et ajoute une carte à la main', async ({ browser }) => { test.setTimeout(60_000) const ctx1 = await browser.newContext() const ctx2 = await browser.newContext() const p1 = await ctx1.newPage() const p2 = await ctx2.newPage() await installPhaserDebugHook(p1) await installPhaserDebugHook(p2) const roomId = await createRoom(p1, 'Alice') await joinRoom(p2, 'Bob', roomId) await startGame(p1) await p1.waitForTimeout(2000) // Accumuler au moins 1 point sur les 4 jauges pour P1 : P1 maximise la // somme des 4 attributs, P2 joue explicitement sa carte la plus FAIBLE // sur ces mêmes attributs (mode 'min') — indispensable pour garantir un // gain non-nul (gain = max(0, ma_valeur - valeur_voisin_gauche)), sinon // la boucle peut ne jamais converger selon les cartes distribuées. let gauges = await getMyGauges(p1) let attempts = 0 while (!['food', 'war', 'health', 'diplo'].every(a => gauges[a] >= 1) && attempts < 12) { await playFullTurn([p1, p2], [ ['food', 'war', 'health', 'diplo'], { attr: ['food', 'war', 'health', 'diplo'], mode: 'min' } ]) gauges = await getMyGauges(p1) attempts++ } expect(['food', 'war', 'health', 'diplo'].every(a => gauges[a] >= 1)).toBe(true) const before = { ...gauges } const drew = await clickEventDrawButton(p1) expect(drew).toBe(true) const after = await getMyGauges(p1) expect(after.food).toBe(before.food - 1) expect(after.war).toBe(before.war - 1) expect(after.health).toBe(before.health - 1) expect(after.diplo).toBe(before.diplo - 1) const hand = await getMyEventHand(p1) expect(hand.length).toBe(1) await ctx1.close() await ctx2.close() }) test('la main d\'événements est limitée à 2 cartes (le bouton se désactive ensuite)', async ({ browser }) => { test.setTimeout(150_000) const ctx1 = await browser.newContext() const ctx2 = await browser.newContext() const p1 = await ctx1.newPage() const p2 = await ctx2.newPage() await installPhaserDebugHook(p1) await installPhaserDebugHook(p2) const roomId = await createRoom(p1, 'Alice') await joinRoom(p2, 'Bob', roomId) await startGame(p1) await p1.waitForTimeout(2000) async function ensureGaugesAndDraw() { let gauges = await getMyGauges(p1) let attempts = 0 while (!['food', 'war', 'health', 'diplo'].every(a => gauges[a] >= 1) && attempts < 12) { await playFullTurn([p1, p2], [ ['food', 'war', 'health', 'diplo'], { attr: ['food', 'war', 'health', 'diplo'], mode: 'min' } ]) gauges = await getMyGauges(p1) attempts++ } return clickEventDrawButton(p1) } expect(await ensureGaugesAndDraw()).toBe(true) // La pioche est immédiate (pas de coût d'un tour), mais un joueur ne peut // tenter qu'UNE pioche par tour : il faut jouer le tour pour pouvoir // retenter la pioche suivante. await playFullTurn([p1, p2]) expect(await ensureGaugesAndDraw()).toBe(true) const hand = await getMyEventHand(p1) expect(hand.length).toBe(2) // Une 3e tentative doit être refusée (main pleine) await playFullTurn([p1, p2]) let gauges = await getMyGauges(p1) let attempts = 0 while (!['food', 'war', 'health', 'diplo'].every(a => gauges[a] >= 1) && attempts < 12) { await playFullTurn([p1, p2], [ ['food', 'war', 'health', 'diplo'], { attr: ['food', 'war', 'health', 'diplo'], mode: 'min' } ]) gauges = await getMyGauges(p1) attempts++ } const thirdDraw = await clickEventDrawButton(p1) expect(thirdDraw).toBe(false) await ctx1.close() await ctx2.close() }) test('jouer "Récolte exceptionnelle" donne +2 Nourriture, résolu avant les gains de carte personnage', async ({ browser }) => { test.setTimeout(210_000) const ctx1 = await browser.newContext() const ctx2 = await browser.newContext() const p1 = await ctx1.newPage() const p2 = await ctx2.newPage() const errors = [] p1.on('pageerror', e => errors.push(e.message)) await installPhaserDebugHook(p1) await installPhaserDebugHook(p2) const roomId = await createRoom(p1, 'Alice') await joinRoom(p2, 'Bob', roomId) await startGame(p1) await p1.waitForTimeout(2000) const drawn = await drawEventUntil(p1, [p2], 'bountiful_harvest') test.skip(!drawn, 'Récolte exceptionnelle non piochée dans la limite de tentatives (probabiliste)') const foodBefore = (await getMyGauges(p1)).food await playEventCard(p1, 'bountiful_harvest') // Neutraliser le gain de carte personnage sur food en jouant la carte // MINIMISANT food pour les deux joueurs, afin d'isoler l'effet événement. await playFullTurn([p1, p2], [ { attr: 'food', mode: 'min' }, { attr: 'food', mode: 'min' } ]) const foodAfter = (await getMyGauges(p1)).food expect(foodAfter).toBeGreaterThanOrEqual(foodBefore + 2) expect(errors).toEqual([]) await ctx1.close() await ctx2.close() }) test('jouer "Épidémie" fait perdre 1 Santé à tous les joueurs (effet de base, sans objet/bâtiment)', async ({ browser }) => { test.setTimeout(210_000) const ctx1 = await browser.newContext() const ctx2 = await browser.newContext() const p1 = await ctx1.newPage() const p2 = await ctx2.newPage() await installPhaserDebugHook(p1) await installPhaserDebugHook(p2) const roomId = await createRoom(p1, 'Alice') await joinRoom(p2, 'Bob', roomId) await startGame(p1) await p1.waitForTimeout(2000) // Accumuler un peu de Santé pour Alice afin que la perte soit visible // sans clamp à 0 : Alice maximise health, Bob joue explicitement sa // carte la plus FAIBLE en health (mode 'min') pour garantir un gain // non-nul à chaque tour. let healthGauge = (await getMyGauges(p1)).health let healthAttempts = 0 while (healthGauge < 3 && healthAttempts < 8) { await playFullTurn([p1, p2], [ ['health'], { attr: 'health', mode: 'min' } ]) healthGauge = (await getMyGauges(p1)).health healthAttempts++ } const drawn = await drawEventUntil(p1, [p2], 'epidemic') test.skip(!drawn, 'Épidémie non piochée dans la limite de tentatives (probabiliste)') const p1HealthBefore = (await getMyGauges(p1)).health await playEventCard(p1, 'epidemic') await playFullTurn([p1, p2], [ { attr: 'health', mode: 'min' }, { attr: 'health', mode: 'min' } ]) const p1HealthAfter = (await getMyGauges(p1)).health // Le lanceur lui-même perd aussi 1 Santé avec l'effet de base (tout le // monde affecté), avant tout gain de carte personnage neutralisé. expect(p1HealthAfter).toBeLessThanOrEqual(p1HealthBefore) await ctx1.close() await ctx2.close() }) test('jouer "Accord royal" avec ciblage : le joueur choisi en gain reçoit +1 Diplomatie', async ({ browser }) => { // Timeout plus généreux que les autres tests d'événement : ce scénario // nécessite 3 joueurs (pour avoir 2 cibles distinctes disponibles), ce // qui multiplie par ~1.5 le coût de chaque tour (3 playCard au lieu de 2) // et peut nécessiter plusieurs cycles d'accumulation de jauge avant de // pouvoir à la fois piocher ET rencontrer l'événement recherché. test.setTimeout(240_000) const ctx1 = await browser.newContext() const ctx2 = await browser.newContext() const ctx3 = await browser.newContext() const p1 = await ctx1.newPage() const p2 = await ctx2.newPage() const p3 = await ctx3.newPage() await installPhaserDebugHook(p1) await installPhaserDebugHook(p2) await installPhaserDebugHook(p3) const roomId = await createRoom(p1, 'Alice') await joinRoom(p2, 'Bob', roomId) await joinRoom(p3, 'Carl', roomId) await startGame(p1) await p1.waitForTimeout(2000) const drawn = await drawEventUntil(p1, [p2, p3], 'royal_accord') test.skip(!drawn, 'Accord royal non piochée dans la limite de tentatives (probabiliste)') const bobDiploBefore = (await getMyGauges(p2)).diplo // Le ciblage d'Accord royal est séquentiel et exige LES DEUX étapes // (gain puis perte) avant que la carte ne soit réellement confirmée et // envoyée au serveur (voir GameScene._onTargetSlotClicked/_startTargeting) // — fournir une seule cible laisse la séquence de ciblage bloquée en // attente de la seconde, sans jamais émettre 'play-event'. // Cible teamIndex 1 (Bob) pour le gain, teamIndex 2 (Carl) pour la perte. await playEventCard(p1, 'royal_accord', [1, 2]) await playFullTurn([p1, p2, p3]) const bobDiploAfter = (await getMyGauges(p2)).diplo expect(bobDiploAfter).toBeGreaterThanOrEqual(bobDiploBefore + 1) await ctx1.close() await ctx2.close() await ctx3.close() }) test('jouer "Complot découvert" ne modifie aucune jauge par lui-même', async ({ browser }) => { test.setTimeout(210_000) const ctx1 = await browser.newContext() const ctx2 = await browser.newContext() const p1 = await ctx1.newPage() const p2 = await ctx2.newPage() const errors = [] p1.on('pageerror', e => errors.push(e.message)) await installPhaserDebugHook(p1) await installPhaserDebugHook(p2) const roomId = await createRoom(p1, 'Alice') await joinRoom(p2, 'Bob', roomId) await startGame(p1) await p1.waitForTimeout(2000) const drawn = await drawEventUntil(p1, [p2], 'plot_uncovered') test.skip(!drawn, 'Complot découvert non piochée dans la limite de tentatives (probabiliste)') await playEventCard(p1, 'plot_uncovered') await playFullTurn([p1, p2], [ { attr: 'food', mode: 'min' }, { attr: 'food', mode: 'min' } ]) expect(errors).toEqual([]) await ctx1.close() await ctx2.close() }) test('jouer "Grande fête au château" donne +1 Nourriture et +1 Diplomatie', async ({ browser }) => { test.setTimeout(210_000) const ctx1 = await browser.newContext() const ctx2 = await browser.newContext() const p1 = await ctx1.newPage() const p2 = await ctx2.newPage() await installPhaserDebugHook(p1) await installPhaserDebugHook(p2) const roomId = await createRoom(p1, 'Alice') await joinRoom(p2, 'Bob', roomId) await startGame(p1) await p1.waitForTimeout(2000) const drawn = await drawEventUntil(p1, [p2], 'castle_feast') test.skip(!drawn, 'Grande fête non piochée dans la limite de tentatives (probabiliste)') const before = await getMyGauges(p1) await playEventCard(p1, 'castle_feast') await playFullTurn([p1, p2], [ { attr: ['food', 'diplo'], mode: 'min' }, { attr: ['food', 'diplo'], mode: 'min' } ]) const after = await getMyGauges(p1) expect(after.food).toBeGreaterThanOrEqual(before.food + 1) expect(after.diplo).toBeGreaterThanOrEqual(before.diplo + 1) await ctx1.close() await ctx2.close() }) test('les résultats des cartes Événement jouées apparaissent dans le résumé du tour', async ({ browser }) => { test.setTimeout(210_000) const ctx1 = await browser.newContext() const ctx2 = await browser.newContext() const p1 = await ctx1.newPage() const p2 = await ctx2.newPage() await installPhaserDebugHook(p1) await installPhaserDebugHook(p2) const roomId = await createRoom(p1, 'Alice') await joinRoom(p2, 'Bob', roomId) await startGame(p1) await p1.waitForTimeout(2000) const drawn = await drawEventUntil(p1, [p2], 'bountiful_harvest') test.skip(!drawn, 'Récolte exceptionnelle non piochée dans la limite de tentatives (probabiliste)') await playEventCard(p1, 'bountiful_harvest') // Jouer la carte personnage du tour (via le helper standard, déjà // défensif face aux états de scène non encore initialisés) pour // déclencher la résolution et observer le résumé affiché juste après. await playCard(p1) await playFullTurn([p2]) await p1.waitForTimeout(500) const readyText = await p1.evaluate(() => { const game = window.__ALL_GAMES__?.[window.__ALL_GAMES__.length - 1] return game?.scene?.getScene('GameScene')?.readyText?.text ?? '' }) expect(readyText).toContain('Récolte exceptionnelle') await ctx1.close() await ctx2.close() }) })