L'objectif
Je voulais un endroit pour les petits jeux que je construis, où jouer avec un ami tient en un clic et non un téléchargement. Cela demandait de vrais comptes, un lobby qui se met à jour en direct, et des parties en ligne qui paraissent justes. Il fallait aussi que ça coûte presque rien à faire tourner tant que c'est petit.
La structure
Un seul Worker sert le site statique et une petite API. Tout ce qui doit garder un état vit dans un Durable Object : une instance unique d'une classe que Cloudflare garde active, avec son propre stockage.
- Hub est une instance unique avec sa propre base de données SQLite. Elle contient les comptes, les sessions, le lobby en direct, l'historique des parties et les résultats.
- Room est créé pour chaque code à 5 caractères. Il relaie les messages entre exactement deux sockets, l'hôte et l'invité. Un troisième socket se fait dire que la salle est pleine.
- Arena fait tourner les parties de Knife Duels, du 1v1 jusqu'au 4v4.
Du défi au match
- Vous ouvrez l'onglet Joueurs. Votre navigateur garde un WebSocket ouvert vers le Hub, qui indique à tout le monde qui est en ligne.
- Vous défiez quelqu'un. Cette personne le voit tout de suite et accepte ou refuse.
- En cas d'acceptation, le Hub crée un code de salle et envoie le feu vert aux deux joueurs.
- Les deux jeux rejoignent cette Room, qui relaie les entrées entre eux.
- Quand le match se termine, les jeux des deux joueurs rapportent le résultat. Il ne compte que s'ils sont d'accord.
Le socket du lobby utilise l'API d'hibernation des WebSockets, si bien qu'un lobby calme ne garde pas le Hub réveillé, ni facturé, pendant que les gens restent simplement sur la page.
Garder ça équitable
Faire tourner chaque match sur le serveur coûte trop cher pour un petit site, alors j'ai plutôt réparti la confiance. Dans Oathsteel, le duel à l'épée, l'hôte décide des coups et des rounds, chaque joueur possède son propre mouvement, et l'autre combattant est piloté par des entrées relayées. Le mouvement reste réactif des deux côtés, et un camp a le dernier mot sur tout ce qui détermine un vainqueur.
Les résultats n'atteignent le classement que pour les matchs faits dans le lobby, et seulement quand les jeux des deux joueurs rapportent le même résultat. Knife Duels a plus de joueurs par match, donc un résultat compte une fois que la majorité des joueurs humains est d'accord dessus.
Des comptes faits proprement
Les mots de passe sont hachés avec PBKDF2-SHA256 à 100 000 itérations. Les jetons de session sont eux aussi stockés hachés, si bien qu'une base de données volée ne donnerait accès à aucun compte.
Tester le multijoueur
Room et Hub ont des vérifications automatisées qui tournent contre un Worker local : 8 pour le relais, et 21 pour les comptes, le lobby et les résultats. Knife Duels garde sa logique de jeu séparée de three.js et de la page, si bien qu'elle peut être testée unitairement à part, avec 178 tests.
Pour l'épreuve réelle, un script fait passer deux navigateurs headless à travers un duel en ligne complet. Le hic, c'est que deux navigateurs à rendu logiciel sur une même machine se privent l'un l'autre, donc ils tournent bien en dessous de la vitesse réelle et chaque attente du test doit être généreuse.
Ce que j'en ai retenu
- Les Durable Objects rendent l'état du multijoueur facile à raisonner : un objet, un seul endroit où vit la vérité.
- Ajouter un jeu tient en un dossier et une ligne dans une liste, ce qui explique pourquoi le hub continue de grandir.
- Pas d'étape de build et un three.js auto-hébergé signifient que le site se déploie en quelques secondes et n'a rien qui puisse casser entre deux versions.