Moteur de règles d'un jeu de course en cartes, pour 2 à 6 joueurs. TypeScript strict, aucune dépendance, aucun accès au réseau, au DOM ni à un quelconque service.
Chaque joueur accumule des bornes jusqu'à 1000, gêne ses adversaires avec des attaques (feu rouge, panne d'essence, crevaison…) et s'en protège avec des parades et des bottes. La partie se joue en manches successives.
Création originale : les règles sont réimplémentées de zéro. Les mécaniques d'un jeu ne sont pas protégeables, et ce dépôt ne reprend ni nom commercial, ni illustration, ni texte d'aucune édition existante.
Un réducteur pur. On lui donne un état et une action, il rend un nouvel état — ou un refus motivé.
import { legalActions, reduce, scoreRound, startRound } from './src/index.js';
let state = startRound({
players: [
{ id: 'p1', teamId: 't1' },
{ id: 'p2', teamId: 't2' },
],
seed: 42,
});
const result = reduce(state, { type: 'draw', player: state.currentPlayer });
if (result.ok) {
state = result.state;
} else {
console.log(result.error.code); // par exemple 'notYourTurn'
}- Pureté. Aucun
Math.random(), aucunDate.now(), aucune entrée-sortie. Tout l'aléatoire passe par un PRNG seedé dont l'état, un entier 32 bits, vit dans l'état de la manche. Une partie se rejoue donc à l'identique, et un serveur peut hiberner entre deux coups sans rien perdre. - Aucune mutation. L'état d'entrée n'est jamais modifié. Les manches de test le gèlent récursivement avant chaque appel : la moindre écriture en place lèverait une exception.
- Aucune exception pour une action illégale.
reducerend{ ok: false, error }avec un motif structuré, exploitable pour griser une carte ou afficher une explication. - Une seule source de vérité.
legalActionsetreducepartagent les mêmes prédicats. Un client qui grise les cartes et un serveur qui valide ne peuvent pas diverger. - Information cachée.
playerViewretire les mains adverses et le contenu de la pioche. Un serveur n'a jamais besoin d'envoyer autre chose sur le réseau.
| Fichier | Rôle |
|---|---|
cards.ts |
Composition du paquet (106 cartes) et relations attaque / parade / botte |
types.ts |
Types du domaine, tous en lecture seule |
rules.ts |
Réducteur, prédicats de légalité, legalActions, mise en place |
scoring.ts |
Décompte de fin de manche, barème configurable |
view.ts |
Projection vers ce qu'un joueur a le droit de voir |
rng.ts |
PRNG seedé (mulberry32) et mélange Fisher-Yates |
pnpm install
pnpm test # 116 tests
pnpm typecheckLes tests couvrent la composition du paquet, chaque règle contre-intuitive prise isolément, le barème bonus par bonus, la non-fuite d'information, et des manches complètes jouées de bout en bout par deux robots déterministes — dont les invariants (conservation des 106 cartes, taille de main, immutabilité) sont vérifiés après chaque action.
Ce sont les points sur lesquels une réimplémentation se trompe le plus souvent.
- Les deux piles sont indépendantes. Une limite de vitesse ne bloque pas : elle plafonne la valeur des bornes jouables, et coexiste avec l'état de la pile de bataille. Elle est donc jouable sur un joueur déjà arrêté par un feu rouge, ou qui n'a pas encore démarré.
- Le Véhicule prioritaire dispense définitivement de feu vert, mais pas de parade : son détenteur reste arrêté par une panne, une crevaison ou un accident, et repart tout seul une fois l'attaque parée.
- Parer ne fait pas repartir. Après une réparation, la pile de bataille n'est pas roulante : il faut encore un feu vert — sauf pour le détenteur du Véhicule prioritaire.
- La fenêtre de coup-fourré ne s'ouvre que si la cible détient la botte. Une fenêtre systématique révélerait, par son absence de déclenchement, que la cible n'a rien.
- Le total doit tomber exactement sur 1000. Toute pose qui dépasserait est refusée.