Kevin L'Hereec 34d1bbe37f fix: une carte Événement jouée pouvait rester affichée comme jouable en main
Cause : this.selectedEventCard gardait une référence DIRECTE vers un objet
Phaser (ItemBoardToken), régulièrement détruit et recréé par
_refreshEventHandAffordability()/_buildEventHand() à chaque game-state.
Cette référence devenait alors obsolète (pointant vers un objet détruit),
et l'état de verrouillage "carte en cours de jeu" était géré par deux
chemins de mutation distincts (reconstruction de la main basée sur
myEventHand seul, ET synchronisation séparée sur pendingEventPlayId) qui
pouvaient se désynchroniser selon l'ordre exact des événements réseau :
une carte déjà envoyée au serveur (play-event émis) pouvait alors
réapparaître visuellement "jouable" dans la main après une reconstruction
intermédiaire qui ignorait le verrouillage réel.

Fix : refactor complet du pilotage de la main d'événements côté GameScene :

- this.selectedEventCard (référence Phaser) remplacé par
  this.selectedEventId (simple eventId, jamais périmé même après
  destruction/recréation des jetons).
- Fusion de _buildEventHand + _refreshEventHandAffordability en une seule
  fonction _renderEventHand(eventIds), SEULE responsable de construire
  this.eventCards — élimine le second chemin de mutation qui pouvait
  diverger.
- Dans le listener 'game-state', selectedEventId est désormais TOUJOURS
  resynchronisé sur state.pendingEventPlayId (source de vérité serveur)
  AVANT de décider si la main doit être reconstruite, et la clé de diff
  inclut maintenant ce verrouillage en plus de la composition de la main —
  auparavant, un changement de pendingEventPlayId seul (main inchangée) ne
  déclenchait aucune reconstruction, laissant l'ancien état affiché.
- _cancelTargeting() reconstruit désormais aussi la main pour lever tout
  verrouillage visuel resté affiché après une annulation.

Fix connexe dans ItemBoardToken.destroy() : le minuteur d'OUVERTURE
(_showTimer) n'était jamais annulé à la destruction (seul _hideTimer
l'était) — un jeton détruit pendant qu'un survol venait de démarrer
pouvait déclencher _showExpanded() sur un objet déjà détruit une fois le
délai écoulé, une source de bug supplémentaire pour des cartes "fantômes".

54 tests unitaires client + 166 tests unitaires serveur toujours au vert.
2026-08-24 16:51:03 +02:00

Regnum

Jeu de cartes en ligne multijoueur (2-4 joueurs). Collecte des points en jouant des cartes de royaume simultanément pendant X tours.

Stack

  • Frontend : Vue 3 + Phaser 3 + Pinia
  • Backend : Node.js + Express + Socket.io
  • Temps réel : WebSockets

Structure

regnum/
├── client/          # Vue 3 + Phaser 3
│   ├── src/
│   │   ├── views/   # HomeView, LobbyView, GameView
│   │   ├── game/    # Scènes Phaser (Boot, Game) + objets + items/buildings/events
│   │   ├── stores/  # Pinia store
│   │   ├── services/# Socket.io client
│   │   └── utils/   # Fonctions pures partagées (HUD, etc.)
│   └── test/unit/   # Tests unitaires Vitest (store, utils)
├── server/          # Node.js
│   ├── src/
│   │   ├── game/    # Room, RoomManager, cards, items, buildings, events
│   │   └── index.js # Serveur Express + Socket.io
│   └── test/game/   # Tests unitaires node:test (moteur de jeu)
└── e2e/             # Tests d'intégration Playwright (client+serveur réels)
    └── tests/

Lancement

Serveur

cd server
npm install
npm run dev

Client

cd client
npm install
npm run dev

Ouvrir http://localhost:5173

Tests

Le projet a 3 niveaux de tests, à exécuter indépendamment :

1. Tests unitaires — serveur (moteur de jeu)

Logique pure de Room.js / cards.js / items.js (règles, gains, achats, effets permanents), avec le test runner natif de Node.js :

cd server
npm test            # exécution unique
npm run test:watch  # mode watch

2. Tests unitaires — client (store, utilitaires)

Store Pinia et fonctions pures du HUD (hudUtils.js), avec Vitest :

cd client
npm run test:unit        # exécution unique
npm run test:unit:watch  # mode watch

3. Tests d'intégration E2E (client + serveur réels)

Parcours complets (créer/rejoindre une salle, jouer des tours, acheter un objet, fin de partie) pilotés dans un vrai navigateur Chromium via Playwright, avec de vrais clients Socket.io. webServer dans e2e/playwright.config.js démarre automatiquement le serveur et le client si besoin.

cd e2e
npm install                # première fois seulement
npx playwright install chromium  # première fois seulement, si besoin
npm test                   # exécution unique
npm run test:ui            # mode interactif (debug)

Règles

