# Brainrot Fight

Application clé-en-main pour organiser des duels infinis 1v1 entre brainrots. Les données sont chargées depuis `stealabrainrot_brainrots.jsonl`, les votes mettent à jour un classement ELO persistant dans `elo.csv`, et l'interface React permet de voter au clic ou via les flèches du clavier.

## Prérequis

- Node.js 18 ou supérieur
- Fichiers de données présents à la racine du projet :
  - `stealabrainrot_brainrots.jsonl` (source des brainrots)
  - `elo.csv` (sera créé automatiquement si absent)

## Installation

```bash
npm install
```

## Lancer le projet

- **Développement** : démarre l'API Express (port 3003) et Vite (port 5173).
  ```bash
  npm run dev
  ```
  L'interface se rend sur http://localhost:5173 et proxy les appels `/api` vers le back-end sur 3003.

- **Production** : construit l'interface puis sert l'ensemble via Express (port 3003).
  ```bash
  npm run build
  npm start
  ```

## Scripts complémentaires

- `npm run dev:server` : API Express avec rechargement `nodemon`.
- `npm run dev:web` : interface React via Vite.
- `npm run build:web` : build statique du front.

## Architecture

```
.
├── server/
│   ├── index.js          # Express + routes API + statiques
│   ├── storage.js        # Chargement JSONL, lecture/écriture CSV, mutex
│   ├── pairing.js        # Algorithme de matchmaking (fenêtre ELO, déciles)
│   ├── elo.js            # Calculs ELO (K dynamique, clamp ±400)
│   └── schema.js         # Validation des requêtes et sérialisation
├── web/
│   ├── index.html        # Entrée Vite
│   ├── src/              # App React + composants + API client
│   ├── vite.config.ts    # Proxy / configuration build
│   ├── tailwind.config.js
│   └── postcss.config.js
├── package.json
└── README.md
```

## Flux de données

- Le JSONL est lu au démarrage et normalisé (`id` slugifié).
- Les images distantes sont téléchargées dans `public/brainrots` au démarrage et exposées via `GET /api/images/:id`.
- `elo.csv` possède les colonnes `id,name,elo,matches,wins,losses,updated_at`. Toute entrée absente est initialisée à ELO 1000.
- Les écritures CSV sont atomiques (fichier temporaire + renommage) et protégées par un mutex.

## Algorithme ELO

- E = `1 / (1 + 10^((Rb - Ra)/400))` avec écart clampé à ±400.
- Facteur K : 32 (<50 matchs), 24 (≥50), 16 (≥200).
- Les victoires incrémentent `matches`, `wins`, `losses` et mettent à jour `updated_at`.

## Pairing

1. Choix d'un pivot pondéré par faible nombre de matchs et ancienneté de mise à jour.
2. Recherche d'un adversaire dans une fenêtre ELO ±200, élargie par pas de 100 jusqu'à 800.
3. Interdiction de duels top décile vs bottom décile si l'écart dépasse 400.
4. Évitement des répétitions immédiates (clé de pair unique et TTL).

## API

### GET `/api/next-fight`
Retourne le prochain duel disponible.
```json
{
  "pairId": "uuid",
  "left": { "id": "...", "name": "...", "image": "...", "page": "...", "elo": 1000 },
  "right": { "id": "...", "name": "...", "image": "...", "page": "...", "elo": 1000 }
}
```

### POST `/api/vote`
Corps : `{ "pairId": string, "winnerId": string, "loserId": string }`
Réponse :
```json
{
  "ok": true,
  "winner": { "id": "...", "elo": 1016, ... },
  "loser": { "id": "...", "elo": 984, ... }
}
```
Le `pairId` doit correspondre à un duel récent obtenu via `GET /api/next-fight`.

### GET `/api/images/:id`
Renvoie l'image locale du brainrot (téléchargée au démarrage). Répond avec `404` si aucune image n'est disponible.

### GET `/api/leaderboard`
Retourne le classement complet trié par ELO : `{ "entries": [{ "rank": 1, "id": "...", "elo": 1234, "matches": 42, ... }] }`.

## Personnalisation

- Variables d'environnement :
  - `PORT` : port du serveur Express (3003 par défaut).
  - `BRAINROT_FILE` / `ELO_FILE` : chemins des sources (valeurs par défaut = fichiers racine).
  - `PUBLIC_DIR` : répertoire racine pour les fichiers `/assets` (défaut = `public`).
  - `ASSETS_DIR` : sous-dossier de cache pour les images (`public/brainrots` par défaut).
  
## Accessibilité & UX

- Navigation clavier (flèches ← →).
- Animations 200 ms (pulse gagnant, fade perdant, fade-in des cartes).
- Classement flottant (scrollable) affiché en permanence.
- Préchargement du duel suivant et gestion d'erreurs via toasts.
