SAÉ S4 · TP JSAE · IUT de Lille

Bug of Thrones

Shoot'em up multijoueur en temps réel : deux joueurs défendent un château contre des vagues d'ennemis de plus en plus rapides. Architecture client / serveur / shared strictement séparée, serveur autoritaire à 60 FPS, 30 tests automatisés.

TypeScript Socket.IO 4 Canvas HTML5 Node.js ≥ 20 Vite 7 SQLite Docker / Podman
Capture d'écran Bug of Thrones

Ce qui distingue ce projet

🖥️

Serveur autoritaire

Toute la logique (physique, collisions, spawn, score) est calculée côté serveur à 60 FPS. Zéro triche possible.

🎮

Rendu pur client

Le client envoie uniquement les inputs et affiche l'état reçu du serveur via Canvas HTML5.

🏗️

Architecture tripartite

src/client, src/server et src/shared strictement séparés — aucune duplication de logique.

🧪

30 tests automatisés

Tests d'intégration Socket.IO avec vrai serveur en mémoire, tests unitaires et tests de persistance SQLite.

Stack technique

TypeScript
Client + serveur, même base de code
Socket.IO 4
Transport temps réel bidirectionnel
Canvas HTML5
Rendu pur côté client
Node.js ≥ 20
Serveur + tsx (pas de build serveur)
Vite 7
Bundler client ultra-rapide
SQLite (better-sqlite3)
Leaderboard persistant
Node test runner
30 tests automatisés natifs
Docker / Podman
Dockerfile + podman-compose tout-en-un

Architecture tripartite

src/client/

  • Rendu Canvas HTML5
  • Gestion des inputs clavier / souris
  • Vues : lobby, game over, leaderboard, crédits
  • Aucune logique de jeu

src/server/

  • Boucle de jeu à 60 FPS (setInterval)
  • Managers : Movement, Fireball, Enemy, Collision, Room, Score
  • Handlers Socket.IO
  • Serveur autoritaire

src/shared/

  • Types partagés client ↔ serveur
  • LobbyRoomSummary, LeaderboardEntry
  • WORLD_WIDTH / WORLD_HEIGHT
  • Contrats d'interface communs

Le serveur stocke le dernier input reçu et l'applique dans le tick — synchronisation cohérente même sous latence.

4 types d'ennemis

🐴
Grunt (pégase)
Basique
HP : 4 Déplacement : Linéaire

L'ennemi de base, prévisible mais nombreux.

🦸
Scout (héros)
Rapide
HP : 2 Déplacement : Rapide

Peu résistant mais sa vitesse le rend difficile à cibler.

🐦
Waver (oiseau)
Sinusoïdal
HP : 5 Déplacement : Sinus

Trajectoire sinusoïdale imprévisible, le plus difficile à toucher.

👹
Goblin Giant
Tank
HP : 6 Attaque : Lances

Marche au sol et tire des lances — menace directe sur le château.

30 tests automatisés

Fichier de test Ce qui est couvert
CollisionTest Détection AABB joueur / ennemi
MovementTest Déplacement clavier avec inertie
FireballTest Création de fireball en mode automatique
ServerLogicTest Invincibilité, HP multi-tirs, bonus, direction clic, bounds, lance multi-joueur
GameUtilsTest computeDifficulty (bornes, clamp), sanitizeUsername
RoomServiceTest Room solo privée, suppression de room vide
SocketIntegrationTest Waiting screen, déconnexion partenaire, room solo masquée du lobby
DbTest Persistance SQLite (save + top N)

Analyse réflexive

Ce que j'ai appris

Architecture distribuée

Séparer nettement client, serveur et contrats partagés dès le départ évite la duplication et les désynchronisations réseau. J'ai compris que les types partagés dans src/shared/ sont le vrai contrat de l'application.

Ce que j'ai appris

Serveur autoritaire

Stocker le dernier input et l'appliquer dans le tick serveur rend la simulation cohérente sous toute latence. Une leçon directement issue d'un bug où les inputs s'accumulaient et le personnage "sautait" à la reconnexion.

Difficultés rencontrées

Déconnexion & état cohérent

Gérer la déconnexion inattendue d'un joueur (réseau coupé, onglet fermé) sans corrompre l'état du serveur a été la partie la plus délicate. Il fallait nettoyer la room, notifier l'autre joueur et revenir au lobby proprement — chaque cas manqué créait une room fantôme.

Difficultés rencontrées

Courbe de difficulté

Calibrer computeDifficulty (spawn rate, vitesse des ennemis) pour que le jeu soit difficile sans être injuste a demandé de nombreuses parties de test. Les tests unitaires sur les bornes m'ont aidé, mais l'équilibre final reste subjectif et itératif.

Ce que je ferais différemment

Reconnexion & snapshots

J'implémenterais un mécanisme de reconnexion (token de session) pour reprendre une partie interrompue. J'utiliserais aussi un format binaire plus compact pour les snapshots d'état envoyés à 60 FPS, pour réduire la bande passante sur les parties longues.

Ce que je ferais différemment

Responsabilités des managers

Certains managers serveur (CollisionManager, EnemyManager) ont grandi au fil du projet et mélangent plusieurs responsabilités. Je les découperais plus finement dès le départ pour que chaque manager soit testable indépendamment sans setup complexe.

Soft skills — illustrés par des situations concrètes

Travail en équipe

Contrats partagés comme langage d'équipe

Définir les types dans src/shared/ dès le début a été notre principal outil de coordination : plutôt que de s'expliquer verbalement comment communiquent client et serveur, on a écrit des types TypeScript que tout le monde pouvait lire. J'ai compris que dans une équipe, le code bien structuré est la meilleure documentation.

Autonomie

Boucle de jeu et tests sans encadrement

J'ai implémenté les managers serveur (Movement, Collision, Score) et l'ensemble des tests d'intégration Socket.IO de façon autonome, en m'appuyant uniquement sur les types partagés et la spécification du projet. Quand je bloquais, je cherchais d'abord seul avant de consulter l'équipe — ce réflexe d'autonomie s'est renforcé tout au long du projet.

Adaptabilité

Gérer l'inattendu en cours de projet

La déconnexion propre d'un partenaire n'était pas prévue dans la conception initiale. Quand on a réalisé que ça créait des rooms fantômes, j'ai adapté le code existant sans tout réarchitecturer : identifier le minimum à modifier, implémenter, tester. Cette capacité à traiter l'imprévu sans tout recommencer est une compétence clé en équipe.

Développé à trois

A
Aubin Cambier
40 %
Architecture, serveur, tests
A
Aliocha Deflou
40 %
Architecture, client, gameplay
S
Sewan Hamida
20 %
Sprites pixel-art, assets