commit 11250de8afa8a4b35ad1b13fe09c0d8a488abb7a Author: thedevilbox Date: Mon Dec 1 20:32:27 2025 +0100 1 er commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..5019530 --- /dev/null +++ b/README.md @@ -0,0 +1,369 @@ +# 🎼 Oulvic - Jeu de type Pac-Man + +Un jeu de type Pac-Man dĂ©veloppĂ© en JavaScript vanilla par **Ludo** et **Syoul**. + +--- + +## 📖 Table des matiĂšres + +1. [Description](#description) +2. [FonctionnalitĂ©s](#fonctionnalitĂ©s) +3. [Structure du projet](#structure-du-projet) +4. [Installation](#installation) +5. [Comment jouer](#comment-jouer) +6. [Architecture du code](#architecture-du-code) +7. [Documentation technique](#documentation-technique) + +--- + +## 📝 Description + +**Oulvic** est un clone de Pac-Man dĂ©veloppĂ© entiĂšrement en HTML5, CSS3 et JavaScript vanilla. Le jeu utilise l'Ă©lĂ©ment `` pour le rendu graphique et propose une expĂ©rience de jeu complĂšte avec : + +- Un personnage jouable aux couleurs changeantes +- 4 fantĂŽmes avec intelligence artificielle +- 3 labyrinthes diffĂ©rents +- SystĂšme de niveaux progressifs +- Classement des scores (localStorage) +- Interface responsive + +--- + +## ✹ FonctionnalitĂ©s + +### Gameplay +- **DĂ©placement fluide** : Mouvement pixel par pixel avec interpolation +- **SystĂšme de vies** : 3 vies reprĂ©sentĂ©es par des cƓurs +- **Points** : Collecte de pastilles (+10 points) et bonus +- **Progression de niveau** : Passer au niveau suivant en mangeant 4 cerises + +### Bonus +| Bonus | Points | Effet | +|-------|--------|-------| +| 🍒 Cerise | +100 | Active la poursuite des fantĂŽmes pendant un temps limitĂ©. 4 cerises = niveau suivant | +| ⭐ Étoile "L" | +15 | Bonus supplĂ©mentaire | + +### FantĂŽmes +- **4 fantĂŽmes** avec couleurs distinctes (rouge, magenta, cyan, orange) +- **IA adaptative** : Les fantĂŽmes poursuivent Pac-Man aprĂšs qu'une cerise soit mangĂ©e +- **Vitesse croissante** : Les fantĂŽmes accĂ©lĂšrent Ă  chaque niveau + +### Labyrinthes +- 3 variantes de labyrinthes qui alternent Ă  chaque niveau +- GĂ©nĂ©ration dynamique avec remplissage des espaces vides +- Randomisation lĂ©gĂšre pour plus de variĂ©tĂ© + +--- + +## 📁 Structure du projet + +``` +pacmanludo/ +├── index.html # Page HTML principale +├── style.css # Feuille de styles +├── game.js # Logique du jeu +└── README.md # Documentation +``` + +--- + +## 🚀 Installation + +1. **Cloner ou tĂ©lĂ©charger** le projet +2. **Ouvrir** `index.html` dans un navigateur web moderne +3. **Jouer !** + +Aucune dĂ©pendance externe n'est requise. + +--- + +## 🎯 Comment jouer + +### ContrĂŽles +| Touche | Action | +|--------|--------| +| ↑ FlĂšche Haut | DĂ©placer vers le haut | +| ↓ FlĂšche Bas | DĂ©placer vers le bas | +| ← FlĂšche Gauche | DĂ©placer vers la gauche | +| → FlĂšche Droite | DĂ©placer vers la droite | + +### Objectif +1. Collectez toutes les pastilles blanches pour marquer des points +2. Mangez les cerises pour activer la poursuite et progresser +3. Évitez les fantĂŽmes ! +4. Atteignez le score le plus Ă©levĂ© possible + +### Progression +- **4 cerises mangĂ©es** = passage au niveau suivant +- À chaque niveau : + - Pac-Man accĂ©lĂšre lĂ©gĂšrement (+5% par niveau) + - Les fantĂŽmes deviennent plus rapides (+20% par niveau) + - Le labyrinthe change (cycle de 3 labyrinthes) + +--- + +## đŸ—ïž Architecture du code + +### `index.html` + +Structure HTML de la page : + +```html +├── main-wrapper +│ ├── container (zone de jeu) +│ │ ├── Titre "OULVIC" +│ │ ├── Champ nom d'utilisateur +│ │ ├── Barre d'informations (score, niveau, vies, statut) +│ │ ├── Canvas du jeu (600x600) +│ │ └── Instructions et bouton rejouer +│ └── leaderboard-container (classement) +└── footer (crĂ©dits) +``` + +### `style.css` + +| Section | Description | +|---------|-------------| +| Reset & Body | Styles de base, fond sombre avec effets | +| Layout | Flexbox pour disposition responsive | +| Game Info | Styles pour score, niveau, vies | +| Canvas | Bordure dorĂ©e, ombres | +| Leaderboard | Tableau des scores stylisĂ© | +| Responsive | Media queries pour mobile | + +### `game.js` + +#### Constantes principales + +```javascript +CELL_SIZE = 20 // Taille d'une cellule en pixels +COLS = 30 // Nombre de colonnes +ROWS = 30 // Nombre de lignes + +// Types de cellules +WALL = 1 // Mur (bleu) +DOT = 2 // Pastille (blanc) +EMPTY = 0 // Vide +TUNNEL = 3 // Tunnel (passage fantĂŽmes) +BONUS_CHERRY = 4 // Cerise bonus +BONUS_LUDO = 5 // Étoile bonus +``` + +--- + +## 📚 Documentation technique + +### Classe `Pacman` + +GĂšre le personnage jouable. + +| PropriĂ©tĂ© | Type | Description | +|-----------|------|-------------| +| `x, y` | number | Position en cellules | +| `pixelX, pixelY` | number | Position en pixels (mouvement fluide) | +| `direction` | number | Direction actuelle (0=haut, 1=droite, 2=bas, 3=gauche) | +| `nextDirection` | number | Prochaine direction demandĂ©e | +| `speed` | number | Vitesse de dĂ©placement | +| `mouthAngle` | number | Animation de la bouche | +| `colorAnimation` | number | Animation des couleurs HSL | + +| MĂ©thode | Description | +|---------|-------------| +| `update()` | Met Ă  jour la position et l'animation | +| `canMove(direction)` | VĂ©rifie si le mouvement est possible | +| `collectDot()` | Collecte les pastilles et bonus | +| `draw()` | Dessine Pac-Man sur le canvas | + +--- + +### Classe `Ghost` + +GĂšre les fantĂŽmes ennemis. + +| PropriĂ©tĂ© | Type | Description | +|-----------|------|-------------| +| `x, y` | number | Position en cellules | +| `pixelX, pixelY` | number | Position en pixels | +| `color` | string | Couleur du fantĂŽme | +| `direction` | number | Direction de dĂ©placement | +| `speed` | number | Vitesse (augmente avec le niveau) | +| `moveInterval` | number | Intervalle entre changements de direction | + +| MĂ©thode | Description | +|---------|-------------| +| `update()` | Met Ă  jour la position et l'IA | +| `updateSpeed()` | Ajuste la vitesse selon le niveau | +| `canMove(direction)` | VĂ©rifie si le mouvement est possible | +| `getDirectionToPacman(dirs)` | Calcule la meilleure direction vers Pac-Man | +| `draw()` | Dessine le fantĂŽme (corps + yeux) | + +--- + +### Classe `Bonus` + +GĂšre les objets bonus (cerises, Ă©toiles). + +| PropriĂ©tĂ© | Type | Description | +|-----------|------|-------------| +| `x, y` | number | Position en cellules | +| `type` | number | Type de bonus (CHERRY ou LUDO) | +| `animation` | number | Animation de pulsation | + +| MĂ©thode | Description | +|---------|-------------| +| `update()` | Met Ă  jour l'animation | +| `draw()` | Dessine le bonus avec effet de scale | + +--- + +### Fonctions principales + +#### Gestion du labyrinthe + +| Fonction | Description | +|----------|-------------| +| `countDots()` | Compte le nombre de pastilles restantes | +| `drawMaze()` | Dessine le labyrinthe complet | +| `fillEmptySpaces()` | Remplit les espaces vides avec des pastilles | +| `randomizeMaze()` | Ajoute des variations alĂ©atoires au labyrinthe | +| `placeBonuses()` | Place les bonus aux positions dĂ©finies | + +#### Gestion du jeu + +| Fonction | Description | +|----------|-------------| +| `initGame()` | Initialise une nouvelle partie | +| `gameLoop()` | Boucle principale du jeu (requestAnimationFrame) | +| `checkCollisions()` | DĂ©tecte les collisions Pac-Man/fantĂŽmes | +| `nextLevel()` | Passe au niveau suivant | +| `restartCurrentLevel()` | Recommence le niveau actuel (aprĂšs perte de vie) | + +#### Interface utilisateur + +| Fonction | Description | +|----------|-------------| +| `updateLivesDisplay()` | Met Ă  jour l'affichage des vies | +| `updateLeaderboard()` | RafraĂźchit le classement | +| `saveScore()` | Sauvegarde le score en localStorage | +| `getScores()` | RĂ©cupĂšre les scores depuis localStorage | + +--- + +### Variables globales d'Ă©tat + +```javascript +let score = 0; // Score actuel +let level = 1; // Niveau actuel +let lives = 3; // Vies restantes +let gameRunning = true; // État du jeu +let totalDots = 0; // Pastilles restantes +let cherriesEaten = 0; // Cerises mangĂ©es (niveau actuel) +let isChangingLevel = false; // Flag de transition +let cherryEatenRecently = false; // Mode poursuite activĂ© +let cherryEatenTimer = 0; // DurĂ©e du mode poursuite +``` + +--- + +### SystĂšme de stockage des scores + +Les scores sont sauvegardĂ©s dans le `localStorage` du navigateur sous la clĂ© `pacmanScores`. + +**Format des donnĂ©es :** +```javascript +{ + username: "NomJoueur", // Nom saisi ou "Anonyme" + score: 1500, // Score final + date: "2025-12-01T..." // Date ISO +} +``` + +- Maximum **10 scores** conservĂ©s +- TriĂ©s par score dĂ©croissant + +--- + +### Labyrinthes + +Le jeu dispose de 3 labyrinthes prĂ©dĂ©finis (`originalMaze1`, `originalMaze2`, `originalMaze3`) stockĂ©s comme tableaux 2D de 30x30. + +**Rotation des labyrinthes :** +```javascript +mazeIndex = (level - 1) % 3 +``` + +Les labyrinthes sont modifiĂ©s dynamiquement : +1. `fillEmptySpaces()` - Remplit les zones vides +2. `randomizeMaze()` - Ajoute de la variĂ©tĂ© + +--- + +## 🎹 Design + +### Palette de couleurs + +| ÉlĂ©ment | Couleur | +|---------|---------| +| Fond | `#0a0a0a` (noir) | +| Murs | `#0000ff` (bleu) | +| Pastilles | `#ffffff` (blanc) | +| Texte titre | `#ffd700` (or) | +| Cerises | `#ff0000` (rouge) | +| CƓurs | `#ff0000` (rouge) | + +### FantĂŽmes +- Rouge : `#ff0000` +- Magenta : `#ff00ff` +- Cyan : `#00ffff` +- Orange : `#ffa500` + +### Pac-Man +- Couleur animĂ©e en HSL (arc-en-ciel) + +--- + +## 🔧 Personnalisation + +### Modifier la difficultĂ© + +Dans `game.js` : + +```javascript +// Vitesse de base de Pac-Man (ligne 145) +this.baseSpeed = 0.15; + +// Vitesse de base des fantĂŽmes (ligne 277) +this.baseSpeed = 0.1; + +// AccĂ©lĂ©ration par niveau +pacman.speed = pacman.baseSpeed * (1 + (level - 1) * 0.05); +ghost.speed = ghost.baseSpeed * (1 + (level - 1) * 0.2); +``` + +### Modifier les points + +```javascript +// Pastille (ligne 213) +score += 10; + +// Cerise (ligne 224) +score += 100; + +// Étoile Ludo (ligne 241) +score += 15; +``` + +--- + +## 📄 Licence + +Projet créé par **Ludo** et **Syoul**. + +--- + +## 🙏 CrĂ©dits + +- Inspiration : Pac-Man original (Namco, 1980) +- DĂ©veloppement : Ludo & Syoul + + diff --git a/game.js b/game.js new file mode 100644 index 0000000..e61c0d1 --- /dev/null +++ b/game.js @@ -0,0 +1,1110 @@ +const canvas = document.getElementById('gameCanvas'); +const ctx = canvas.getContext('2d'); +const scoreElement = document.getElementById('score'); +const levelElement = document.getElementById('level'); +const livesElement = document.getElementById('lives'); +const statusElement = document.getElementById('status'); +const restartBtn = document.getElementById('restartBtn'); +const usernameInput = document.getElementById('username'); +const leaderboardElement = document.getElementById('leaderboard'); + +const CELL_SIZE = 20; +const COLS = 30; +const ROWS = 30; + +const WALL = 1; +const DOT = 2; +const EMPTY = 0; +const TUNNEL = 3; +const BONUS_CHERRY = 4; +const BONUS_LUDO = 5; + +const originalMaze1 = [ + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], + [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], + [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,1,1,1,3,3,3,3,1,1,1,0,1,1,2,1,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,0,2,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,2,2,1,1,2,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,2,1,1,2,2,2,1], + [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], + [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], + [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], + [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +]; + +const originalMaze2 = [ + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], + [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], + [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,1,1,1,3,3,3,3,1,1,1,0,1,1,2,1,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,0,2,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,2,2,1,1,2,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,2,1,1,2,2,2,1], + [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], + [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], + [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], + [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +]; + +const originalMaze3 = [ + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], + [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], + [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,1,1,1,3,3,3,3,1,1,1,0,1,1,2,1,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,0,2,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], + [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], + [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], + [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], + [1,2,2,2,1,1,2,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,2,1,1,2,2,2,1], + [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], + [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], + [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], + [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], + [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] +]; + +const mazeVariants = [originalMaze1, originalMaze2, originalMaze3]; +let originalMaze = originalMaze1; +let currentMazeIndex = 0; + +let maze = originalMaze.map(row => [...row]); + +let score = 0; +let level = 1; +let gameRunning = true; +let totalDots = 0; +let cherriesEaten = 0; +let isChangingLevel = false; +let cherryEatenRecently = false; +let cherryEatenTimer = 0; +let lives = 3; + +class Pacman { + constructor() { + this.x = 14; + this.y = 23; + this.direction = 0; + this.nextDirection = 0; + this.mouthAngle = 0; + this.mouthOpen = true; + this.baseSpeed = 0.15; + this.speed = this.baseSpeed; + this.pixelX = this.x * CELL_SIZE + CELL_SIZE / 2; + this.pixelY = this.y * CELL_SIZE + CELL_SIZE / 2; + this.colorAnimation = 0; + } + + update() { + if (!gameRunning) return; + + this.mouthAngle += 0.2; + if (this.mouthAngle > Math.PI * 2) { + this.mouthAngle = 0; + this.mouthOpen = !this.mouthOpen; + } + + this.colorAnimation += 0.3; + if (this.colorAnimation > Math.PI * 2) { + this.colorAnimation = 0; + } + + const gridX = Math.floor(this.pixelX / CELL_SIZE); + const gridY = Math.floor(this.pixelY / CELL_SIZE); + + if (this.canMove(this.nextDirection)) { + this.direction = this.nextDirection; + } + + if (this.canMove(this.direction)) { + const dx = [0, 1, 0, -1]; + const dy = [-1, 0, 1, 0]; + + this.pixelX += dx[this.direction] * this.speed * CELL_SIZE; + this.pixelY += dy[this.direction] * this.speed * CELL_SIZE; + + if (this.pixelX < 0) { + this.pixelX = COLS * CELL_SIZE; + } else if (this.pixelX > COLS * CELL_SIZE) { + this.pixelX = 0; + } + } else { + this.pixelX = gridX * CELL_SIZE + CELL_SIZE / 2; + this.pixelY = gridY * CELL_SIZE + CELL_SIZE / 2; + } + + this.x = Math.floor(this.pixelX / CELL_SIZE); + this.y = Math.floor(this.pixelY / CELL_SIZE); + + this.collectDot(); + } + + canMove(direction) { + const dx = [0, 1, 0, -1]; + const dy = [-1, 0, 1, 0]; + + const nextX = Math.floor((this.pixelX + dx[direction] * CELL_SIZE * 0.5) / CELL_SIZE); + const nextY = Math.floor((this.pixelY + dy[direction] * CELL_SIZE * 0.5) / CELL_SIZE); + + if (nextX < 0 || nextX >= COLS || nextY < 0 || nextY >= ROWS) { + return true; + } + + return maze[nextY][nextX] !== WALL; + } + + collectDot() { + if (maze[this.y][this.x] === DOT) { + maze[this.y][this.x] = EMPTY; + score += 10; + scoreElement.textContent = score; + totalDots--; + } else if (maze[this.y][this.x] === BONUS_CHERRY) { + if (isChangingLevel) { + console.log('Changement de niveau en cours, cerise ignorĂ©e'); + return; + } + + console.log('Cerise collectĂ©e, cherriesEaten:', cherriesEaten); + maze[this.y][this.x] = EMPTY; + score += 100; + scoreElement.textContent = score; + bonuses = bonuses.filter(b => !(b.x === this.x && b.y === this.y && b.type === BONUS_CHERRY)); + cherriesEaten++; + + cherryEatenRecently = true; + cherryEatenTimer = Math.max(150, 300 - (level - 1) * 20); + + console.log('AprĂšs incrĂ©mentation, cherriesEaten:', cherriesEaten, 'isChangingLevel:', isChangingLevel); + + if (cherriesEaten >= 4 && !isChangingLevel) { + console.log('4 cerises mangĂ©es, passage au niveau suivant'); + cherriesEaten = 0; + nextLevel(); + } + } else if (maze[this.y][this.x] === BONUS_LUDO) { + maze[this.y][this.x] = EMPTY; + score += 15; + scoreElement.textContent = score; + bonuses = bonuses.filter(b => !(b.x === this.x && b.y === this.y && b.type === BONUS_LUDO)); + } + } + + draw() { + ctx.save(); + ctx.translate(this.pixelX, this.pixelY); + + const rotation = [Math.PI * 1.5, 0, Math.PI * 0.5, Math.PI]; + ctx.rotate(rotation[this.direction]); + + const hue = (this.colorAnimation * 180 / Math.PI) % 360; + ctx.fillStyle = `hsl(${hue}, 100%, 50%)`; + ctx.beginPath(); + + if (this.mouthOpen) { + ctx.arc(0, 0, CELL_SIZE * 0.4, 0.2, Math.PI * 2 - 0.2); + } else { + ctx.arc(0, 0, CELL_SIZE * 0.4, 0, Math.PI * 2); + } + + ctx.lineTo(0, 0); + ctx.fill(); + ctx.restore(); + } +} + +class Ghost { + constructor(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.direction = Math.floor(Math.random() * 4); + this.baseSpeed = 0.1; + this.speed = this.baseSpeed; + this.pixelX = this.x * CELL_SIZE + CELL_SIZE / 2; + this.pixelY = this.y * CELL_SIZE + CELL_SIZE / 2; + this.moveCounter = 0; + this.moveInterval = 30; + } + + updateSpeed() { + this.speed = this.baseSpeed * (1 + (level - 1) * 0.2); + } + + update() { + if (!gameRunning) return; + + if (cherryEatenTimer > 0) { + cherryEatenTimer--; + } else { + cherryEatenRecently = false; + } + + this.moveInterval = Math.max(15, 30 - (level - 1) * 2); + + this.moveCounter++; + + if (this.moveCounter > this.moveInterval || !this.canMove(this.direction)) { + const possibleDirections = []; + for (let i = 0; i < 4; i++) { + if (this.canMove(i)) { + possibleDirections.push(i); + } + } + + if (possibleDirections.length > 0) { + if (cherryEatenRecently) { + this.direction = this.getDirectionToPacman(possibleDirections); + } else { + this.direction = possibleDirections[Math.floor(Math.random() * possibleDirections.length)]; + } + } + this.moveCounter = 0; + } + + const dx = [0, 1, 0, -1]; + const dy = [-1, 0, 1, 0]; + + this.pixelX += dx[this.direction] * this.speed * CELL_SIZE; + this.pixelY += dy[this.direction] * this.speed * CELL_SIZE; + + if (this.pixelX < 0) { + this.pixelX = COLS * CELL_SIZE; + } else if (this.pixelX > COLS * CELL_SIZE) { + this.pixelX = 0; + } + + this.x = Math.floor(this.pixelX / CELL_SIZE); + this.y = Math.floor(this.pixelY / CELL_SIZE); + } + + getDirectionToPacman(possibleDirections) { + const dx = [0, 1, 0, -1]; + const dy = [-1, 0, 1, 0]; + + let bestDirection = possibleDirections[0]; + let minDistance = Infinity; + + for (let dir of possibleDirections) { + const nextX = this.x + dx[dir]; + const nextY = this.y + dy[dir]; + + const distance = Math.sqrt( + Math.pow(pacman.x - nextX, 2) + + Math.pow(pacman.y - nextY, 2) + ); + + if (distance < minDistance) { + minDistance = distance; + bestDirection = dir; + } + } + + return bestDirection; + } + + getDirectionToPacman(possibleDirections) { + const dx = [0, 1, 0, -1]; + const dy = [-1, 0, 1, 0]; + + let bestDirection = possibleDirections[0]; + let minDistance = Infinity; + + for (let dir of possibleDirections) { + const nextX = this.x + dx[dir]; + const nextY = this.y + dy[dir]; + + const distance = Math.sqrt( + Math.pow(pacman.x - nextX, 2) + + Math.pow(pacman.y - nextY, 2) + ); + + if (distance < minDistance) { + minDistance = distance; + bestDirection = dir; + } + } + + return bestDirection; + } + + canMove(direction) { + const dx = [0, 1, 0, -1]; + const dy = [-1, 0, 1, 0]; + + const nextX = Math.floor((this.pixelX + dx[direction] * CELL_SIZE * 0.5) / CELL_SIZE); + const nextY = Math.floor((this.pixelY + dy[direction] * CELL_SIZE * 0.5) / CELL_SIZE); + + if (nextX < 0 || nextX >= COLS || nextY < 0 || nextY >= ROWS) { + return true; + } + + return maze[nextY][nextX] !== WALL; + } + + draw() { + ctx.save(); + ctx.translate(this.pixelX, this.pixelY); + + const size = CELL_SIZE * 0.75; + + ctx.fillStyle = this.color; + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 2; + + ctx.beginPath(); + ctx.arc(0, -size * 0.3, size * 0.5, Math.PI, 0, false); + ctx.fill(); + ctx.stroke(); + + ctx.fillRect(-size * 0.5, -size * 0.3, size * 1.0, size * 0.7); + ctx.strokeRect(-size * 0.5, -size * 0.3, size * 1.0, size * 0.7); + + const waveHeight = size * 0.15; + const waveWidth = size * 0.2; + ctx.beginPath(); + ctx.moveTo(-size * 0.5, size * 0.4); + for (let i = 0; i < 5; i++) { + const x = -size * 0.5 + i * waveWidth; + const y = size * 0.4 + (i % 2 === 0 ? 0 : waveHeight); + ctx.lineTo(x, y); + } + ctx.lineTo(size * 0.5, size * 0.4); + ctx.lineTo(size * 0.5, size * 0.7); + ctx.lineTo(-size * 0.5, size * 0.7); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(-size * 0.2, -size * 0.1, size * 0.12, 0, Math.PI * 2); + ctx.arc(size * 0.2, -size * 0.1, size * 0.12, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#000000'; + ctx.beginPath(); + ctx.arc(-size * 0.2, -size * 0.1, size * 0.06, 0, Math.PI * 2); + ctx.arc(size * 0.2, -size * 0.1, size * 0.06, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + } +} + +class Bonus { + constructor(x, y, type) { + this.x = x; + this.y = y; + this.type = type; + this.animation = 0; + } + + update() { + this.animation += 0.1; + if (this.animation > Math.PI * 2) { + this.animation = 0; + } + } + + draw() { + const cellX = this.x * CELL_SIZE + CELL_SIZE / 2; + const cellY = this.y * CELL_SIZE + CELL_SIZE / 2; + const scale = 1 + Math.sin(this.animation) * 0.2; + + ctx.save(); + ctx.translate(cellX, cellY); + ctx.scale(scale, scale); + + if (this.type === BONUS_CHERRY) { + ctx.fillStyle = '#ff0000'; + ctx.beginPath(); + ctx.arc(0, 0, CELL_SIZE * 0.25, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#00ff00'; + ctx.beginPath(); + ctx.arc(-CELL_SIZE * 0.15, -CELL_SIZE * 0.2, CELL_SIZE * 0.1, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = '#00aa00'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(-CELL_SIZE * 0.15, -CELL_SIZE * 0.3); + ctx.lineTo(-CELL_SIZE * 0.25, -CELL_SIZE * 0.4); + ctx.stroke(); + } else if (this.type === BONUS_LUDO) { + const size = CELL_SIZE * 0.45; + + ctx.fillStyle = '#ffd700'; + ctx.strokeStyle = '#ffaa00'; + ctx.lineWidth = 3; + + ctx.beginPath(); + const spikes = 5; + const outerRadius = size; + const innerRadius = size * 0.4; + + for (let i = 0; i < spikes * 2; i++) { + const angle = (i * Math.PI) / spikes; + const radius = i % 2 === 0 ? outerRadius : innerRadius; + const x = Math.cos(angle) * radius; + const y = Math.sin(angle) * radius; + + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = '#ffff00'; + ctx.beginPath(); + ctx.arc(0, 0, size * 0.3, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#ffd700'; + ctx.font = `bold ${size * 0.6}px Arial`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('L', 0, 0); + } + + ctx.restore(); + } +} + +let pacman = new Pacman(); +const ghosts = [ + new Ghost(14, 11, '#ff0000'), + new Ghost(15, 11, '#ff00ff'), + new Ghost(14, 12, '#00ffff'), + new Ghost(15, 12, '#ffa500') +]; + +let bonuses = []; + +function countDots() { + totalDots = 0; + if (!maze || maze.length === 0) { + console.error('countDots() - maze est vide ou undefined'); + return; + } + for (let y = 0; y < ROWS; y++) { + if (!maze[y]) { + console.error(`countDots() - maze[${y}] est undefined`); + continue; + } + for (let x = 0; x < COLS; x++) { + if (maze[y][x] === DOT) { + totalDots++; + } + } + } +} + +function drawMaze() { + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + for (let y = 0; y < ROWS; y++) { + for (let x = 0; x < COLS; x++) { + const cellX = x * CELL_SIZE; + const cellY = y * CELL_SIZE; + + if (maze[y][x] === WALL) { + ctx.fillStyle = '#0000ff'; + ctx.fillRect(cellX, cellY, CELL_SIZE, CELL_SIZE); + ctx.strokeStyle = '#000080'; + ctx.strokeRect(cellX, cellY, CELL_SIZE, CELL_SIZE); + } else if (maze[y][x] === DOT) { + ctx.fillStyle = '#fff'; + ctx.beginPath(); + ctx.arc(cellX + CELL_SIZE / 2, cellY + CELL_SIZE / 2, 2, 0, Math.PI * 2); + ctx.fill(); + } + } + } + + for (let bonus of bonuses) { + bonus.update(); + bonus.draw(); + } +} + +function checkCollisions() { + if (!gameRunning) return; + + for (let ghost of ghosts) { + const distance = Math.sqrt( + Math.pow(pacman.pixelX - ghost.pixelX, 2) + + Math.pow(pacman.pixelY - ghost.pixelY, 2) + ); + + if (distance < CELL_SIZE * 0.6) { + lives--; + updateLivesDisplay(); + + if (lives <= 0) { + gameRunning = false; + statusElement.textContent = 'Game Over !'; + restartBtn.style.display = 'block'; + saveScore(); + } else { + restartCurrentLevel(); + } + } + } +} + +function gameLoop() { + if (isChangingLevel || !gameRunning) { + if (isChangingLevel) { + console.log('gameLoop() - Changement de niveau en cours, arrĂȘt'); + } + return; + } + + drawMaze(); + pacman.update(); + pacman.draw(); + + for (let ghost of ghosts) { + ghost.update(); + ghost.draw(); + } + + checkCollisions(); + + if (gameRunning && !isChangingLevel) { + requestAnimationFrame(gameLoop); + } else { + console.log('gameLoop() - ArrĂȘt de la boucle, gameRunning:', gameRunning, 'isChangingLevel:', isChangingLevel); + } +} + +function updateLivesDisplay() { + const hearts = livesElement.querySelectorAll('.heart'); + hearts.forEach((heart, index) => { + if (index < lives) { + heart.style.opacity = '1'; + } else { + heart.style.opacity = '0.3'; + } + }); +} + +function restartCurrentLevel() { + if (isChangingLevel) return; + + console.log('RedĂ©marrage du niveau actuel'); + isChangingLevel = true; + gameRunning = false; + + requestAnimationFrame(() => { + const currentLevel = level; + const mazeIndex = (currentLevel - 1) % mazeVariants.length; + const currentMaze = mazeVariants[mazeIndex]; + + maze = []; + for (let i = 0; i < currentMaze.length; i++) { + maze[i] = [...currentMaze[i]]; + } + + fillEmptySpaces(); + randomizeMaze(); + countDots(); + + bonuses = []; + placeBonuses(); + + pacman = new Pacman(); + pacman.speed = pacman.baseSpeed * (1 + (level - 1) * 0.05); + ghosts[0] = new Ghost(14, 11, '#ff0000'); + ghosts[1] = new Ghost(15, 11, '#ff00ff'); + ghosts[2] = new Ghost(14, 12, '#00ffff'); + ghosts[3] = new Ghost(15, 12, '#ffa500'); + + for (let ghost of ghosts) { + ghost.updateSpeed(); + } + + cherriesEaten = 0; + cherryEatenRecently = false; + cherryEatenTimer = 0; + + statusElement.textContent = `Niveau ${level} - Recommencement !`; + statusElement.style.color = '#ff6b6b'; + + drawMaze(); + pacman.draw(); + for (let ghost of ghosts) { + ghost.draw(); + } + + setTimeout(() => { + isChangingLevel = false; + gameRunning = true; + statusElement.textContent = 'En jeu'; + statusElement.style.color = '#ffd700'; + gameLoop(); + }, 2000); + }); +} + +function nextLevel() { + console.log('=== nextLevel() appelĂ©e ==='); + console.log('gameRunning:', gameRunning, 'isChangingLevel:', isChangingLevel); + + if (!gameRunning || isChangingLevel) { + console.log('nextLevel() annulĂ©e - gameRunning:', gameRunning, 'isChangingLevel:', isChangingLevel); + return; + } + + console.log('DĂ©but du changement de niveau'); + isChangingLevel = true; + const wasRunning = gameRunning; + gameRunning = false; + console.log('gameRunning mis Ă  false, wasRunning:', wasRunning); + + requestAnimationFrame(() => { + console.log('Dans requestAnimationFrame, changement du niveau'); + level++; + console.log('Nouveau niveau:', level); + levelElement.textContent = level; + cherriesEaten = 0; + + const mazeIndex = (level - 1) % mazeVariants.length; + console.log('Index du labyrinthe:', mazeIndex); + currentMazeIndex = mazeIndex; + const newMaze = mazeVariants[mazeIndex]; + + console.log('CrĂ©ation du nouveau labyrinthe'); + console.log('newMaze.length:', newMaze.length, 'ROWS:', ROWS); + console.log('newMaze[0]?.length:', newMaze[0]?.length, 'COLS:', COLS); + + if (!newMaze || newMaze.length !== ROWS) { + console.error('Erreur: Le nouveau labyrinthe a une taille incorrecte:', newMaze?.length); + isChangingLevel = false; + gameRunning = wasRunning; + return; + } + + maze = []; + for (let i = 0; i < newMaze.length; i++) { + if (!newMaze[i] || newMaze[i].length !== COLS) { + console.error(`Erreur: Ligne ${i} du labyrinthe a une taille incorrecte:`, newMaze[i]?.length); + } + maze[i] = [...newMaze[i]]; + } + + console.log('Labyrinthe copiĂ©, maze.length:', maze.length); + + fillEmptySpaces(); + randomizeMaze(); + + countDots(); + console.log('Total dots:', totalDots); + + bonuses = []; + + console.log('RĂ©initialisation de Pacman et des fantĂŽmes'); + pacman = new Pacman(); + pacman.speed = pacman.baseSpeed * (1 + (level - 1) * 0.05); + ghosts[0] = new Ghost(14, 11, '#ff0000'); + ghosts[1] = new Ghost(15, 11, '#ff00ff'); + ghosts[2] = new Ghost(14, 12, '#00ffff'); + ghosts[3] = new Ghost(15, 12, '#ffa500'); + + for (let ghost of ghosts) { + ghost.updateSpeed(); + } + + placeBonuses(); + console.log('Bonus placĂ©s, nombre:', bonuses.length); + + statusElement.textContent = `Niveau ${level} - Labyrinthe ${mazeIndex + 1} !`; + statusElement.style.color = '#00ff00'; + + console.log('Redessin du labyrinthe'); + drawMaze(); + pacman.draw(); + for (let ghost of ghosts) { + ghost.draw(); + } + + console.log('Attente de 2 secondes avant redĂ©marrage'); + setTimeout(() => { + console.log('RedĂ©marrage de la boucle de jeu'); + isChangingLevel = false; + gameRunning = wasRunning; + console.log('isChangingLevel:', isChangingLevel, 'gameRunning:', gameRunning); + if (gameRunning) { + statusElement.textContent = 'En jeu'; + statusElement.style.color = '#ffd700'; + console.log('Appel de gameLoop() pour redĂ©marrer'); + gameLoop(); + } + }, 2000); + }); +} + +function fillEmptySpaces() { + console.log('Remplissage de tous les espaces vides avec des chemins'); + + // PremiĂšre passe : remplir tous les empty + for (let y = 1; y < ROWS - 1; y++) { + for (let x = 1; x < COLS - 1; x++) { + if (maze[y][x] === EMPTY) { + maze[y][x] = DOT; + } + } + } + + // DĂ©tecter et remplir les grandes zones vides + for (let iteration = 0; iteration < 6; iteration++) { + for (let y = 1; y < ROWS - 1; y++) { + for (let x = 1; x < COLS - 1; x++) { + if (maze[y][x] === WALL) { + const neighbors = [ + maze[y-1][x], maze[y+1][x], maze[y][x-1], maze[y][x+1] + ]; + const emptyCount = neighbors.filter(c => c === EMPTY).length; + const dotCount = neighbors.filter(c => c === DOT).length; + const wallCount = neighbors.filter(c => c === WALL).length; + + // Si entourĂ© d'espaces vides ou de beaucoup de chemins, transformer en chemin + if (emptyCount >= 1) { + maze[y][x] = DOT; + } else if (wallCount <= 1 && dotCount >= 2) { + if (Math.random() < 0.9) { + maze[y][x] = DOT; + } + } else if (wallCount === 0) { + maze[y][x] = DOT; + } + } else if (maze[y][x] === EMPTY) { + maze[y][x] = DOT; + } + } + } + + // DĂ©tecter les grandes zones vides (empty) et les remplir + for (let y = 2; y < ROWS - 2; y++) { + for (let x = 2; x < COLS - 2; x++) { + if (maze[y][x] === EMPTY) { + let emptyArea = 0; + const visited = new Set(); + const stack = [[x, y]]; + + while (stack.length > 0 && emptyArea < 20) { + const [cx, cy] = stack.pop(); + const key = `${cy},${cx}`; + if (visited.has(key)) continue; + visited.add(key); + + if (maze[cy][cx] === EMPTY) { + emptyArea++; + if (cy > 1) stack.push([cx, cy-1]); + if (cy < ROWS-2) stack.push([cx, cy+1]); + if (cx > 1) stack.push([cx-1, cy]); + if (cx < COLS-2) stack.push([cx+1, cy]); + } + } + + if (emptyArea >= 5) { + for (const key of visited) { + const [cy, cx] = key.split(',').map(Number); + if (maze[cy][cx] === EMPTY) { + maze[cy][cx] = DOT; + } + } + } + } + } + } + } + + // CrĂ©er une petite cavitĂ© au centre seulement + const centerX = Math.floor(COLS / 2); + const centerY = Math.floor(ROWS / 2); + + if (maze[centerY] && maze[centerY][centerX] !== WALL) { + maze[centerY][centerX] = EMPTY; + } + + console.log('Tous les grands espaces rĂ©duits, chemins denses créés'); +} + +function randomizeMaze() { + const modificationRate = 0.05; + const changes = Math.floor(ROWS * COLS * modificationRate); + + console.log('Modification alĂ©atoire du labyrinthe,', changes, 'changements'); + + const centerX = Math.floor(COLS / 2); + const centerY = Math.floor(ROWS / 2); + + for (let i = 0; i < changes; i++) { + const x = Math.floor(Math.random() * COLS); + const y = Math.floor(Math.random() * ROWS); + + if (x === 0 || x === COLS - 1 || y === 0 || y === ROWS - 1) { + continue; + } + + const distX = Math.abs(x - centerX); + const distY = Math.abs(y - centerY); + + if (distX <= 1 && distY <= 1) { + continue; + } + + const currentCell = maze[y][x]; + + if (currentCell === WALL) { + if (Math.random() < 0.8) { + maze[y][x] = DOT; + } + } else if (currentCell === EMPTY) { + maze[y][x] = DOT; + } + } + + // Remplir tous les empty restants + for (let y = 1; y < ROWS - 1; y++) { + for (let x = 1; x < COLS - 1; x++) { + const distX = Math.abs(x - centerX); + const distY = Math.abs(y - centerY); + + if (distX > 1 || distY > 1) { + if (maze[y][x] === EMPTY) { + maze[y][x] = DOT; + } + } + } + } + + // Transformer les murs isolĂ©s en chemins + for (let iteration = 0; iteration < 3; iteration++) { + for (let y = 1; y < ROWS - 1; y++) { + for (let x = 1; x < COLS - 1; x++) { + const distX = Math.abs(x - centerX); + const distY = Math.abs(y - centerY); + + if (distX <= 1 && distY <= 1) { + continue; + } + + if (maze[y][x] === WALL) { + const neighbors = [ + maze[y-1][x], maze[y+1][x], maze[y][x-1], maze[y][x+1] + ]; + const wallCount = neighbors.filter(c => c === WALL).length; + const dotCount = neighbors.filter(c => c === DOT).length; + + if (wallCount <= 1 && dotCount >= 2 && Math.random() < 0.85) { + maze[y][x] = DOT; + } else if (wallCount === 0) { + maze[y][x] = DOT; + } + } + } + } + } + + console.log('Labyrinthe modifiĂ© avec chemins denses, grands espaces rĂ©duits'); +} + +function placeBonuses() { + bonuses = []; + const bonusPositions = [ + {x: 1, y: 1, type: BONUS_CHERRY}, + {x: 28, y: 1, type: BONUS_CHERRY}, + {x: 1, y: 28, type: BONUS_CHERRY}, + {x: 28, y: 28, type: BONUS_CHERRY}, + {x: 14, y: 14, type: BONUS_LUDO}, + {x: 15, y: 14, type: BONUS_LUDO} + ]; + + for (let pos of bonusPositions) { + if (maze[pos.y][pos.x] === EMPTY || maze[pos.y][pos.x] === DOT) { + maze[pos.y][pos.x] = pos.type; + bonuses.push(new Bonus(pos.x, pos.y, pos.type)); + } + } +} + +function initGame() { + currentMazeIndex = 0; + originalMaze = mazeVariants[0]; + maze = originalMaze.map(row => [...row]); + countDots(); + score = 0; + level = 1; + lives = 3; + cherriesEaten = 0; + scoreElement.textContent = score; + levelElement.textContent = level; + updateLivesDisplay(); + gameRunning = true; + statusElement.textContent = 'En jeu'; + statusElement.style.color = '#ffd700'; + restartBtn.style.display = 'none'; + + pacman = new Pacman(); + pacman.speed = pacman.baseSpeed * (1 + (level - 1) * 0.05); + ghosts[0] = new Ghost(14, 11, '#ff0000'); + ghosts[1] = new Ghost(15, 11, '#ff00ff'); + ghosts[2] = new Ghost(14, 12, '#00ffff'); + ghosts[3] = new Ghost(15, 12, '#ffa500'); + + for (let ghost of ghosts) { + ghost.updateSpeed(); + } + + placeBonuses(); + countDots(); + gameLoop(); +} + +document.addEventListener('keydown', (e) => { + if (!gameRunning) return; + + switch(e.key) { + case 'ArrowUp': + pacman.nextDirection = 0; + e.preventDefault(); + break; + case 'ArrowRight': + pacman.nextDirection = 1; + e.preventDefault(); + break; + case 'ArrowDown': + pacman.nextDirection = 2; + e.preventDefault(); + break; + case 'ArrowLeft': + pacman.nextDirection = 3; + e.preventDefault(); + break; + } +}); + +function getScores() { + const scoresJson = localStorage.getItem('pacmanScores'); + return scoresJson ? JSON.parse(scoresJson) : []; +} + +function saveScore() { + const username = usernameInput.value.trim() || 'Anonyme'; + if (score > 0) { + const scores = getScores(); + scores.push({ + username: username, + score: score, + date: new Date().toISOString() + }); + scores.sort((a, b) => b.score - a.score); + const topScores = scores.slice(0, 10); + localStorage.setItem('pacmanScores', JSON.stringify(topScores)); + updateLeaderboard(); + } +} + +function updateLeaderboard() { + const scores = getScores(); + leaderboardElement.innerHTML = ''; + + if (scores.length === 0) { + leaderboardElement.innerHTML = '
Aucun score enregistré
'; + return; + } + + scores.forEach((entry, index) => { + const item = document.createElement('div'); + item.className = 'leaderboard-item' + (index < 3 ? ' top' : ''); + + const rank = document.createElement('div'); + rank.className = 'leaderboard-rank'; + rank.textContent = (index + 1) + '.'; + + const name = document.createElement('div'); + name.className = 'leaderboard-name'; + name.textContent = entry.username; + + const scoreDiv = document.createElement('div'); + scoreDiv.className = 'leaderboard-score'; + scoreDiv.textContent = entry.score; + + item.appendChild(rank); + item.appendChild(name); + item.appendChild(scoreDiv); + leaderboardElement.appendChild(item); + }); +} + +restartBtn.addEventListener('click', () => { + initGame(); +}); + +updateLeaderboard(); +initGame(); + diff --git a/index.html b/index.html new file mode 100644 index 0000000..2b7eb58 --- /dev/null +++ b/index.html @@ -0,0 +1,40 @@ + + + + + + Jeu Oulvic + + + +
+
+

