Skip to content

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Push Your Luck

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.

Le jeu en deux règles

  1. 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).
  2. 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.

Démarrer

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

Langues, thèmes et identité

  • Deux langues : français sur des URL sans préfixe (/regles), anglais préfixé (/en/rules). Les slugs sont traduits. Toute la copie vit dans src/i18n/fr.ts et src/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.svg et leurs variantes claires/monochromes). npm run logos en exporte les PNG et reconstruit src/app/favicon.ico. Ne retouche jamais un PNG à la main.

Scripts

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

Architecture

  • 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 de src/app/(fr)/ et src/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.

Exploitation

  • Mesure d'audience : Matomo auto-hébergé, chargé uniquement si NEXT_PUBLIC_MATOMO_URL et NEXT_PUBLIC_MATOMO_SITE_ID sont 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é dans dump/ 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.
    1. 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": 999999 ne donne rien : le champ est ignoré.
    2. Origine vérifiée sur les écritures (src/lib/origin.ts) — un POST sans Origin, 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 un curl, qui pose l'en-tête qu'il veut. Aucune vérification d'origine ne le peut.
    3. 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é par x-real-ip, à défaut par la dernière valeur de x-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.
  • Pseudos : src/lib/nameFilter.ts refuse 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.ts vé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é.

Isolation vis-à-vis du loup-garou

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).

⚠️ Un seul conflit possible, en production : le 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.

Déploiement

MYSQL_PASSWORD=… MYSQL_ROOT_PASSWORD=… docker compose -p pushyourluck up -d --build

Les 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).

Le schéma de la base

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_init

Activer la mesure d'audience en production

Les 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=6
docker compose -p pushyourluck up -d --build   # --build est indispensable

Trois garde-fous :

  • le .env n'entre jamais dans l'image (exclu du contexte) — Next le recopie sinon tel quel dans la sortie standalone ;
  • la mesure est coupée hors production, même variables renseignées, pour qu'un npm run dev ne 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.

L'image de production

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.

Contribuer

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.

Licence

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.

About

Jeu de cartes « pousse ta chance » — partie du jour commune, classement quotidien, sans inscription. Next.js, moteur déterministe, scores vérifiés par rejeu serveur.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Used by

Contributors

Languages