tetris fonctionnel sans bug
This commit is contained in:
@@ -1,348 +0,0 @@
|
||||
# Transcendence
|
||||
|
||||
---
|
||||
|
||||
## Configuration — Fichier `.env`
|
||||
|
||||
```env
|
||||
POSTGRES_PASSWORD=coucou
|
||||
JWT_SECRET=superlongsecretkeyatleast32characterspleasenevercommitthis
|
||||
POSTGRES_DB=database
|
||||
POSTGRES_HOST=database
|
||||
POSTGRES_USER=user
|
||||
|
||||
GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
GITHUB_CALLBACK_URL=http://localhost:8080/api/auth/github/callback
|
||||
```
|
||||
|
||||
> Les variables `GITHUB_*` sont à générer sur [github.com/settings/applications/new](https://github.com/settings/applications/new)
|
||||
|
||||
---
|
||||
|
||||
## Gestion des amitiés (PostgreSQL)
|
||||
|
||||
| Statut | Signification |
|
||||
|-------------|----------------------------|
|
||||
| `pending` | Demande envoyée |
|
||||
| `accepted` | Amis |
|
||||
| `blocked` | Utilisateur bloqué |
|
||||
| `rejected` | Demande refusée |
|
||||
|
||||
---
|
||||
|
||||
## Ressources
|
||||
|
||||
- [Documentation PostgreSQL](https://www.postgresql.org/docs/)
|
||||
- [Autoriser les OAuth Apps — GitHub](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps)
|
||||
- [Créer une OAuth App — GitHub](https://docs.github.com/fr/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||
|
||||
---
|
||||
|
||||
## Journal des modifications
|
||||
|
||||
### BACKEND
|
||||
|
||||
| Date | Description |
|
||||
|-------|-------------|
|
||||
| 17/01 | Ajout du service/route pour le système de `game_room` — création/rejoindre une room, destruction automatique si vide, liste des rooms joignables et des joueurs avec scores et état |
|
||||
| 21/01 | Ajout du service/route pour le système d'avatar — changement, suppression, et récupération de l'avatar |
|
||||
|
||||
### DATABASE
|
||||
|
||||
| Date | Description |
|
||||
|-------|-------------|
|
||||
| 17/01 | Ajout des tables `game_rooms`, `game_players`, `game_rounds`, `words` — nom/statut/paramètres de game, joueurs/scores/rôles, historique des rounds, liste des mots |
|
||||
| 21/01 | Ajout de `avatar_url` dans la table `users` |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
# TETRIS
|
||||
|
||||
```
|
||||
████████╗███████╗████████╗██████╗ ██╗███████╗
|
||||
██╔══╝██╔════╝╚══██╔══╝██╔══██╗██║██╔════╝
|
||||
██║ █████╗ ██║ ██████╔╝██║███████╗
|
||||
██║ ██╔══╝ ██║ ██╔══██╗██║╚════██║
|
||||
██║ ███████╗ ██║ ██║ ██║██║███████║
|
||||
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝
|
||||
```
|
||||
|
||||
Implémentation du jeu Tetris avec un thème cyberpunk, entièrement découpée en modules JS séparés.
|
||||
|
||||
---
|
||||
|
||||
## Architecture des fichiers
|
||||
|
||||
```
|
||||
srcs/frontend/src/
|
||||
├── pieces.js ← Définition des 7 pièces Tetris et leurs rotations
|
||||
├── tetris.js ← Logique du jeu (classe Tetris)
|
||||
├── renderer.js ← Rendu canvas (grille principale, hold, next)
|
||||
└── ui.js ← Glue UI : boutons, overlay, liaison game ↔ DOM
|
||||
```
|
||||
|
||||
```
|
||||
srcs/frontend/
|
||||
└── tetris.html ← Structure HTML de la page
|
||||
tetris.css ← Styles (thème cyberpunk)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contrôles clavier
|
||||
|
||||
| Touche | Action |
|
||||
|-------------|-------------------------------------|
|
||||
| `←` `→` | Déplacer la pièce horizontalement |
|
||||
| `↓` | Descente douce (+1 pt) |
|
||||
| `Espace` | Hard drop — chute instantanée (+2 pts par case) |
|
||||
| `Q` | Rotation gauche |
|
||||
| `W` | Rotation droite |
|
||||
| `C` | Hold — stocker / échanger la pièce courante |
|
||||
|
||||
---
|
||||
|
||||
## Flux de jeu
|
||||
|
||||
```
|
||||
spawn()
|
||||
│
|
||||
▼
|
||||
tick() ──── toutes les timeToDown ms ────┐
|
||||
│ │
|
||||
├─ canMoveDown ? ──── oui ──► moveDown()
|
||||
│
|
||||
└─ non ──► lockPiece()
|
||||
│
|
||||
├─► verifierLignes() (efface + score)
|
||||
├─► _makeHarder() (accélération)
|
||||
└─► spawn()
|
||||
│
|
||||
└─ canSpawn ? ──── non ──► GAME OVER
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `pieces.js` — Les pièces Tetris
|
||||
|
||||
### Classe de base : `Piece`
|
||||
|
||||
Classe abstraite dont héritent toutes les pièces du jeu.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ class Piece │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ position { x, y } — coordonnées dans la grille │
|
||||
│ currentRotation index de la rotation active │
|
||||
│ rotations tableau de toutes les formes rotées │
|
||||
│ shape forme actuellement active │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Méthode | Description |
|
||||
|--------------------|-------------|
|
||||
| `defineRotations()`| Retourne le tableau de toutes les matrices de rotation de la pièce. Surchargée dans chaque sous-classe. |
|
||||
| `getColor()` | Retourne l'index couleur de la pièce (1 à 7). Surchargée dans chaque sous-classe. |
|
||||
| `getPosition()` | Retourne une copie de `{ x, y }` — position actuelle dans la grille. |
|
||||
| `getShape()` | Retourne la matrice 2D de la forme active (rotation courante). |
|
||||
| `moveDown()` | Incrémente `y` de 1 — descend la pièce d'une case. |
|
||||
| `moveLeft()` | Décrémente `x` de 1 — déplace la pièce d'une case à gauche. |
|
||||
| `moveRight()` | Incrémente `x` de 1 — déplace la pièce d'une case à droite. |
|
||||
| `rotateLeft()` | Passe à la rotation précédente dans le tableau (sens anti-horaire). |
|
||||
| `rotateRight()` | Passe à la rotation suivante dans le tableau (sens horaire). |
|
||||
|
||||
---
|
||||
|
||||
### Les 7 pièces — sous-classes de `Piece`
|
||||
|
||||
Chaque pièce définit ses rotations via une matrice `1`/`0` et son index couleur.
|
||||
|
||||
| Classe | Forme | Couleur (index) | Rotations |
|
||||
|-----------------|-------|-----------------|-----------|
|
||||
| `PieceT` | T | Violet `1` | 4 |
|
||||
| `PieceL` | L | Orange `2` | 4 |
|
||||
| `PieceReverseL` | J | Bleu `3` | 4 |
|
||||
| `PieceI` | I | Cyan `4` | 2 |
|
||||
| `PieceZ` | Z | Rouge `5` | 2 |
|
||||
| `PieceReverseZ` | S | Vert `6` | 2 |
|
||||
| `PieceO` | O | Jaune `7` | 1 |
|
||||
|
||||
**Exemple — `PieceT` (4 rotations) :**
|
||||
```
|
||||
Rotation 0 Rotation 1 Rotation 2 Rotation 3
|
||||
0 1 0 0 1 0 0 0 0 0 1 0
|
||||
1 1 1 0 1 1 1 1 1 1 1 0
|
||||
0 0 0 0 1 0 0 1 0 0 1 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `tetris.js` — Logique du jeu
|
||||
|
||||
### Classe `Tetris`
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ new Tetris(onRender, onGameOver) │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ onRender : () => void — callback de rendu à chaque frame │
|
||||
│ onGameOver : (score) => void — callback appelé en fin de partie │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### État interne
|
||||
|
||||
| Propriété | Type | Description |
|
||||
|----------------------|-----------|-------------|
|
||||
| `grid` | `number[][]` | Grille principale 10×20 — `0` = vide, sinon index couleur |
|
||||
| `bufferGrid` | `number[][]` | Grille 10×5 utilisée pour afficher la pièce suivante |
|
||||
| `currentPiece` | `Piece` | Pièce en cours de chute |
|
||||
| `nextPiece` | `Piece` | Prochaine pièce à spawner |
|
||||
| `storedPiece` | `Piece` | Pièce en hold (stockée par le joueur) |
|
||||
| `score` | `number` | Score courant |
|
||||
| `timeToDown` | `number` | Intervalle (ms) entre deux descentes automatiques |
|
||||
| `hardening` | `number` | Seuil de points avant chaque accélération |
|
||||
| `decrementTTD` | `number` | Réduction de `timeToDown` à chaque palier |
|
||||
| `count` | `number` | Accumulateur de points depuis le dernier palier |
|
||||
| `isRunning` | `boolean` | Vrai si une partie est en cours |
|
||||
| `isPaused` | `boolean` | Vrai si la partie est en pause |
|
||||
| `canStore` | `boolean` | Faux si le hold a déjà été utilisé depuis le dernier spawn |
|
||||
|
||||
---
|
||||
|
||||
### Méthodes publiques
|
||||
|
||||
| Méthode | Description |
|
||||
|------------------------|-------------|
|
||||
| `configure(options)` | Applique les paramètres de difficulté — `timeToDown`, `hardening`, `decrementTTD`. Doit être appelé **avant** `start()` pour que `timeToDown` soit pris en compte. |
|
||||
| `start()` | Initialise et démarre une nouvelle partie (réinitialise la grille, le score, et spawn la première pièce). |
|
||||
| `stop()` | Arrête la partie — annule la boucle `requestAnimationFrame` et retire l'écouteur clavier. |
|
||||
| `pause()` | Bascule entre pause et reprise. En reprise, réinitialise `lastTime` pour éviter un saut d'accumulation. |
|
||||
|
||||
---
|
||||
|
||||
### Méthodes privées
|
||||
|
||||
#### Boucle de jeu
|
||||
|
||||
| Méthode | Description |
|
||||
|----------------------|-------------|
|
||||
| `_startGameLoop()` | Lance la boucle via `requestAnimationFrame`. Calcule le `deltaTime` entre chaque frame et accumule le temps. Déclenche `_tick()` dès que l'accumulateur dépasse `timeToDown`. |
|
||||
| `_tick()` | Un pas logique du jeu : descend la pièce si possible, sinon la verrouille, vérifie les lignes, accélère le jeu, puis spawne la suivante. Si le spawn est impossible → game over. |
|
||||
|
||||
#### Gestion des pièces
|
||||
|
||||
| Méthode | Description |
|
||||
|------------------------|-------------|
|
||||
| `_spawnNewPiece()` | Fait de `nextPiece` la pièce courante, génère une nouvelle `nextPiece`, et met à jour la `bufferGrid`. |
|
||||
| `_createRandomPiece()` | Instancie aléatoirement l'une des 7 pièces à la position de départ `(3, 0)`. |
|
||||
| `_updateBufferGrid()` | Recrée la grille miniature `bufferGrid` (10×5) centrée sur la forme de `nextPiece`. |
|
||||
| `_lockPiece()` | Grave la forme et la couleur de `currentPiece` dans `grid` à sa position actuelle. |
|
||||
| `_storePiece()` | Échange `currentPiece` et `storedPiece` (ou stocke la pièce courante si hold vide). Désactive le hold jusqu'au prochain spawn. |
|
||||
| `_rotatePiece(dir)` | Tente une rotation (`-1` = gauche, `1` = droite). En cas de collision, essaie des décalages latéraux ou verticaux (wall kick) avant d'annuler. |
|
||||
| `_hardDrop()` | Téléporte la pièce au bas de sa trajectoire, ajoute +2 pts par case parcourue, puis la verrouille immédiatement. |
|
||||
|
||||
#### Collisions
|
||||
|
||||
| Méthode | Description |
|
||||
|----------------------|-------------|
|
||||
| `_canMoveDown()` | Vérifie que chaque cellule de la pièce peut descendre d'une ligne (pas de mur bas, pas de case occupée). |
|
||||
| `_canMoveLeft()` | Vérifie que chaque cellule peut se déplacer d'une colonne à gauche. |
|
||||
| `_canMoveRight()` | Vérifie que chaque cellule peut se déplacer d'une colonne à droite. |
|
||||
| `_isValidPosition()` | Vérifie que la pièce est entièrement dans les limites de la grille et n'occupe aucune case déjà remplie. |
|
||||
| `_canSpawn()` | Alias de `_isValidPosition()` — utilisé pour détecter le game over après un spawn. |
|
||||
|
||||
#### Score & difficulté
|
||||
|
||||
| Méthode | Description |
|
||||
|------------------|-------------|
|
||||
| `verifierLignes()` | Parcourt la grille de bas en haut. Supprime chaque ligne complète, insère une ligne vide en haut. Ajoute les points selon le nombre de lignes effacées simultanément : `0→0 / 1→100 / 2→300 / 3→500 / 4→800`. |
|
||||
| `_makeHarder()` | Si `count` (points accumulés depuis le dernier palier) dépasse `hardening`, remet `count` à 0 et réduit `timeToDown` de `decrementTTD` (minimum 100 ms). |
|
||||
| `_gameOver()` | Appelle `stop()` puis déclenche le callback `onGameOver(score)`. |
|
||||
|
||||
#### Entrées clavier
|
||||
|
||||
| Méthode | Description |
|
||||
|-------------------|-------------|
|
||||
| `_handleKey(e)` | Écouteur `keydown`. Dispatche l'action selon la touche pressée. Appelle `onRender()` après chaque action. Ignoré si la partie est en pause (sauf pour les touches déjà bloquées par `isRunning`). |
|
||||
|
||||
---
|
||||
|
||||
### Paramètres de difficulté
|
||||
|
||||
| Paramètre | Défaut | Description |
|
||||
|---------------|----------|-------------|
|
||||
| `timeToDown` | `1000 ms`| Intervalle entre deux descentes automatiques. Plus faible = plus rapide. |
|
||||
| `hardening` | `1000 pts`| Points cumulés avant chaque accélération. Plus élevé = progression plus lente. |
|
||||
| `decrementTTD`| `100 ms` | Réduction de `timeToDown` à chaque palier. Plus élevé = accélération plus brutale. |
|
||||
|
||||
---
|
||||
|
||||
## `renderer.js` — Rendu canvas
|
||||
|
||||
### Constantes
|
||||
|
||||
| Constante | Valeur | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `CELL` | `30` | Taille en pixels d'une cellule dans la grille principale |
|
||||
| `COLORS` | tableau| 8 couleurs indexées de `0` (fond) à `7` (jaune) — thème cyberpunk |
|
||||
|
||||
### Fonctions
|
||||
|
||||
| Fonction | Description |
|
||||
|---------------------------------------|-------------|
|
||||
| `drawCell(ctx, x, y, colorIndex, size)` | Dessine une cellule colorée à la position `(x, y)` avec un effet 3D : highlight blanc en haut/gauche, ombre noire en bas/droite. |
|
||||
| `clearCanvas(ctx, w, h)` | Efface un canvas en le remplissant avec la couleur de fond `#070712`. |
|
||||
| `drawGridLines(ctx, cols, rows, size)`| Trace la grille de fond en lignes semi-transparentes (opacité 4 %). |
|
||||
| `drawGhost(ctx, piece, grid)` | Calcule et affiche la "pièce fantôme" — projection de la pièce courante au bas de sa trajectoire, dessinée en contour blanc semi-transparent. |
|
||||
| `drawMiniPiece(ctx, piece, w, h)` | Dessine une pièce centrée dans un petit canvas (pour les panneaux **Next** et **Hold**), en utilisant des cellules de 20 px. |
|
||||
| `render()` | Fonction de rendu principale appelée à chaque frame. Redessine : la grille principale, les cellules verrouillées, la pièce ghost, la pièce courante, les panneaux Next/Hold, et le score. |
|
||||
|
||||
---
|
||||
|
||||
## `ui.js` — Interface utilisateur
|
||||
|
||||
### Fonctions
|
||||
|
||||
| Fonction | Description |
|
||||
|-----------------------------|-------------|
|
||||
| `updateButtons()` | Met à jour l'état des boutons et des inputs selon `game.isRunning` et `game.isPaused` — désactive Start si en cours, active Pause/Stop seulement pendant une partie, bascule le label entre "Pause" et "Resume", verrouille les inputs de settings pendant le jeu. |
|
||||
| `showOverlay(title, score)` | Affiche l'overlay (écran superposé) avec un titre et optionnellement un score. Utilisé pour "GAME OVER", "PAUSE", et "STOPPED". |
|
||||
| `hideOverlay()` | Retire la classe `visible` de l'overlay pour le masquer. |
|
||||
| `applySettings()` | Lit les valeurs des trois inputs de configuration et appelle `game.configure(...)` pour mettre à jour les paramètres de difficulté. |
|
||||
|
||||
### Initialisation
|
||||
|
||||
```js
|
||||
const game = new Tetris(
|
||||
() => { render(); updateButtons(); }, // onRender
|
||||
(score) => { render(); updateButtons(); showOverlay('GAME OVER', score); } // onGameOver
|
||||
);
|
||||
```
|
||||
|
||||
| Événement | Action |
|
||||
|----------------------------------|--------|
|
||||
| Clic `btn-start` | Masque l'overlay, démarre la partie, met à jour UI et canvas |
|
||||
| Clic `btn-pause` | Bascule pause, affiche ou masque l'overlay "PAUSE" |
|
||||
| Clic `btn-stop` | Arrête la partie, affiche l'overlay "STOPPED" |
|
||||
| `change` sur les inputs settings | Appelle `applySettings()` pour reconfigurer la difficulté |
|
||||
|
||||
---
|
||||
|
||||
## Système de couleurs
|
||||
|
||||
```
|
||||
Index Couleur Hex Pièce
|
||||
0 Fond #070712 (vide)
|
||||
1 Violet #a855f7 PieceT
|
||||
2 Orange #f97316 PieceL
|
||||
3 Bleu #3b82f6 PieceReverseL
|
||||
4 Cyan #06b6d4 PieceI
|
||||
5 Rouge #ef4444 PieceZ
|
||||
6 Vert #22c55e PieceReverseZ
|
||||
7 Jaune #eab308 PieceO
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
le you win apparait sur la grille de l'adversaire, elle doit apparaitre sur la grille principale du joueur qui gagne FIXED ? [OUI]
|
||||
|
||||
undefined is not an object (evaluating 'grid[ny][nx]')
|
||||
a la ligne 56 de renderer.js FIXED ? [CA_EN_A_L'AIR]
|
||||
A tester plusieurs game, si ca freeze et que l'erreur reviens, NAN, j'ai changer les limite
|
||||
sur les bord gauche droit de la grid, a voir
|
||||
|
||||
Quand je fais pause,
|
||||
ca ne fait pas pose pour tout le monde FIXED[OUI]
|
||||
|
||||
Quand je fait stop,
|
||||
ca ne fais pas stop pour tout le monde FIXED[OUI]
|
||||
|
||||
durant le duel du tetris en ligne:
|
||||
j'ai plusieurs probleme:
|
||||
|
||||
|
||||
Les parametre doivent etre les memes pour tout le monde
|
||||
FIXED[OUI]
|
||||
|
||||
|
||||
DES GAMES OVER ARRIVE COMME CA SANS RAISON durant le duel FIXED[OUI]
|
||||
|
||||
est-ce du au valid-block ? au addGarbage ?
|
||||
|
||||
Bug A — Faux game over via garbage (tetris.js)
|
||||
|
||||
addGarbageLines appelait _isValidPosition() qui retourne false si gy < 0.
|
||||
Or après garbage, la pièce monte légitimement au-dessus de la grille
|
||||
(y négatif).
|
||||
Fix : nouvelle méthode _isValidPositionAllowTop() qui
|
||||
ignore les cellules au-dessus de la grille (zone tampon) et
|
||||
ne vérifie que les collisions réelles dans la grille.
|
||||
|
||||
Bug B — Crash si la pièce est au-dessus de la grille (tetris.js)
|
||||
|
||||
_canMoveDown, _canMoveLeft, _canMoveRight, et _lockPiece accèdent à grid[y + row]
|
||||
sans vérifier si y + row < 0 → undefined → crash. Fix :
|
||||
skip des rangées hors grille avec continue.
|
||||
|
||||
Bug C — Game over par garbage ne termine pas le duel (duel.js)
|
||||
|
||||
onLocalGameOver ne faisait endDuel() que si validBlock=true. Un game over réel dû à du garbage (validBlock=false) laissait le duel dans un état cassé et l'adversaire ne voyait jamais "YOU WIN". Fix : endDuel() systématique, et OPPONENT_GAME_OVER affiche toujours "YOU WIN".
|
||||
|
||||
|
||||
|
||||
separer le code tetris du reste, de meme pour les sockets. FAIT[?]
|
||||
|
||||
|
||||
error:
|
||||
renderer.js:56 Uncaught TypeError: Cannot read properties of undefined (reading 'length')
|
||||
at drawGhost (renderer.js:56:71)
|
||||
at render (renderer.js:101:9)
|
||||
at Tetris.onRender (ui.js:107:9)
|
||||
at gameLoop (tetris.js:115:18)
|
||||
|
||||
FIXED[ON DIRAIS BIEN]
|
||||
|
||||
Il faut verifier si le garbage si retrouve a la premiere ligne,
|
||||
si c'est le cas, game-over
|
||||
|
||||
Il faut un bouton restart
|
||||
|
||||
system d'attribution de point et d'enregistrement de point
|
||||
@@ -493,6 +493,39 @@ function setupSocketIO(io)
|
||||
}
|
||||
});
|
||||
|
||||
// pause → relayé aux DEUX joueurs de la room
|
||||
socket.on('tetris:pause', () => {
|
||||
const code = socket.tetrisRoomCode;
|
||||
if (!code) return;
|
||||
const room = tetrisRooms.get(code);
|
||||
if (!room) return;
|
||||
for (const s of room.values()) {
|
||||
s.emit('tetris:pause');
|
||||
}
|
||||
});
|
||||
|
||||
// stop → relayé aux DEUX joueurs de la room
|
||||
socket.on('tetris:stop', () => {
|
||||
const code = socket.tetrisRoomCode;
|
||||
if (!code) return;
|
||||
const room = tetrisRooms.get(code);
|
||||
if (!room) return;
|
||||
for (const s of room.values()) {
|
||||
s.emit('tetris:stop');
|
||||
}
|
||||
});
|
||||
|
||||
// settings → relayé aux DEUX joueurs de la room
|
||||
socket.on('tetris:settings', (data) => {
|
||||
const code = socket.tetrisRoomCode;
|
||||
if (!code) return;
|
||||
const room = tetrisRooms.get(code);
|
||||
if (!room) return;
|
||||
for (const s of room.values()) {
|
||||
s.emit('tetris:settings', data);
|
||||
}
|
||||
});
|
||||
|
||||
// game-over → relayé en opponent-game-over chez l'adversaire
|
||||
socket.on('tetris:game-over', (data) => {
|
||||
_tetrisRelayToOpponent(socket, 'tetris:opponent-game-over', data);
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
Je veux faire un mode duel du tetris,
|
||||
il est fonctionnel est solo.
|
||||
|
||||
Pour commencer, je vais devoir
|
||||
creer une div qui sera le rendu
|
||||
du joueur second joueur. il sera a droite de la
|
||||
div principale qui lui meme sera legerement decale vers la gauche,
|
||||
cette div sera grossierement identitique en style qui la div principale
|
||||
|
||||
je ne vais pas voir en temps reelle
|
||||
les pieces du joueur qui tombe.
|
||||
A la place quand le joueur 2 a mis une piece, il envoie un signal,
|
||||
le joueur 1 envoie un signal egalement quand la piece tombe.
|
||||
|
||||
en mode duel, quand une ligne est clear, il envoie la ligne
|
||||
moins la cellule qui a provoquer le clear (donc avec un trou pile
|
||||
la ou la derniere piece est arrive) au joueur opposant.
|
||||
|
||||
Ce qui a pour effet de decaler toute les lignes vers le haut
|
||||
a l'opposant pour recevoir la ligne recu avec le trou.
|
||||
|
||||
Pour se faire je vais devoir connecter les deux joueurs.
|
||||
|
||||
Il me faudra :
|
||||
|
||||
une class Duel il aura pour methode et membre:
|
||||
|
||||
action_queue: c'est un tableau qui repertorie tous les
|
||||
signaux a traiter, c'est un tableau qui sera partager
|
||||
entre le joueur1 et le joueur2
|
||||
|
||||
|
||||
Syncronise_game: fonction qui traite les
|
||||
actions de action_queue et qui verifie l'integrite du duel,
|
||||
il va par exemple regarder l'etat du jeu de chaque joueur
|
||||
pour voir s'il correspond bien a ce qui est attendu
|
||||
|
||||
il y aura different type de signaux.
|
||||
Le signal bloc pose avec le type de bloc,
|
||||
sa rotation et as position
|
||||
|
||||
le signal line cleared, avec le nombre de ligne
|
||||
cleared et on ajoute le trou aussi
|
||||
|
||||
Aucune idee de si je dois utiliser web socket ou autre
|
||||
@@ -54,9 +54,16 @@ class Duel {
|
||||
this.socket.emit('tetris:lines-cleared', { count, holeCol, garbageLines });
|
||||
}
|
||||
|
||||
onLocalGameOver(score) {
|
||||
onLocalGameOver(score, validBlock) {
|
||||
if (!this.isReady) return;
|
||||
this.socket.emit('tetris:game-over', { score });
|
||||
this.socket.emit('tetris:game-over', { score, validBlock });
|
||||
this.endDuel();
|
||||
}
|
||||
|
||||
endDuel() {
|
||||
this.isReady = false;
|
||||
this.action_queue = [];
|
||||
if (this.tetrisGame.isRunning) this.tetrisGame.stop();
|
||||
}
|
||||
|
||||
// ─── Traitement de la queue ───────────────
|
||||
@@ -82,7 +89,8 @@ class Duel {
|
||||
break;
|
||||
|
||||
case 'OPPONENT_GAME_OVER':
|
||||
this._showOpponentOverlay('YOU WIN', action.score);
|
||||
showOverlay('YOU WIN', action.score);
|
||||
this.endDuel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -116,12 +124,48 @@ class Duel {
|
||||
});
|
||||
|
||||
this.socket.on('tetris:opponent-game-over', (data) => {
|
||||
this.action_queue.push({ type: 'OPPONENT_GAME_OVER', score: data.score });
|
||||
this.action_queue.push({ type: 'OPPONENT_GAME_OVER', score: data.score, validBlock: data.validBlock });
|
||||
});
|
||||
|
||||
this.socket.on('tetris:start-duel', () => {
|
||||
if (this.onStart) this.onStart();
|
||||
});
|
||||
|
||||
this.socket.on('tetris:pause', () => {
|
||||
this.tetrisGame.pause();
|
||||
updateButtons();
|
||||
if (this.tetrisGame.isPaused) showOverlay('PAUSE');
|
||||
else hideOverlay();
|
||||
});
|
||||
|
||||
this.socket.on('tetris:stop', () => {
|
||||
this.tetrisGame.stop();
|
||||
updateButtons();
|
||||
render();
|
||||
showOverlay('STOPPED');
|
||||
});
|
||||
|
||||
this.socket.on('tetris:settings', (data) => {
|
||||
document.getElementById('input-ttd').value = data.timeToDown;
|
||||
document.getElementById('input-hardening').value = data.hardening;
|
||||
document.getElementById('input-decrement').value = data.decrementTTD;
|
||||
this.tetrisGame.configure(data);
|
||||
});
|
||||
}
|
||||
|
||||
togglePause() {
|
||||
if (!this.isReady) return;
|
||||
this.socket.emit('tetris:pause');
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.isReady) return;
|
||||
this.socket.emit('tetris:stop');
|
||||
}
|
||||
|
||||
syncSettings(settings) {
|
||||
if (!this.isReady) return;
|
||||
this.socket.emit('tetris:settings', settings);
|
||||
}
|
||||
|
||||
// ─── Utilitaires ─────────────────────────
|
||||
|
||||
@@ -53,7 +53,7 @@ function drawGhost(ctx, piece, grid) {
|
||||
if (shape[row][col] !== 0) {
|
||||
const ny = ghost.y + row;
|
||||
const nx = ghost.x + col;
|
||||
if (ny >= grid.length || grid[ny][nx] !== 0) valid = false;
|
||||
if (ny < 0 || ny >= grid.length || nx < 0 || nx >= grid[ny].length || grid[ny][nx] !== 0) valid = false;
|
||||
}
|
||||
if (!valid) { ghost.y--; break; }
|
||||
}
|
||||
|
||||
@@ -55,11 +55,11 @@ h1 {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Section locale (légèrement décalée à gauche par le flex naturel) ── */
|
||||
/* ── Section locale ── */
|
||||
#local-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#app {
|
||||
@@ -165,11 +165,37 @@ button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#btn-start { color: var(--accent); border-color: var(--accent); }
|
||||
#btn-start:hover:not(:disabled) { background: var(--accent); color: var(--bg); box-shadow: 0 0 15px var(--accent); }
|
||||
#btn-start {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
#btn-pause { color: var(--accent2); border-color: var(--accent2); }
|
||||
#btn-pause:hover:not(:disabled) { background: var(--accent2); color: var(--bg); box-shadow: 0 0 15px var(--accent2); }
|
||||
#btn-start:hover:not(:disabled)
|
||||
{
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
box-shadow: 0 0 15px var(--accent);
|
||||
}
|
||||
|
||||
#btn-restart {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
#btn-restart:hover:not(:disabled){
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
box-shadow: 0 0 15px var(--accent);
|
||||
}
|
||||
|
||||
#btn-pause {
|
||||
color: var(--accent2);
|
||||
border-color: var(--accent2);
|
||||
}
|
||||
#btn-pause:hover:not(:disabled) {
|
||||
background: var(--accent2);
|
||||
color: var(--bg); box-shadow: 0 0 15px var(--accent2);
|
||||
}
|
||||
|
||||
#btn-stop { color: #ef4444; border-color: #ef4444; }
|
||||
#btn-stop:hover:not(:disabled) { background: #ef4444; color: var(--bg); box-shadow: 0 0 15px #ef4444; }
|
||||
@@ -293,21 +319,26 @@ button:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
#duel-status.waiting { color: #f97316; }
|
||||
#duel-status.ready { color: var(--accent); }
|
||||
|
||||
/* ── Colonne gauche (panel + settings empilés) ── */
|
||||
#left-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 130px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Settings Panel ── */
|
||||
#settings-panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 14px 20px;
|
||||
margin-top: -250px;
|
||||
margin-left: -600px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 0 20px rgba(0,255,231,0.05);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: fit-content;
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
@@ -322,10 +353,9 @@ button:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
|
||||
.settings-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
font-size: 0.6rem;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 0.55rem;
|
||||
color: var(--dim);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
@@ -338,7 +368,7 @@ button:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 0.65rem;
|
||||
padding: 4px 8px;
|
||||
width: 80px;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
@@ -28,20 +28,40 @@
|
||||
<div id="local-section">
|
||||
<div id="app">
|
||||
|
||||
<!-- Panneau gauche : Hold + Score + Boutons -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">Hold</div>
|
||||
<canvas id="canvas-hold" width="100" height="80"></canvas>
|
||||
<!-- Colonne gauche : Hold + Score + Boutons + Settings -->
|
||||
<div id="left-column">
|
||||
<div class="panel">
|
||||
<div class="panel-title">Hold</div>
|
||||
<canvas id="canvas-hold" width="100" height="80"></canvas>
|
||||
|
||||
<div class="score-block">
|
||||
<div class="score-label">Score</div>
|
||||
<div class="score-value" id="score-display">0</div>
|
||||
<div class="score-block">
|
||||
<div class="score-label">Score</div>
|
||||
<div class="score-value" id="score-display">0</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button id="btn-start">Start</button>
|
||||
<button id="btn-restart" disabled>Restart</button>
|
||||
<button id="btn-pause" disabled>Pause</button>
|
||||
<button id="btn-stop" disabled>Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button id="btn-start">Start</button>
|
||||
<button id="btn-pause" disabled>Pause</button>
|
||||
<button id="btn-stop" disabled>Stop</button>
|
||||
<!-- Panneau de configuration -->
|
||||
<div id="settings-panel">
|
||||
<div class="settings-title">Paramètres</div>
|
||||
<div class="settings-row">
|
||||
<label for="input-ttd">Vitesse initiale (ms)</label>
|
||||
<input type="number" id="input-ttd" min="100" max="3000" step="50" value="1000">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label for="input-hardening">Points avant accélération</label>
|
||||
<input type="number" id="input-hardening" min="100" max="5000" step="100" value="1000">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label for="input-decrement">Réduction vitesse (ms)</label>
|
||||
<input type="number" id="input-decrement" min="10" max="500" step="10" value="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,23 +113,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Panneau de configuration -->
|
||||
<div id="settings-panel">
|
||||
<div class="settings-title">Paramètres</div>
|
||||
<div class="settings-row">
|
||||
<label for="input-ttd">Vitesse initiale (ms)</label>
|
||||
<input type="number" id="input-ttd" min="100" max="3000" step="50" value="1000">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label for="input-hardening">Points avant accélération</label>
|
||||
<input type="number" id="input-hardening" min="100" max="5000" step="100" value="1000">
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label for="input-decrement">Réduction vitesse (ms)</label>
|
||||
<input type="number" id="input-decrement" min="10" max="500" step="10" value="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="pieces.js"></script>
|
||||
|
||||
@@ -129,7 +129,7 @@ class Tetris {
|
||||
this._makeHarder();
|
||||
this._spawnNewPiece();
|
||||
this.canStore = true;
|
||||
if (!this._canSpawn()) this._gameOver();
|
||||
if (!this._canSpawn()) this._gameOver(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ class Tetris {
|
||||
this._spawnNewPiece();
|
||||
this.canStore = true;
|
||||
this.accumulator = 0;
|
||||
if (!this._canSpawn()) this._gameOver();
|
||||
if (!this._canSpawn()) this._gameOver(true);
|
||||
}
|
||||
|
||||
_rotatePiece(direction) {
|
||||
@@ -290,6 +290,7 @@ class Tetris {
|
||||
if (shape[row][col] !== 0) {
|
||||
const ny = y + row + 1;
|
||||
const nx = x + col;
|
||||
if (ny < 0) continue; // encore au-dessus de la grille
|
||||
if (ny >= this.grid.length || this.grid[ny][nx] !== 0) return false;
|
||||
}
|
||||
return true;
|
||||
@@ -302,6 +303,7 @@ class Tetris {
|
||||
for (let row = 0; row < shape.length; row++)
|
||||
for (let col = 0; col < shape[row].length; col++)
|
||||
if (shape[row][col] !== 0) {
|
||||
if (y + row < 0) continue; // au-dessus de la grille
|
||||
const nx = x + col - 1;
|
||||
if (nx < 0 || this.grid[y + row][nx] !== 0) return false;
|
||||
}
|
||||
@@ -315,6 +317,7 @@ class Tetris {
|
||||
for (let row = 0; row < shape.length; row++)
|
||||
for (let col = 0; col < shape[row].length; col++)
|
||||
if (shape[row][col] !== 0) {
|
||||
if (y + row < 0) continue; // au-dessus de la grille
|
||||
const nx = x + col + 1;
|
||||
if (nx >= this.grid[0].length || this.grid[y + row][nx] !== 0) return false;
|
||||
}
|
||||
@@ -346,7 +349,7 @@ class Tetris {
|
||||
const color = this.currentPiece.getColor();
|
||||
for (let row = 0; row < shape.length; row++)
|
||||
for (let col = 0; col < shape[row].length; col++)
|
||||
if (shape[row][col] !== 0)
|
||||
if (shape[row][col] !== 0 && y + row >= 0)
|
||||
this.grid[y + row][x + col] = color;
|
||||
this.lastLandingCol = x + Math.floor(shape[0].length / 2);
|
||||
if (this.onBlockPlaced) this.onBlockPlaced(this.grid.map(r => [...r]));
|
||||
@@ -355,12 +358,36 @@ class Tetris {
|
||||
addGarbageLines(lines) {
|
||||
if (!this.isRunning || !lines.length) return;
|
||||
this.grid.splice(0, lines.length);
|
||||
for (const line of lines) this.grid.push([...line]);
|
||||
if (!this._isValidPosition()) this._gameOver();
|
||||
for (const line of lines) this.grid.push([...line]); // ...line pour faire une copie independante
|
||||
// La grille a remonté de lines.length lignes — on remonte la pièce du même décalage
|
||||
// pour qu'elle reste dans la même position relative aux blocs verrouillés.
|
||||
if (this.currentPiece) {
|
||||
this.currentPiece.position.y -= lines.length;
|
||||
}
|
||||
if (this.grid[0].some(c => c !== 0)) { this._gameOver(false); return; }
|
||||
if (!this._isValidPositionAllowTop()) this._gameOver(false);
|
||||
}
|
||||
|
||||
_gameOver() {
|
||||
// Comme _isValidPosition mais tolère gy < 0 (zone tampon au-dessus de la grille après garbage)
|
||||
_isValidPositionAllowTop() {
|
||||
if (!this.currentPiece) return true;
|
||||
const { x, y } = this.currentPiece.getPosition();
|
||||
const shape = this.currentPiece.getShape();
|
||||
for (let row = 0; row < shape.length; row++)
|
||||
for (let col = 0; col < shape[row].length; col++)
|
||||
if (shape[row][col] !== 0) {
|
||||
const gy = y + row;
|
||||
const gx = x + col;
|
||||
if (gy < 0) continue; // au-dessus de la grille : OK
|
||||
if (gx < 0 || gx >= this.grid[0].length ||
|
||||
gy >= this.grid.length ||
|
||||
this.grid[gy][gx] !== 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_gameOver(validBlock = false) {
|
||||
this.stop();
|
||||
this.onGameOver(this.score);
|
||||
this.onGameOver(this.score, validBlock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,8 @@ const game = new Tetris(
|
||||
updateButtons();
|
||||
},
|
||||
// onGameOver
|
||||
(score) => {
|
||||
if (duel) duel.onLocalGameOver(score);
|
||||
(score, validBlock) => {
|
||||
if (duel) duel.onLocalGameOver(score, validBlock);
|
||||
render();
|
||||
updateButtons();
|
||||
showOverlay('GAME OVER', score);
|
||||
@@ -133,25 +133,35 @@ btnStart.addEventListener('click', () => {
|
||||
});
|
||||
|
||||
btnPause.addEventListener('click', () => {
|
||||
game.pause();
|
||||
updateButtons();
|
||||
if (game.isPaused) showOverlay('PAUSE');
|
||||
else hideOverlay();
|
||||
if (duel && duel.isReady) {
|
||||
duel.togglePause();
|
||||
} else {
|
||||
game.pause();
|
||||
updateButtons();
|
||||
if (game.isPaused) showOverlay('PAUSE');
|
||||
else hideOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
btnStop.addEventListener('click', () => {
|
||||
game.stop();
|
||||
updateButtons();
|
||||
render();
|
||||
showOverlay('STOPPED');
|
||||
if (duel && duel.isReady) {
|
||||
duel.stop();
|
||||
} else {
|
||||
game.stop();
|
||||
updateButtons();
|
||||
render();
|
||||
showOverlay('STOPPED');
|
||||
}
|
||||
});
|
||||
|
||||
function applySettings() {
|
||||
game.configure({
|
||||
const settings = {
|
||||
timeToDown: parseInt(inputTTD.value, 10),
|
||||
hardening: parseInt(inputHardening.value, 10),
|
||||
decrementTTD: parseInt(inputDecrement.value, 10),
|
||||
});
|
||||
};
|
||||
game.configure(settings);
|
||||
if (duel && duel.isReady) duel.syncSettings(settings);
|
||||
}
|
||||
|
||||
inputTTD.addEventListener('change', applySettings);
|
||||
|
||||
Reference in New Issue
Block a user