- 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.
376 lines
13 KiB
JavaScript
376 lines
13 KiB
JavaScript
import { test, expect } from '@playwright/test'
|
|
import {
|
|
installPhaserDebugHook, createRoom, joinRoom, startGame,
|
|
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 en jouant des cartes qui
|
|
// maximisent la somme des 4 attributs.
|
|
let gauges = await getMyGauges(p1)
|
|
let attempts = 0
|
|
while (!['food', 'war', 'health', 'diplo'].every(a => gauges[a] >= 1) && attempts < 6) {
|
|
await playFullTurn([p1, p2], [['food', 'war', 'health', 'diplo'], ['food', 'war', 'health', 'diplo']])
|
|
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(90_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 < 6) {
|
|
await playFullTurn([p1, p2], [['food', 'war', 'health', 'diplo'], ['food', 'war', 'health', 'diplo']])
|
|
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 < 6) {
|
|
await playFullTurn([p1, p2], [['food', 'war', 'health', 'diplo'], ['food', 'war', 'health', 'diplo']])
|
|
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(90_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(90_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 les deux joueurs afin que la perte soit visible sans clamp à 0.
|
|
for (let t = 0; t < 3; t++) {
|
|
await playFullTurn([p1, p2], [
|
|
['health'],
|
|
{ attr: 'health', mode: 'min' }
|
|
])
|
|
}
|
|
|
|
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 }) => {
|
|
test.setTimeout(90_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 p2.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
const me = scene.store.players.find(p => p.teamIndex === scene.store.myTeamIndex)
|
|
return me.gauges.diplo
|
|
}))
|
|
|
|
// Cible teamIndex 1 (Bob) pour le gain, pas de cible pour la perte (laissée non cliquée)
|
|
await playEventCard(p1, 'royal_accord', [1])
|
|
await playFullTurn([p1, p2, p3])
|
|
|
|
const bobDiploAfter = (await p2.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
const me = scene.store.players.find(p => p.teamIndex === scene.store.myTeamIndex)
|
|
return me.gauges.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(90_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(90_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(90_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 manuellement pour observer le texte affiché juste après résolution,
|
|
// sans attendre le waitForTimeout complet de playFullTurn.
|
|
const canvas1 = p1.locator('#phaser-container canvas')
|
|
const box1 = await canvas1.boundingBox()
|
|
const cardPos = await p1.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
const b = scene.handCards[0].getBounds()
|
|
return { x: b.centerX, y: b.centerY }
|
|
})
|
|
await p1.mouse.click(box1.x + cardPos.x, box1.y + cardPos.y)
|
|
await p1.waitForTimeout(200)
|
|
const btnPos = await p1.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
const scene = game.scene.getScene('GameScene')
|
|
const b = scene.playBtn.getBounds()
|
|
return { x: b.centerX, y: b.centerY }
|
|
})
|
|
await p1.mouse.click(box1.x + btnPos.x, box1.y + btnPos.y)
|
|
|
|
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()
|
|
})
|
|
})
|