Jeu de cartes « pousse ta chance » jouable dans le navigateur, sur pushyourluck.net. Une partie du jour identique pour tous, un classement quotidien, aucune inscription.
- Chaque carte tirée gonfle le pot ; cinq bombes sont dans le paquet. Une bombe fait perdre le pot et une vie (trois vies par partie).
- Encaisser retire définitivement du paquet les cartes tirées. Comme une manche encaissée ne contient jamais de bombe, ce sont toujours de bonnes cartes qui partent : le paquet devient plus dangereux à chaque encaissement.
Une prime d'enchaînement de +22 % par carte tirée (cumulative) récompense les
manches longues. La valeur BANK_GROWTH a été calibrée par simulation : voir
scripts/simulate.ts.
npm run db:up # MySQL 8.4 sur le port 3307 (données dans ./mysql-data-dev)
npx prisma generate # prérequis : le client Prisma est généré, pas installé
npm run db:migrate # applique les migrations (crée la table Score)
npm run dev # http://localhost:3001
# Une seule fois, pour pouvoir lancer les tests fonctionnels et visuels :
npm run db:test:setup
npx playwright install chromium- Deux langues : français sur des URL sans préfixe (
/regles), anglais préfixé (/en/rules). Les slugs sont traduits. Toute la copie vit danssrc/i18n/fr.tsetsrc/i18n/en.ts; l'anglais est typé d'après le français, donc une clé oubliée casse le build. - Deux thèmes clair et sombre, avec suivi du réglage système et bascule
manuelle persistée. Les tokens sont dans
src/app/globals.css. - Logos : les SVG de
public/sont la source (logo.svg,logo-mark.svget leurs variantes claires/monochromes).npm run logosen exporte les PNG et reconstruitsrc/app/favicon.ico. Ne retouche jamais un PNG à la main.
| Commande | Rôle |
|---|---|
npm run dev |
serveur de développement sur le port 3001 |
npm run build / npm start |
build et serveur de production (port 3001) |
npm run sim |
simulation d'équilibrage headless (milliers de parties) |
npm run smoke |
test bout en bout de la soumission de score sur un serveur local |
npm run logos |
réexporte les PNG et le favicon depuis les SVG de public/ |
npm test |
les trois couches — obligatoire avant toute livraison |
npm run test:unit |
tests unitaires (Vitest), sans navigateur ni base |
npm run test:functional |
parcours réels (Playwright + base de test) |
npm run test:visual |
captures de référence et débordements mobiles |
npm run test:visual:update |
régénère les captures après un changement voulu |
npm run db:test:setup |
repose la base pushyourluck_test et y joue les migrations |
npm run backup |
sauvegarde manuelle de la base dans dump/ |
npm run seed:history |
remplit la base de DEV d'un historique fictif (calendrier) |
npm run db:up / db:down |
base de développement |
npm run db:migrate / db:studio |
migrations et exploration |
src/games/push-your-luck/— moteur pur, sans DOM ni React.engine.ts(état et actions),cards.ts(catalogue de cartes en données),rng.ts(aléatoire déterministe),replay.ts(rejeu d'une partie).src/components/game/— l'interface, seule à connaître React.src/i18n/— dictionnaires, table des URL traduites, aides de formatage.src/views/— une vue par page, partagée par les deux langues ; les fichiers desrc/app/(fr)/etsrc/app/(en)/ne font que les appeler avec leur langue. Deux layouts racine (un par groupe) donnent le bon<html lang>sans préfixer le français ni rediriger.src/content/changelog.ts— notes de version, bilingues dans la même entrée.src/app/api/scores/— classement. Le score n'est jamais lu depuis la requête : le serveur rejoue la suite d'actions envoyée par le client depuis la graine du jour et recalcule le résultat lui-même.
Le déterminisme du moteur est la clef de voûte : il donne la partie du jour commune, la validation anti-triche par rejeu, et l'équilibrage par simulation.
- Mesure d'audience : Matomo auto-hébergé, chargé uniquement si
NEXT_PUBLIC_MATOMO_URLetNEXT_PUBLIC_MATOMO_SITE_IDsont renseignés, et configuré sans cookie (disableCookies). C'est cette configuration qui dispense de bandeau de consentement — à condition que l'instance Matomo anonymise bien les adresses IP de son côté. - Sauvegardes : le processus Next lance un cron (
src/instrumentation.ts→src/server/cron.ts) qui, chaque nuit à 03h15 heure de Paris, écrit un dump SQL compressé dansdump/et supprime les scores de plus d'un an — la durée annoncée dans la politique de confidentialité. Les sept dernières sauvegardes sont conservées. Le cron est désactivé hors production. - Restauration :
gunzip -c dump/pushyourluck-….sql.gz | docker exec -i pyl_db mysql -upushyourluck -p pushyourluck - API des scores : trois défenses distinctes, qui ne se remplacent pas.
- Le score n'est jamais lu depuis la requête — le serveur rejoue les
actions envoyées depuis la graine du jour et recalcule (
replay.ts). Annoncer"score": 999999ne donne rien : le champ est ignoré. - Origine vérifiée sur les écritures (
src/lib/origin.ts) — un POST sansOrigin, ou venant d'un autre domaine, est refusé (403). Cela protège d'un site tiers qui ferait poster ton visiteur à son insu depuis son navigateur ; cela n'arrête pas uncurl, qui pose l'en-tête qu'il veut. Aucune vérification d'origine ne le peut. - Quota par appelant (
rateLimit.ts) — 20 envois par quart d'heure. C'est la seule chose qui borne un robot jouant de vraies parties : le moteur est déterministe et public, donc un score optimal est calculable. L'appelant est identifié parx-real-ip, à défaut par la dernière valeur dex-forwarded-for— jamais la première, qui vient du client. La lecture du classement (GET) reste publique : elle est affichée sur le site, la fermer ne protégerait rien.
- Le score n'est jamais lu depuis la requête — le serveur rejoue les
actions envoyées depuis la graine du jour et recalcule (
- Pseudos :
src/lib/nameFilter.tsrefuse côté serveur les insultes, les usurpations (admin,staff…) et leurs contournements courants (chiffres à la place des lettres, lettres espacées, répétitions).npx vitest run src/lib/nameFilter.test.tsvérifie autant les refus que les faux positifs — un filtre qui bloque « Cassandra » fait plus de dégâts qu'un gros mot. - Une instance à la fois : le cron tourne dans le processus applicatif. Le jour où l'app passera en plusieurs instances, il faudra un verrou partagé.
Les deux projets cohabitent sur la même machine sans se marcher dessus :
projet Docker pushyourluck, réseau pyl_network, conteneurs pyl_*,
MySQL sur 3307 (le loup-garou utilise Postgres 5433 et Redis 6379).
docker-compose.yml du
loup-garou mappe déjà l'hôte 3001 (werewolf_node_blue). Si les deux stacks
de production tournent sur la même machine, poser PYL_HOST_PORT=3005 dans le
.env du serveur.
MYSQL_PASSWORD=… MYSQL_ROOT_PASSWORD=… docker compose -p pushyourluck up -d --buildLes deux mots de passe sont obligatoires : le dépôt est public, aucune
valeur de repli n'y figure et la stack refuse de démarrer sans eux. Données
MySQL dans ./mysql-data, sauvegardes dans ./dump (bind mounts à la racine).
Les migrations sont jouées par l'entrypoint du conteneur applicatif
(docker-entrypoint.sh), avant que Next ne démarre. Rien à lancer à la main :
un déploiement applique ce qui est en attente, et migrate deploy ne fait rien
quand tout est déjà appliqué.
Si une migration échoue, le conteneur s'arrête au lieu de servir
l'application. C'est délibéré : Next démarre parfaitement sur une base sans
table, le healthcheck passe, et la panne n'apparaît qu'à la première requête
d'un visiteur — « The table Score does not exist ». Mieux vaut un conteneur
qui refuse de démarrer qu'un site qui répond 500 en silence.
La CLI Prisma et son moteur de schéma vivent dans /opt/prisma, à l'écart du
node_modules de la sortie standalone : les fusionner écraserait des paquets
@prisma/* dont l'application a besoin pour servir les requêtes. Ils coûtent
252 Mo dans l'image — la CLI embarque Studio, effect et @electric-sql, et
en retirer ne serait-ce que @prisma/studio-core fait échouer migrate deploy
(son build les importe tous au chargement).
Base déjà peuplée sans historique de migration (cas d'un schéma posé jadis
par db push) : la marquer comme déjà appliquée plutôt que la rejouer, sans
quoi migrate deploy échouera en voulant recréer une table existante.
DATABASE_URL="mysql://…" npx prisma migrate resolve --applied 0_initLes variables NEXT_PUBLIC_* sont inlinées dans le bundle du navigateur au
moment de la construction : les passer au démarrage du conteneur n'a aucun
effet. Elles doivent être présentes dans le .env du serveur avant le
docker compose build, qui les transmet en arguments de construction :
# .env du serveur
NEXT_PUBLIC_SITE_URL=https://pushyourluck.net
NEXT_PUBLIC_MATOMO_URL=https://matomo.leoderoin.fr
NEXT_PUBLIC_MATOMO_SITE_ID=6docker compose -p pushyourluck up -d --build # --build est indispensableTrois garde-fous :
- le
.envn'entre jamais dans l'image (exclu du contexte) — Next le recopie sinon tel quel dans la sortiestandalone; - la mesure est coupée hors production, même variables renseignées, pour
qu'un
npm run devne compte pas de visites dans les chiffres réels ; - la CSP ajoute l'hôte Matomo automatiquement à partir de la même variable : changer d'instance ne demande aucune modification de code.
Vérifier après déploiement, dans cet ordre — les deux premiers points sont servis par le runtime et peuvent être verts alors que rien n'est compté, puisque l'identifiant du site part, lui, dans le bundle à la construction :
# 1. La CSP mentionne bien l'instance
curl -sI https://pushyourluck.net | grep -i content-security-policy
# 2. L'identifiant de site existe côté Matomo — 200 attendu.
# Un site inconnu répond 400 et la visite est jetée en silence : ni la
# console du navigateur ni la CSP ne le signalent.
curl -so /dev/null -w '%{http_code}\n' \
"https://matomo.leoderoin.fr/matomo.php?idsite=6&rec=1&url=https%3A%2F%2Fpushyourluck.net%2F&action_name=probe&rand=1&apiv=1"
# 3. L'identifiant réellement inliné dans le bundle est celui attendu
curl -s https://pushyourluck.net | grep -oE '/_next/static/chunks/[^"]+\.js' | sort -u \
| while read -r c; do curl -s "https://pushyourluck.net$c"; done | grep -o 'MATOMO_SITE_ID[^,]*,[^,]*,"[0-9]*"'Puis une visite doit apparaître dans Matomo en temps réel.
237 Mo, contre 322 Mo pour un Dockerfile naïf. Trois décisions :
| Levier | Gain |
|---|---|
alpine nue + binaire Node recopié, au lieu de node:26-alpine |
−59 Mo |
| Binaire Node débarrassé de ses symboles de débogage | −20 Mo |
sharp et @img exclus du traçage (aucun next/image dans le projet) |
−19 Mo |
Ce qui reste est incompressible : le binaire Node pèse 128 Mo à lui seul, ICU complet inclus — indispensable au formatage des dates en français.
Servir le build depuis nginx n'est pas possible : l'application a besoin de Node à la requête pour l'API de scores (validation anti-triche par rejeu), pour le classement rendu à la demande, et pour le cron de sauvegarde. Nginx a sa place devant — TLS, cache des fichiers statiques, limitation de débit — mais pas à la place.
Les contributions sont bienvenues — lis CONTRIBUTING.md d'abord, en particulier la règle « une fonctionnalité = un test ». Pour une faille de sécurité, passe par SECURITY.md, jamais par une issue publique.
MIT. La licence porte sur le code : elle ne concède aucun droit sur le nom « Push Your Luck » ni sur l'identité visuelle du site.