diff --git a/package-lock.json b/package-lock.json index 28ea435..081b362 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "npm-run-all2": "^6.2.3", "prettier": "^3.3.3", "typescript": "~5.5.4", - "vite": "^5.4.8", + "vite": "^5.4.10", "vite-plugin-vue-devtools": "^7.4.6", "vue-tsc": "^2.1.6" } @@ -4553,7 +4553,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index cacc175..9fc718a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "npm-run-all2": "^6.2.3", "prettier": "^3.3.3", "typescript": "~5.5.4", - "vite": "^5.4.8", + "vite": "^5.4.10", "vite-plugin-vue-devtools": "^7.4.6", "vue-tsc": "^2.1.6" } diff --git a/public/favicon.ico b/public/favicon.ico index df36fcf..e3f0861 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/src/App.vue b/src/App.vue index c4884a4..a9d340b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -24,42 +24,3 @@ - - diff --git a/src/assets/main.css b/src/assets/main.css index ef4840b..cec489f 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -45,38 +45,197 @@ a, } } +/* APP HEADER */ +header { + position: fixed; + z-index: 2; + height: 40px; + width: 100%; + padding: 10px; + background-color: #333; + color: #fff; + + * { + display: inline-block; + vertical-align: middle; + } + + h1 { + text-align: center; + left: 0; + width: 200px; + font-size: 22px; + } + + h2 { + text-align: left; + left: 220px; + font-size: medium; + } + + nav { + right: 100px; + font-size: 20px; + } + + nav * { + margin-left: 20px; + } +} + + /* SIDEBAR NAVIGATION */ .sidenav { - position: fixed; + position: absolute; z-index: 1; - width: 203px; /* 200px is the width of the header vl position */ + width: 203px; /* 200px is the width of the header vl position + half the bar */ background-color: #111; -} -.sidenav a { - padding: 6px 8px 6px 16px; - text-decoration: none; - font-size: 25px; - color: #818181; - display: block; -} + a { + padding: 6px 8px 6px 16px; + text-decoration: none; + font-size: 25px; + color: #818181; + display: block; + + } -.sidenav a:hover { - color: #f1f1f1; + a:hover { + color: #f1f1f1; + } } /* PAGE CONTENT */ .main { - margin-top: 40px; /* 60px is the height of the header navbar */ - height: calc(100% - 40px); - padding: 2rem 2rem; display: flex; flex-direction: column; - align-items: center; justify-content: center; - gap: 10px; + align-items: center; + overflow-x: auto; + width: 100%; + height: calc(100% - 40px); + margin-top: 40px; /* 40px is the height of the header navbar */ + padding: 2rem; + gap: 20px; } +/* GAME BOARD */ +.space { + display: flex; + justify-content: end; /* Centre les deux éléments horizontalement */ + width: 100%; + gap: 20px; /* Ajoute un espacement entre le h1 et le timer */ + + h1 { + font-weight: bold; + font-size: 20px; + } +} + +.timer { + text-align: center; + width: 135px; + border-radius: 10px; + border: 2px solid #555; + background-color: #333; + color: white; + font-weight: bold; + font-size: 22px; +} + +.menu { + display: flex; + justify-content: space-between; /* Aligne les deux groupes à gauche et à droite */ + width: 100%; /* Prend toute la largeur disponible */ + margin-top: -5px; + margin-bottom: 10px; + + button { + cursor: pointer; + height: 36px; + color: white; + border-radius: 10px; + font-size: 16px; + } +} + +.presets { + flex: auto; + display: flex; + align-items: center; + gap: 10px; /* Ajoute un espacement entre les boutons */ + + button { + width: 75px; + height: 50px; + background-color: #3f77ff; + border: 2px solid #7aa0ff; + + } + button:hover { + background-color: #1c5dff; + } +} + +.inputs { + display: flex; + align-items: center; + gap: 10px; /* Ajoute un espacement entre les inputs */ + margin-right: 20px; + + div { + display: flex; + flex-direction: column; + align-items: center; + } + + label { + font-weight: bold; + font-size: 14px; + } + + input { + text-align: center; + width: 80px; + padding: 5px; + border: 1px solid #ccc; + border-radius: 5px; + } +} + +.newgame { + width: 135px; + margin-top: 15px; + background-color: #4CAF50; + border: 2px solid #6bdf6f; +} + +.newgame:hover { + background-color: #3b8a3f; +} + +.grid { + display: grid; + border: 4px solid rgb(65, 65, 65); + margin: 0 auto; /* Centre la grille horizontalement */ + overflow-x: auto; /* Active le défilement horizontal si nécessaire */ + max-width: 100%; /* Empêche la grille de déborder trop */ +} + +.row { + display: flex; +} + +.cell { + background-color: #f1f1f1; + text-align: center; + color: black; + width: 32px; + height: 32px; + user-select: none; +} + +/* FORMATTING AND EFFECTS */ .rounded { border-radius: 20%; } @@ -99,6 +258,20 @@ a, border-left: 6px solid rgb(0, 255, 140); } +.message { + position: absolute; + z-index: 1000; + color: white; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 8px; + padding: 10px; + transform: translate(-50%, 50%); + pointer-events: none; + white-space: nowrap; + font-weight: bold; + font-size: 18px; +} + /* ROUTE HIGHLIGHT */ .router-link-exact-active { border-left: 5px solid rgb(0, 255, 140); diff --git a/src/utils/game.ts b/src/utils/game.ts index e1d0b5b..e496205 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -1,6 +1,6 @@ -type CellGrid = string[][] // type CellGrid (grille de jeu) -type MineGrid = number[][] // type MineGrid (grille de bombes) -type Directions = number[][] // type Directions (directions possibles) +type CellGrid = string[][] // type CellGrid (grille de jeu) +type MineGrid = number[][] // type MineGrid (grille de bombes) +type Directions = number[][] // type Directions (directions possibles) function getDirections(x: number, y: number): Directions { return [[x-1, y-1], [x-1, y], [x-1, y+1], [x, y-1], [x, y+1], [x+1, y-1], [x+1, y], [x+1, y+1]] @@ -27,14 +27,23 @@ function genMineGrid(width: number, length: number, nbMines: number, fx: number, } -function cliqueGauche(gameStatus: number, cellGrid: CellGrid, mineGrid: MineGrid, x: number, y: number) { +function cliqueGauche(cellGrid: CellGrid, mineGrid: MineGrid, x: number, y: number): number { const checkDrop = ['/sEmpty.png', '/sUnknown.png', '/sFlag.png', '/sQuestion.png'] - if (checkDrop.includes(cellGrid[y][x])) return - else if (cellGrid[y][x] === '/sClick.png') checkCell(gameStatus, cellGrid, mineGrid, x, y) - else revealCell(gameStatus, cellGrid, mineGrid, x, y) + if (checkDrop.includes(cellGrid[y][x])) return 1 + else if (cellGrid[y][x] === '/sClick.png') { + const result = checkCell(cellGrid, mineGrid, x, y) + if (result !== 0) return result + } + else { + const result = revealCell(cellGrid, mineGrid, x, y) + if (result !== 0) return result + } + + // Continuer le jeu + return 1 } -function cliqueDroit(cellGrid: string[][], x: number, y: number) { // fonction cliqueDroit (flag d'une case) // modification de l'image de la case +function cliqueDroit(cellGrid: string[][], x: number, y: number) { // fonction cliqueDroit (flag d'une case) switch (cellGrid[y][x]) { case '/sUnknown.png': cellGrid[y][x] = "/sFlag.png" @@ -48,9 +57,10 @@ function cliqueDroit(cellGrid: string[][], x: number, y: number) { } } -function checkCell(gameStatus: number, cellGrid: CellGrid, mineGrid: MineGrid, x: number, y: number) { - if (mineGrid.some(m => m[0] === x && m[1] === y)) return gameOver(gameStatus, cellGrid, mineGrid, x, y) +function checkCell(cellGrid: CellGrid, mineGrid: MineGrid, x: number, y: number): number { + if (mineGrid.some(m => m[0] === x && m[1] === y)) return gameOver(cellGrid, mineGrid, x, y) + // Compter le nombre de mines adjacentes let minesAdjacentes = 0 const directions = getDirections(x, y) for (const [dx, dy] of directions) { @@ -58,18 +68,34 @@ function checkCell(gameStatus: number, cellGrid: CellGrid, mineGrid: MineGrid, x if (mineGrid.some(m => m[0] === dx && m[1] === dy)) minesAdjacentes++ } - if (minesAdjacentes > 0) cellGrid[y][x] = `/s${minesAdjacentes}.png` - else { + // Si la cellule a des mines adjacentes, afficher le nombre de mines + if (minesAdjacentes > 0) { + cellGrid[y][x] = `/s${minesAdjacentes}.png` + + const result = revealCell(cellGrid, mineGrid, x, y) + if (result !== 0) return result + } else { cellGrid[y][x] = '/sEmpty.png' for (const [dx, dy] of directions) { if (dx >= 0 && dx < cellGrid[0].length && dy >= 0 && dy < cellGrid.length && cellGrid[dy][dx] === '/sUnknown.png') { - checkCell(gameStatus, cellGrid, mineGrid, dx, dy) + const result = checkCell(cellGrid, mineGrid, dx, dy) + if (result !== 0) return result } } } + + // Vérifier si toutes les cellules non-mines sont révélées pour déterminer la victoire + if (cellGrid.flat().every((cell, index) => { + const x = index % cellGrid[0].length + const y = Math.floor(index / cellGrid[0].length) + return cell !== '/sUnknown.png' || mineGrid.some(m => m[0] === x && m[1] === y) + })) return 2 + + // Continuer le jeu + return 0 } -function revealCell(gameStatus: number, cellGrid: CellGrid, mineGrid: MineGrid, x: number, y: number) { +function revealCell(cellGrid: CellGrid, mineGrid: MineGrid, x: number, y: number): number { const directions = getDirections(x, y) let flagCount = 0 for (const [dx, dy] of directions) { @@ -78,18 +104,24 @@ function revealCell(gameStatus: number, cellGrid: CellGrid, mineGrid: MineGrid, const cellValue = parseInt(cellGrid[y][x].match(/\/s(\d)\.png$/)?.[1] || '0') if (flagCount === cellValue) { for (const [dx, dy] of directions) { - if (dx >= 0 && dx < cellGrid[0].length && dy >= 0 && dy < cellGrid.length && cellGrid[dy][dx] === '/sHighlight.png') { + if (dx >= 0 && dx < cellGrid[0].length && dy >= 0 && dy < cellGrid.length && (cellGrid[dy][dx] === '/sHighlight.png' || cellGrid[dy][dx] === '/sUnknown.png')) { cellGrid[dy][dx] = '/sUnknown.png' - checkCell(gameStatus, cellGrid, mineGrid, dx, dy) + + const result = checkCell(cellGrid, mineGrid, dx, dy) + if (result !== 0) return result } } } + + // Continuer le jeu + return 0 } -function gameOver(gameStatus: number, cellGrid: CellGrid, mineGrid: MineGrid, x: number, y: number) { +function gameOver(cellGrid: CellGrid, mineGrid: MineGrid, x: number, y: number): number { for (const [mx, my] of mineGrid) { if (cellGrid[my][mx] === '/sUnknown.png') cellGrid[my][mx] = '/sMine.png' } + // Vérifier l'emplacement de tous les flags dans toute la grille et les remplacer par '/sFlagWrong.png' s'il y en a un qui n'est pas sur une bombe for (let y = 0; y < cellGrid.length; y++) { for (let x = 0; x < cellGrid[y].length; x++) { @@ -98,8 +130,8 @@ function gameOver(gameStatus: number, cellGrid: CellGrid, mineGrid: MineGrid, x: } cellGrid[y][x] = '/sExploded.png' - gameStatus = 3 + return 3 } -export { getDirections, genCellGrid, genMineGrid, cliqueGauche, cliqueDroit } // export des fonctions -export type { Directions, CellGrid, MineGrid } // export des types \ No newline at end of file +export { getDirections, genCellGrid, genMineGrid, cliqueGauche, cliqueDroit } // export des fonctions +export type { Directions, CellGrid, MineGrid } // export des types \ No newline at end of file diff --git a/src/views/Game/PartyView.vue b/src/views/Game/PartyView.vue index e0d25f3..e766b76 100644 --- a/src/views/Game/PartyView.vue +++ b/src/views/Game/PartyView.vue @@ -1,5 +1,13 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/src/views/Game/SoloView.vue b/src/views/Game/SoloView.vue index 0f3f4d0..e1d8d6e 100644 --- a/src/views/Game/SoloView.vue +++ b/src/views/Game/SoloView.vue @@ -4,13 +4,13 @@ import { getDirections, genCellGrid, genMineGrid, cliqueGauche, cliqueDroit } fr import type { CellGrid, MineGrid } from '@/utils/game' // Variables du jeu -const width = 10 -const length = 15 -const nbMines = 25 +const width = ref(10) +const length = ref(15) +const nbMines = ref(20) // Création grille des cases const cellGrid = ref([]) -cellGrid.value = genCellGrid(width, length) +cellGrid.value = genCellGrid(width.value, length.value) // Création grille des mines let mineGrid: MineGrid = [] @@ -18,18 +18,45 @@ let mineGrid: MineGrid = [] // État du jeu const gameStatus = ref(0) // 0: vide, 1: en cours, 2: gagné, 3: perdu const timer = ref(0) -let timerInterval: number | undefined = undefined +let timerInterval: number | null = null +// Fonction pour démarrer une nouvelle partie +const startNewGame = () => { + gameStatus.value = 0 + timer.value = 0 + cellGrid.value = genCellGrid(width.value, length.value) +} + +// Formater le timer en heure:minute:seconde +const formatTime = (seconds: number) => { + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const secs = seconds % 60 + + // Si les heures sont 0, ne pas les afficher + const hoursDisplay = hours > 0 ? `${hours}:` : '' + // Afficher les minutes avec un padding de 0 si elles sont inférieures à 10 + const minutesDisplay = `${hours > 0 ? String(minutes).padStart(2, '0') : minutes}:` + // Toujours afficher les secondes avec un padding de 0 + const secondsDisplay = String(secs).padStart(2, '0') + + return `${hoursDisplay}${minutesDisplay}${secondsDisplay}` +} + +// Watch pour démarrer ou arrêter le timer selon l'état du jeu watch(gameStatus, async (status) => { - if (status === 1) { - timerInterval = setInterval(() => timer.value++, 1000) - } else { - if (timerInterval !== null) clearInterval(timerInterval) - if (status === 2) alert('Vous avez gagné !') - if (status === 3) alert('Vous avez perdu !') + if (status === 0) { + timer.value = 0 + cellGrid.value = genCellGrid(width.value, length.value) } + if (status === 1) timerInterval = setInterval(() => timer.value++, 1000) + else if (timerInterval) clearInterval(timerInterval) }) +// Variables pour stocker la position du curseur +const mouseX = ref(0) +const mouseY = ref(0) + // État pour gérer le clic et la position de la cellule const isMouseDown = ref(false) const currentCell = ref<{ rowIndex: number, cellIndex: number } | null>(null) @@ -51,12 +78,12 @@ const handleMouseDown = (rowIndex: number, cellIndex: number) => { // Gestion des événements de relâchement de clic et de la position de la cellule const handleMouseUp = async (rowIndex: number, cellIndex: number) => { isMouseDown.value = false - if (!mineGrid.length) { - mineGrid = genMineGrid(width, length, nbMines, cellIndex, rowIndex) + if (!gameStatus.value) { + mineGrid = genMineGrid(width.value, length.value, nbMines.value, cellIndex, rowIndex) gameStatus.value = 1 } - cliqueGauche(gameStatus.value, cellGrid.value, mineGrid, cellIndex, rowIndex) + gameStatus.value = cliqueGauche(cellGrid.value, mineGrid, cellIndex, rowIndex) currentCell.value = null // Réinitialiser les cases surlignées @@ -91,6 +118,9 @@ const handleMouseMove = (rowIndex: number, cellIndex: number) => { // Gestion des événements de déplacement de la souris et de la position de la cellule const handleGlobalMouseMove = (event: MouseEvent) => { + mouseX.value = event.clientX + mouseY.value = event.clientY + if (isMouseDown.value && currentCell.value) { const target = event.target as HTMLElement @@ -125,49 +155,62 @@ onUnmounted(() => document.removeEventListener('mousemove', handleGlobalMouseMov \ No newline at end of file diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 06c8ba3..50df5ee 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -1,11 +1,18 @@