Principe général

  • 2 à 4 joueurs par partie, chacun avec son propre deck des mêmes 12 personnages (seule la couleur d'équipe change).
  • Chaque joueur commence avec une main de 4 cartes ; le reste du deck (8 cartes) est pioché une carte par tour.
  • La partie dure 12 tours. Elle se termine avant si tous les joueurs n'ont plus ni carte en main ni carte à piocher.
  • Chaque tour, tous les joueurs choisissent simultanément une carte (cachée), puis les cartes sont révélées et résolues en même temps.

Les 4 jauges

Chaque personnage possède 4 attributs (valeurs de 1 à 6), qui correspondent à 4 jauges cumulées par le joueur pendant toute la partie :

Jauge Icône
Nourriture 🟡
Guerre 🔴
Santé 🟢
Diplomatie 🔵

Résolution d'un tour

Pour chaque attribut, un joueur compare la valeur de sa carte jouée à celle de son voisin de gauche (ordre des joueurs dans la partie, en boucle) :

gain = max(0, ma_valeur - valeur_du_voisin_de_gauche)

Le gain (jamais négatif) est ajouté à la jauge correspondante du joueur. Le score total d'un joueur est la somme de ses 4 jauges.

Les 12 personnages

Reine, Prince, Boucher, Médecin, Apothicaire, Fou du Roi, Assassin, Paysan, Bourreau, Chevalier, Marchand, Évêque — chacun avec un profil d'attributs différent (ex : le Paysan est fort en Nourriture mais faible en Diplomatie, l'Assassin est très fort en Guerre mais faible partout ailleurs).

Objets achetables

Pendant la phase de sélection, un joueur peut dépenser des points d'une jauge pour acheter un objet, qui procure un effet permanent pour le reste de la partie tant qu'il le possède. Chaque objet coûte 3 points et n'existe qu'en un seul exemplaire pour toute la partie (premier arrivé, premier servi) :

Objet Jauge liée Effet
Trébuchet 🔴 Guerre Double le gain de Guerre à chaque tour
Charrue 🟡 Nourriture Double le gain de Nourriture à chaque tour
Trousse du Médecin 🟢 Santé Garantit +2 en Santé à chaque tour, quel que soit le résultat
Sceau Royal 🔵 Diplomatie Copie le meilleur gain de Diplomatie parmi tous les autres joueurs

En cas de conflit (plusieurs joueurs achètent le même objet le même tour), c'est le joueur le plus proche du "premier joueur" du tour (rôle qui tourne entre les joueurs à chaque tour) qui l'obtient ; les autres conservent leurs points.

Bâtiments achetables

Comme les objets, mais un bâtiment coûte des points sur deux jauges à la fois et garantit un minimum sur chacun de ses deux attributs à chaque tour, avec un bonus de synergie (+1 supplémentaire) si l'objet lié au même attribut est aussi possédé par le joueur. Un seul exemplaire de chaque bâtiment existe pour toute la partie, avec la même règle de priorité que les objets en cas de conflit d'achat.

Bâtiment Coût Effet de base Synergie
Église 3 🟢 Santé + 2 🔵 Diplomatie +1 Santé et +1 Diplomatie garantis chaque tour +1 Santé supplémentaire si la Trousse du Médecin est possédée ; +1 Diplomatie supplémentaire si le Sceau Royal est possédé
Château Royal 3 🟡 Nourriture + 2 🔴 Guerre +1 Nourriture et +1 Guerre garantis chaque tour +1 Nourriture supplémentaire si la Charrue est possédée ; +1 Guerre supplémentaire si le Trébuchet est possédé

Cartes Événement

En plus de sa carte personnage, un joueur peut piocher et jouer des cartes Événement à chaque tour :

  • Pioche : en phase de sélection, un joueur peut acheter une pioche pour un coût fixe de 1 point dans chacune des 4 jauges, une seule tentative par tour. La carte piochée provient d'un deck partagé de 18 cartes (3 exemplaires de chacun des 6 événements ci-dessous), mélangé au démarrage de la partie et commun à tous les joueurs.
  • Main d'événements : les cartes piochées mais pas encore jouées s'accumulent dans une main personnelle limitée à 2 cartes. Un joueur choisit librement, tour par tour, de jouer l'une de ses cartes (au maximum une par tour) ou de la garder.
  • Résolution : toutes les cartes Événement jouées un même tour sont résolues avant le calcul des gains des cartes personnage (les jauges reflètent donc déjà leurs effets au moment de la comparaison des personnages). Une carte jouée est toujours défaussée après résolution, qu'elle ait ou non atteint son plein effet.
Événement Effet Synergie
Récolte exceptionnelle +2 Nourriture +1 Nourriture supplémentaire avec la Charrue
Épidémie Tous les joueurs perdent 1 Santé Avec la Trousse du Médecin : un adversaire choisi perd 2 Santé à la place. Avec l'Église (prioritaire sur la Trousse) : tous les adversaires perdent 2 Santé à la place
Levée des troupes +2 Guerre, -1 Diplomatie +2 Guerre supplémentaires avec le Trébuchet (soit 4 Guerre au total)
Accord royal +1 Diplomatie pour soi ; un joueur choisi gagne 1 Diplomatie ; un joueur choisi (peut être différent) perd 1 Diplomatie +1 Diplomatie supplémentaire avec le Sceau Royal
Complot découvert Jusqu'à la fin du tour, annule pour son porteur les effets négatifs des autres cartes Événement jouées ce tour —
Grande fête au château +1 Nourriture, +1 Diplomatie Pioche 1 carte Événement supplémentaire avec le Château Royal

Fin de partie

Le joueur avec le score total (somme des 4 jauges) le plus élevé après 12 tours remporte la partie.

S
Description
Jeu de cartes en ligne - 4 joueurs - collecte de points par royaume
Readme
117 MiB
0 Stars 1 Watchers 0 Forks
Languages
JavaScript 95.5%
Vue 4%
Dockerfile 0.4%
HTML 0.1%