OULVIC

+
+ + +
+
+
Score: 0
+
Niveau: 1
+
Vies: ♄♄♄
+
PrĂȘt Ă  jouer
+
+ +
+

Utilisez les flÚches directionnelles pour déplacer Oulvic

+ +
+
+
+

Classement

+
+
+
+
+

By Ludo and Syoul

+
+ + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..92a8593 --- /dev/null +++ b/style.css @@ -0,0 +1,302 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: #0a0a0a; + background-image: + radial-gradient(circle at 20% 30%, rgba(255, 0, 0, 0.1) 0%, transparent 50%), + radial-gradient(circle at 80% 70%, rgba(0, 0, 255, 0.1) 0%, transparent 50%), + radial-gradient(circle at 50% 50%, rgba(128, 0, 128, 0.1) 0%, transparent 50%); + color: #fff; + min-height: 100vh; + padding: 20px; + position: relative; + overflow-x: hidden; +} + +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + url("data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M50 20 Q60 10 70 20 Q80 30 70 40 Q60 50 50 40 Q40 50 30 40 Q20 30 30 20 Q40 10 50 20 Z' fill='%23ffffff' opacity='0.05'/%3E%3Ccircle cx='45' cy='30' r='3' fill='%23000000' opacity='0.3'/%3E%3Ccircle cx='55' cy='30' r='3' fill='%23000000' opacity='0.3'/%3E%3Cpath d='M50 40 Q45 45 50 50 Q55 45 50 40' fill='%23000000' opacity='0.2'/%3E%3C/svg%3E"); + background-size: 200px 200px; + background-repeat: repeat; + pointer-events: none; + z-index: 0; +} + +body::after { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: + repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(255, 255, 255, 0.02) 2px, + rgba(255, 255, 255, 0.02) 4px + ); + pointer-events: none; + z-index: 0; +} + +.main-wrapper { + display: flex; + justify-content: center; + align-items: flex-start; + gap: 30px; + max-width: 1400px; + margin: 0 auto; + position: relative; + z-index: 1; +} + +.container { + text-align: center; + background: rgba(0, 0, 0, 0.7); + padding: 30px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8), 0 0 50px rgba(255, 0, 0, 0.2); + flex-shrink: 0; + position: relative; + z-index: 1; + border: 2px solid rgba(255, 0, 0, 0.3); +} + +h1 { + font-size: 3em; + margin-bottom: 20px; + text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.5); + color: #ffd700; +} + +.game-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding: 10px 20px; + background: rgba(0, 0, 0, 0.2); + border-radius: 10px; + gap: 15px; + flex-wrap: wrap; +} + +.score { + font-size: 1.5em; + font-weight: bold; +} + +.level { + font-size: 1.5em; + font-weight: bold; + color: #ffd700; +} + +.lives { + font-size: 1.5em; + font-weight: bold; +} + +.lives .heart { + color: #ff0000; + font-size: 1.2em; + margin: 0 2px; + transition: opacity 0.3s; +} + +#status { + font-size: 1.2em; + color: #ffd700; +} + +#gameCanvas { + border: 3px solid #ffd700; + border-radius: 10px; + background: #000; + display: block; + margin: 0 auto; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); +} + +.user-input-section { + margin-bottom: 15px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.user-input-section label { + font-size: 1.1em; + font-weight: bold; +} + +#username { + padding: 8px 15px; + font-size: 1em; + border: 2px solid #ffd700; + border-radius: 8px; + background: rgba(0, 0, 0, 0.3); + color: #fff; + outline: none; + max-width: 200px; +} + +#username:focus { + border-color: #ffed4e; + box-shadow: 0 0 10px rgba(255, 215, 0, 0.5); +} + +.instructions { + margin-top: 20px; + font-size: 1.1em; +} + +.leaderboard-container { + background: rgba(0, 0, 0, 0.7); + padding: 25px; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8), 0 0 50px rgba(255, 0, 0, 0.2); + min-width: 300px; + max-height: 700px; + position: relative; + z-index: 1; + border: 2px solid rgba(255, 0, 0, 0.3); +} + +.leaderboard-container h2 { + color: #ffd700; + text-align: center; + margin-bottom: 20px; + font-size: 2em; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); +} + +#leaderboard { + max-height: 600px; + overflow-y: auto; +} + +.leaderboard-item { + background: rgba(255, 255, 255, 0.1); + padding: 12px; + margin-bottom: 10px; + border-radius: 8px; + display: flex; + justify-content: space-between; + align-items: center; + border-left: 4px solid #ffd700; +} + +.leaderboard-item.top { + background: rgba(255, 215, 0, 0.2); + border-left-color: #ffed4e; + font-weight: bold; +} + +.leaderboard-rank { + font-size: 1.3em; + font-weight: bold; + color: #ffd700; + min-width: 30px; +} + +.leaderboard-name { + flex: 1; + text-align: left; + margin-left: 15px; + font-size: 1.1em; +} + +.leaderboard-score { + font-size: 1.2em; + font-weight: bold; + color: #ffd700; + min-width: 80px; + text-align: right; +} + +.empty-leaderboard { + text-align: center; + color: #aaa; + padding: 20px; + font-style: italic; +} + +#restartBtn { + margin-top: 15px; + padding: 12px 30px; + font-size: 1.2em; + background: #ffd700; + color: #000; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s; +} + +#restartBtn:hover { + background: #ffed4e; +} + +#restartBtn:active { + transform: scale(0.95); +} + +footer { + text-align: center; + margin-top: 30px; + padding: 20px; + color: #ffd700; + font-size: 1.1em; + font-weight: bold; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); +} + +@media (max-width: 1200px) { + .main-wrapper { + flex-direction: column; + align-items: center; + } + + .leaderboard-container { + width: 100%; + max-width: 600px; + } +} + +@media (max-width: 700px) { + #gameCanvas { + width: 100%; + height: auto; + } + + h1 { + font-size: 2em; + } + + .game-info { + flex-direction: column; + gap: 10px; + } + + .user-input-section { + flex-direction: column; + gap: 8px; + } +} +