From 209877f1cdefa5f26c5ca28dfcbf944696d07fff Mon Sep 17 00:00:00 2001 From: hieunv Date: Thu, 20 Feb 2025 22:56:32 +0700 Subject: [PATCH] add minesweeper game page --- docusaurus.config.js | 2 +- src/pages/minesweeper/index.js | 219 ++++++++++++++++++++++++ src/pages/minesweeper/styles.module.css | 218 +++++++++++++++++++++++ 3 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 src/pages/minesweeper/index.js create mode 100644 src/pages/minesweeper/styles.module.css diff --git a/docusaurus.config.js b/docusaurus.config.js index 416b1d0..c17119d 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -74,7 +74,7 @@ module.exports = { position: "right", }, { to: "blog/", label: "Blog", position: "right" }, - { to: "projects/", label: "Projects", position: "right" }, + { to: "minesweeper/", label: "Games", position: "right" }, { href: "https://behitek.com/pdf/resume.pdf", label: "Resume", diff --git a/src/pages/minesweeper/index.js b/src/pages/minesweeper/index.js new file mode 100644 index 0000000..faa6fe6 --- /dev/null +++ b/src/pages/minesweeper/index.js @@ -0,0 +1,219 @@ +import Layout from '@theme/Layout'; +import React, { useEffect, useState } from 'react'; +import styles from './styles.module.css'; + +const DIFFICULTIES = { + easy: { rows: 8, cols: 8, mines: 10 }, + medium: { rows: 16, cols: 16, mines: 40 }, + hard: { rows: 16, cols: 30, mines: 99 } +}; + +const createBoard = (rows, cols, mines) => { + const board = Array(rows).fill().map(() => + Array(cols).fill().map(() => ({ + isMine: false, + isRevealed: false, + isFlagged: false, + neighborMines: 0 + })) + ); + + // Place mines randomly + let minesPlaced = 0; + while (minesPlaced < mines) { + const row = Math.floor(Math.random() * rows); + const col = Math.floor(Math.random() * cols); + if (!board[row][col].isMine) { + board[row][col].isMine = true; + minesPlaced++; + } + } + + // Calculate neighbor mines + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + if (!board[row][col].isMine) { + let count = 0; + for (let i = -1; i <= 1; i++) { + for (let j = -1; j <= 1; j++) { + if (row + i >= 0 && row + i < rows && col + j >= 0 && col + j < cols) { + if (board[row + i][col + j].isMine) count++; + } + } + } + board[row][col].neighborMines = count; + } + } + } + + return board; +}; + +const MinesweeperGame = () => { + const [difficulty, setDifficulty] = useState('medium'); + const [board, setBoard] = useState([]); + const [gameOver, setGameOver] = useState(false); + const [gameWon, setGameWon] = useState(false); + const [flagsPlaced, setFlagsPlaced] = useState(0); + const [time, setTime] = useState(0); + const [timerActive, setTimerActive] = useState(false); + + useEffect(() => { + resetGame(); + }, [difficulty]); + + useEffect(() => { + let interval; + if (timerActive && !gameOver && !gameWon) { + interval = setInterval(() => { + setTime(prevTime => prevTime + 1); + }, 1000); + } + return () => clearInterval(interval); + }, [timerActive, gameOver, gameWon]); + + const resetGame = () => { + const { rows, cols, mines } = DIFFICULTIES[difficulty]; + setBoard(createBoard(rows, cols, mines)); + setGameOver(false); + setGameWon(false); + setFlagsPlaced(0); + setTime(0); + setTimerActive(false); + }; + + const revealCell = (row, col) => { + if (gameOver || gameWon || board[row][col].isRevealed || board[row][col].isFlagged) return; + + if (!timerActive) { + setTimerActive(true); + } + + const newBoard = [...board]; + + if (newBoard[row][col].isMine) { + // Game Over - reveal all mines + newBoard.forEach(row => row.forEach(cell => { + if (cell.isMine) cell.isRevealed = true; + })); + setGameOver(true); + } else { + // Reveal clicked cell and neighbors if empty + const revealNeighbors = (r, c) => { + if (r < 0 || r >= board.length || c < 0 || c >= board[0].length) return; + if (newBoard[r][c].isRevealed || newBoard[r][c].isFlagged) return; + + newBoard[r][c].isRevealed = true; + + if (newBoard[r][c].neighborMines === 0) { + for (let i = -1; i <= 1; i++) { + for (let j = -1; j <= 1; j++) { + revealNeighbors(r + i, c + j); + } + } + } + }; + + revealNeighbors(row, col); + } + + setBoard(newBoard); + + // Check for win + const unrevealedCount = newBoard.flat().filter(cell => !cell.isRevealed).length; + if (unrevealedCount === DIFFICULTIES[difficulty].mines) { + setGameWon(true); + } + }; + + const toggleFlag = (row, col, e) => { + e.preventDefault(); + if (gameOver || gameWon || board[row][col].isRevealed) return; + + const newBoard = [...board]; + const cell = newBoard[row][col]; + + if (!cell.isFlagged && flagsPlaced >= DIFFICULTIES[difficulty].mines) return; + + cell.isFlagged = !cell.isFlagged; + setFlagsPlaced(prev => cell.isFlagged ? prev + 1 : prev - 1); + setBoard(newBoard); + }; + + const renderCell = (cell, row, col) => { + let content = ''; + let className = styles.cell; + + if (cell.isRevealed) { + className += ` ${styles.revealed}`; + if (cell.isMine) { + content = '💣'; + className += ` ${styles.mine}`; + } else if (cell.neighborMines > 0) { + content = cell.neighborMines; + className += ` ${styles[`number-${cell.neighborMines}`]}`; + } + } else if (cell.isFlagged) { + content = '🚩'; + className += ` ${styles.flag}`; + } + + return ( + + ); + }; + + return ( + +
+
+

Minesweeper

+
+ +
+
+ {Object.keys(DIFFICULTIES).map(mode => ( + + ))} + +
+ +
+ 💣 {DIFFICULTIES[difficulty].mines - flagsPlaced} + ⏱️ {String(Math.floor(time / 60)).padStart(2, '0')}:{String(time % 60).padStart(2, '0')} + + {gameOver ? '💥 Game Over!' : gameWon ? '🎉 You Win!' : 'Playing...'} + +
+ +
+ {board.map((row, rowIndex) => + row.map((cell, colIndex) => renderCell(cell, rowIndex, colIndex)) + )} +
+
+
+
+ ); +}; + +export default MinesweeperGame; \ No newline at end of file diff --git a/src/pages/minesweeper/styles.module.css b/src/pages/minesweeper/styles.module.css new file mode 100644 index 0000000..9a15f90 --- /dev/null +++ b/src/pages/minesweeper/styles.module.css @@ -0,0 +1,218 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + padding: 2rem; + background: linear-gradient(45deg, #2c3e50, #3498db); +} + +.gameContainer { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + padding: 2rem; + margin-top: 2rem; +} + +.header { + text-align: center; + margin-bottom: 2rem; +} + +.title { + font-size: 2.5rem; + color: #fff; + margin-bottom: 1rem; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); +} + +.controls { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + justify-content: center; +} + +.button { + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + background: #fff; + color: #2c3e50; +} + +.button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.button.active { + background: #3498db; + color: #fff; +} + +.button:last-child { + background: linear-gradient(45deg, #ff6b6b, #ff8e53); + color: white; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; + padding: 0.6rem 1.2rem; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.button:last-child:hover { + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4); + background: linear-gradient(45deg, #ff8e53, #ff6b6b); +} + +.button:last-child:active { + transform: translateY(-1px); + box-shadow: 0 2px 10px rgba(255, 107, 107, 0.3); +} + +.gameStatus { + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(52, 73, 94, 0.8); + backdrop-filter: blur(10px); + padding: 0.5rem 1rem; + border-radius: 12px; + color: #fff; + margin-bottom: 1rem; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); + position: relative; +} + +.gameStatus > * { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.2rem 0.6rem; + font-size: 0.9rem; +} + +.gameStatus > * { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.grid { + display: grid; + gap: 0.5px; + background: #34495e; + padding: 0.5px; + border-radius: 4px; + width: fit-content; + margin: 0 auto; + overflow-x: auto; + max-width: 100%; + -webkit-overflow-scrolling: touch; +} + +.cell { + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + background: #ffffff; + border: none; + font-weight: bold; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.cell:hover { + background: #e2e8f0; +} + +.cell.revealed { + background: #e2e2e2; +} + +.cell.mine { + background: #ff4444; + animation: explode 0.5s ease-out; +} + +@keyframes explode { + 0% { + transform: scale(0.8); + opacity: 0; + background: #ffeb3b; + } + 50% { + transform: scale(1.2); + opacity: 1; + background: #ff9800; + } + 100% { + transform: scale(1); + background: #ff4444; + } +} + +.cell.flag { + background: #ffd700; +} + +.number-1 { color: #3498db; } +.number-2 { color: #2ecc71; } +.number-3 { color: #e74c3c; } +.number-4 { color: #8e44ad; } +.number-5 { color: #c0392b; } +.number-6 { color: #16a085; } +.number-7 { color: #2c3e50; } +.number-8 { color: #7f8c8d; } + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .gameContainer { + padding: 1rem; + } + + .cell { + width: 25px; + height: 25px; + font-size: 0.9rem; + } + + .title { + font-size: 2rem; + } + + .mobileWarning { + background: rgba(255, 193, 7, 0.2); + color: #856404; + padding: 0.75rem; + border-radius: 6px; + margin-bottom: 1rem; + text-align: center; + font-size: 0.9rem; + border: 1px solid rgba(255, 193, 7, 0.3); + } + + .grid { + padding: 0.5px 0.5px 0.5px; + margin: 0 -1rem; + border-radius: 0; + } +} \ No newline at end of file