Compare commits

1 Commits

23 changed files with 1670 additions and 30 deletions

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Dependencies
node_modules/
/.pnp
.pnp.js
# Testing
/coverage
# Production
/build
/dist
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Vite
.vite/

View File

@ -4,37 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jeu Oulvic</title> <title>Jeu Oulvic</title>
<link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<div class="main-wrapper"> <div id="root"></div>
<div class="container"> <script type="module" src="/src/index.js"></script>
<h1>OULVIC</h1>
<div class="user-input-section">
<label for="username">Nom d'utilisateur:</label>
<input type="text" id="username" placeholder="Entrez votre nom" maxlength="15">
</div>
<div class="game-info">
<div class="score">Score: <span id="score">0</span></div>
<div class="level">Niveau: <span id="level">1</span></div>
<div class="lives">Vies: <span id="lives"><span class="heart"></span><span class="heart"></span><span class="heart"></span></span></div>
<div class="status" id="status">Prêt à jouer</div>
</div>
<canvas id="gameCanvas" width="600" height="600"></canvas>
<div class="instructions">
<p>Utilisez les flèches directionnelles pour déplacer Oulvic</p>
<button id="restartBtn" style="display: none;">Rejouer</button>
</div>
</div>
<div class="leaderboard-container">
<h2>Classement</h2>
<div id="leaderboard"></div>
</div>
</div>
<footer>
<p>By Ludo and Syoul</p>
</footer>
<script src="game.js"></script>
</body> </body>
</html> </html>

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "oulvic-game",
"version": "1.0.0",
"description": "Jeu Oulvic - Refactoring React",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}

16
src/App.js Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
import Game from './components/Game/Game';
import Footer from './components/Footer/Footer';
import './styles/App.css';
function App() {
return (
<div className="App">
<Game />
<Footer />
</div>
);
}
export default App;

View File

@ -0,0 +1,12 @@
import React from 'react';
function Footer() {
return (
<footer>
<p>By Ludo and Syoul</p>
</footer>
);
}
export default Footer;

226
src/components/Game/Game.js Normal file
View File

@ -0,0 +1,226 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Pacman, Ghost, Bonus } from '../../utils/entities';
import { mazeVariants } from '../../utils/mazeData';
import { fillEmptySpaces, randomizeMaze, getBonusPositions, countDots } from '../../utils/gameLogic';
import GameCanvas from '../GameCanvas/GameCanvas';
import GameInfo from '../GameInfo/GameInfo';
import UserInput from '../UserInput/UserInput';
import Leaderboard from '../Leaderboard/Leaderboard';
import { useGameState } from '../../hooks/useGameState';
import { useKeyboard } from '../../hooks/useKeyboard';
import { useLeaderboard } from '../../hooks/useLeaderboard';
import { saveScore } from '../../utils/leaderboard';
function Game() {
const gameState = useGameState();
const { scores, addScore: addLeaderboardScore } = useLeaderboard();
const [username, setUsername] = useState('');
const [maze, setMaze] = useState([]);
const [pacman, setPacman] = useState(null);
const [ghosts, setGhosts] = useState([]);
const [bonuses, setBonuses] = useState([]);
const initializeGame = useCallback(() => {
const initialMaze = mazeVariants[0].map(row => [...row]);
fillEmptySpaces(initialMaze);
randomizeMaze(initialMaze);
setMaze(initialMaze);
const newPacman = new Pacman();
newPacman.speed = newPacman.baseSpeed * (1 + (gameState.level - 1) * 0.05);
setPacman(newPacman);
const newGhosts = [
new Ghost(14, 11, '#ff0000'),
new Ghost(15, 11, '#ff00ff'),
new Ghost(14, 12, '#00ffff'),
new Ghost(15, 12, '#ffa500')
];
newGhosts.forEach(ghost => ghost.updateSpeed(gameState.level));
setGhosts(newGhosts);
const bonusPositions = getBonusPositions();
const newBonuses = [];
const mazeCopy = initialMaze.map(row => [...row]);
for (let pos of bonusPositions) {
if (mazeCopy[pos.y][pos.x] === 0 || mazeCopy[pos.y][pos.x] === 2) {
mazeCopy[pos.y][pos.x] = pos.type;
newBonuses.push(new Bonus(pos.x, pos.y, pos.type));
}
}
setBonuses(newBonuses);
setMaze(mazeCopy);
}, [gameState.level]);
useEffect(() => {
initializeGame();
gameState.resetGame();
}, []);
const handleCollect = useCallback((type, points) => {
gameState.addScore(points);
if (type === 'cherry') {
const newCherriesEaten = gameState.cherriesEaten + 1;
gameState.setCherriesEaten(newCherriesEaten);
gameState.setCherryEatenRecently(true);
gameState.setCherryEatenTimer(Math.max(150, 300 - (gameState.level - 1) * 20));
}
}, [gameState]);
const handleCollision = useCallback(() => {
const newLives = gameState.lives - 1;
gameState.setLives(newLives);
if (newLives <= 0) {
gameState.setGameRunning(false);
gameState.setStatus('Game Over !');
if (username.trim() !== '') {
saveScore(username, gameState.score);
addLeaderboardScore(username, gameState.score);
}
} else {
gameState.restartLevel();
setTimeout(() => {
initializeGame();
gameState.setIsChangingLevel(false);
gameState.setGameRunning(true);
gameState.setStatus('En jeu');
}, 2000);
}
}, [gameState, username, initializeGame, addLeaderboardScore]);
const handleNextLevel = useCallback(() => {
const newLevel = gameState.level + 1;
gameState.setLevel(newLevel);
gameState.setCherriesEaten(0);
gameState.setIsChangingLevel(true);
gameState.setCherryEatenRecently(false);
gameState.setCherryEatenTimer(0);
setTimeout(() => {
const mazeIndex = (newLevel - 1) % mazeVariants.length;
const newMaze = mazeVariants[mazeIndex].map(row => [...row]);
fillEmptySpaces(newMaze);
randomizeMaze(newMaze);
setMaze(newMaze);
const newPacman = new Pacman();
newPacman.speed = newPacman.baseSpeed * (1 + (newLevel - 1) * 0.05);
setPacman(newPacman);
const newGhosts = [
new Ghost(14, 11, '#ff0000'),
new Ghost(15, 11, '#ff00ff'),
new Ghost(14, 12, '#00ffff'),
new Ghost(15, 12, '#ffa500')
];
newGhosts.forEach(ghost => ghost.updateSpeed(newLevel));
setGhosts(newGhosts);
const bonusPositions = getBonusPositions();
const newBonuses = [];
const mazeCopy = newMaze.map(row => [...row]);
for (let pos of bonusPositions) {
if (mazeCopy[pos.y][pos.x] === 0 || mazeCopy[pos.y][pos.x] === 2) {
mazeCopy[pos.y][pos.x] = pos.type;
newBonuses.push(new Bonus(pos.x, pos.y, pos.type));
}
}
setBonuses(newBonuses);
setMaze(mazeCopy);
gameState.setIsChangingLevel(false);
gameState.setGameRunning(true);
gameState.setStatus('En jeu');
}, 2000);
}, [gameState]);
const handleKeyPress = useCallback((key) => {
if (!gameState.gameRunning || !pacman) return;
switch(key) {
case 'ArrowUp':
pacman.nextDirection = 0;
break;
case 'ArrowRight':
pacman.nextDirection = 1;
break;
case 'ArrowDown':
pacman.nextDirection = 2;
break;
case 'ArrowLeft':
pacman.nextDirection = 3;
break;
default:
break;
}
}, [gameState.gameRunning, pacman]);
useKeyboard(handleKeyPress);
const handleRestart = useCallback(() => {
initializeGame();
gameState.resetGame();
}, [initializeGame, gameState]);
useEffect(() => {
if (gameState.cherryEatenTimer > 0) {
const timer = setInterval(() => {
const newTimer = gameState.cherryEatenTimer - 1;
if (newTimer <= 0) {
gameState.setCherryEatenRecently(false);
gameState.setCherryEatenTimer(0);
} else {
gameState.setCherryEatenTimer(newTimer);
}
}, 16);
return () => clearInterval(timer);
}
}, [gameState.cherryEatenTimer, gameState]);
useEffect(() => {
if (gameState.cherriesEaten >= 4 && !gameState.isChangingLevel && gameState.gameRunning) {
handleNextLevel();
}
}, [gameState.cherriesEaten, gameState.isChangingLevel, gameState.gameRunning, handleNextLevel]);
return (
<div className="main-wrapper">
<div className="container">
<h1>OULVIC</h1>
<UserInput username={username} onUsernameChange={setUsername} />
<GameInfo
score={gameState.score}
level={gameState.level}
lives={gameState.lives}
status={gameState.status}
/>
<GameCanvas
maze={maze}
pacman={pacman}
ghosts={ghosts}
bonuses={bonuses}
gameRunning={gameState.gameRunning}
isChangingLevel={gameState.isChangingLevel}
level={gameState.level}
cherryEatenRecently={gameState.cherryEatenRecently}
onCollect={handleCollect}
onCollision={handleCollision}
/>
<div className="instructions">
<p>Utilisez les flèches directionnelles pour déplacer Oulvic</p>
{!gameState.gameRunning && (
<button id="restartBtn" onClick={handleRestart}>
Rejouer
</button>
)}
</div>
</div>
<Leaderboard />
</div>
);
}
export default Game;

View File

@ -0,0 +1,81 @@
import React, { useRef, useEffect } from 'react';
import { drawMaze } from '../../utils/maze';
import { COLS, ROWS, CELL_SIZE } from '../../utils/constants';
function GameCanvas({
maze,
pacman,
ghosts,
bonuses,
gameRunning,
isChangingLevel,
level,
cherryEatenRecently,
onCollect,
onCollision
}) {
const canvasRef = useRef(null);
const animationFrameRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
canvas.width = COLS * CELL_SIZE;
canvas.height = ROWS * CELL_SIZE;
const gameLoop = () => {
if (isChangingLevel || !gameRunning) {
return;
}
drawMaze(ctx, maze, bonuses);
if (pacman) {
pacman.update(maze, gameRunning);
pacman.collectDot(maze, (type, points) => {
onCollect(type, points);
});
pacman.draw(ctx);
}
if (ghosts && ghosts.length > 0) {
ghosts.forEach(ghost => {
ghost.update(maze, gameRunning, level, pacman, cherryEatenRecently);
ghost.draw(ctx);
const distance = Math.sqrt(
Math.pow(pacman.pixelX - ghost.pixelX, 2) +
Math.pow(pacman.pixelY - ghost.pixelY, 2)
);
if (distance < CELL_SIZE * 0.6) {
onCollision();
}
});
}
if (gameRunning && !isChangingLevel) {
animationFrameRef.current = requestAnimationFrame(gameLoop);
}
};
if (gameRunning && !isChangingLevel) {
animationFrameRef.current = requestAnimationFrame(gameLoop);
}
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [maze, bonuses, pacman, ghosts, gameRunning, isChangingLevel, level, cherryEatenRecently, onCollect, onCollision]);
return <canvas ref={canvasRef} id="gameCanvas" />;
}
export default GameCanvas;

View File

@ -0,0 +1,18 @@
import React from 'react';
import Lives from '../Lives/Lives';
function GameInfo({ score, level, lives, status }) {
return (
<div className="game-info">
<div className="score">Score: <span>{score}</span></div>
<div className="level">Niveau: <span>{level}</span></div>
<Lives lives={lives} />
<div className="status" style={{ color: status === 'En jeu' ? '#ffd700' : '#ff6b6b' }}>
{status}
</div>
</div>
);
}
export default GameInfo;

View File

@ -0,0 +1,52 @@
import React, { useEffect, useState } from 'react';
import { getLeaderboard } from '../../utils/leaderboard';
function Leaderboard() {
const [scores, setScores] = useState([]);
useEffect(() => {
updateLeaderboard();
}, []);
const updateLeaderboard = () => {
const leaderboard = getLeaderboard();
setScores(leaderboard);
};
React.useEffect(() => {
const interval = setInterval(() => {
updateLeaderboard();
}, 1000);
return () => clearInterval(interval);
}, []);
if (scores.length === 0) {
return (
<div className="leaderboard-container">
<h2>Classement</h2>
<div className="empty-leaderboard">Aucun score enregistré</div>
</div>
);
}
return (
<div className="leaderboard-container">
<h2>Classement</h2>
<div>
{scores.map((entry, index) => (
<div
key={index}
className={`leaderboard-item ${index < 3 ? 'top' : ''}`}
>
<div className="leaderboard-rank">{index + 1}</div>
<div className="leaderboard-name">{entry.name}</div>
<div className="leaderboard-score">{entry.score}</div>
</div>
))}
</div>
</div>
);
}
export default Leaderboard;

View File

@ -0,0 +1,16 @@
import React from 'react';
function Lives({ lives }) {
return (
<div className="lives">
Vies: <span>
<span className="heart" style={{ opacity: lives >= 1 ? 1 : 0.3 }}></span>
<span className="heart" style={{ opacity: lives >= 2 ? 1 : 0.3 }}></span>
<span className="heart" style={{ opacity: lives >= 3 ? 1 : 0.3 }}></span>
</span>
</div>
);
}
export default Lives;

View File

@ -0,0 +1,20 @@
import React from 'react';
function UserInput({ username, onUsernameChange }) {
return (
<div className="user-input-section">
<label htmlFor="username">Nom d'utilisateur:</label>
<input
type="text"
id="username"
placeholder="Entrez votre nom"
maxLength="15"
value={username}
onChange={(e) => onUsernameChange(e.target.value)}
/>
</div>
);
}
export default UserInput;

89
src/hooks/useGameState.js Normal file
View File

@ -0,0 +1,89 @@
import { useState, useCallback } from 'react';
export function useGameState() {
const [score, setScore] = useState(0);
const [level, setLevel] = useState(1);
const [lives, setLives] = useState(3);
const [gameRunning, setGameRunning] = useState(true);
const [status, setStatus] = useState('En jeu');
const [cherriesEaten, setCherriesEaten] = useState(0);
const [isChangingLevel, setIsChangingLevel] = useState(false);
const [cherryEatenRecently, setCherryEatenRecently] = useState(false);
const [cherryEatenTimer, setCherryEatenTimer] = useState(0);
const addScore = useCallback((points) => {
setScore(prev => prev + points);
}, []);
const resetGame = useCallback(() => {
setScore(0);
setLevel(1);
setLives(3);
setGameRunning(true);
setStatus('En jeu');
setCherriesEaten(0);
setIsChangingLevel(false);
setCherryEatenRecently(false);
setCherryEatenTimer(0);
}, []);
const loseLife = useCallback(() => {
setLives(prev => prev - 1);
}, []);
const eatCherry = useCallback(() => {
setCherriesEaten(prev => prev + 1);
setCherryEatenRecently(true);
setCherryEatenTimer(300);
}, []);
const nextLevel = useCallback(() => {
setLevel(prev => prev + 1);
setCherriesEaten(0);
setIsChangingLevel(true);
setCherryEatenRecently(false);
setCherryEatenTimer(0);
}, []);
const restartLevel = useCallback(() => {
setCherriesEaten(0);
setIsChangingLevel(true);
setCherryEatenRecently(false);
setCherryEatenTimer(0);
}, []);
const updateCherryTimer = useCallback(() => {
if (cherryEatenTimer > 0) {
setCherryEatenTimer(prev => prev - 1);
} else {
setCherryEatenRecently(false);
}
}, [cherryEatenTimer]);
return {
score,
level,
lives,
gameRunning,
status,
cherriesEaten,
isChangingLevel,
cherryEatenRecently,
cherryEatenTimer,
setScore,
setLevel,
setLives,
setGameRunning,
setStatus,
setCherriesEaten,
setIsChangingLevel,
addScore,
resetGame,
loseLife,
eatCherry,
nextLevel,
restartLevel,
updateCherryTimer
};
}

15
src/hooks/useKeyboard.js Normal file
View File

@ -0,0 +1,15 @@
import { useEffect } from 'react';
export function useKeyboard(onKeyPress) {
useEffect(() => {
const handleKeyDown = (e) => {
onKeyPress(e.key);
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [onKeyPress]);
}

View File

@ -0,0 +1,25 @@
import { useState, useEffect, useCallback } from 'react';
import { getLeaderboard, saveScore } from '../utils/leaderboard';
export function useLeaderboard() {
const [scores, setScores] = useState([]);
const updateLeaderboard = useCallback(() => {
const leaderboard = getLeaderboard();
setScores(leaderboard);
}, []);
const addScore = useCallback((username, score) => {
const updatedScores = saveScore(username, score);
setScores(updatedScores);
}, []);
useEffect(() => {
updateLeaderboard();
const interval = setInterval(updateLeaderboard, 1000);
return () => clearInterval(interval);
}, [updateLeaderboard]);
return { scores, addScore, updateLeaderboard };
}

12
src/index.js Normal file
View File

@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles/App.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

307
src/styles/App.css Normal file
View File

@ -0,0 +1,307 @@
* {
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;
}
.App {
position: relative;
z-index: 1;
}
.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;
}
}

11
src/utils/constants.js Normal file
View File

@ -0,0 +1,11 @@
export const CELL_SIZE = 20;
export const COLS = 30;
export const ROWS = 30;
export const WALL = 1;
export const DOT = 2;
export const EMPTY = 0;
export const TUNNEL = 3;
export const BONUS_CHERRY = 4;
export const BONUS_LUDO = 5;

342
src/utils/entities.js Normal file
View File

@ -0,0 +1,342 @@
import { CELL_SIZE, COLS, ROWS, WALL, DOT, EMPTY, BONUS_CHERRY, BONUS_LUDO } from './constants';
export 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;
}
canMove(direction, maze) {
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;
}
update(maze, gameRunning) {
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, maze)) {
this.direction = this.nextDirection;
}
if (this.canMove(this.direction, maze)) {
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);
}
collectDot(maze, onCollect) {
if (maze[this.y][this.x] === DOT) {
maze[this.y][this.x] = EMPTY;
onCollect('dot', 10);
} else if (maze[this.y][this.x] === BONUS_CHERRY) {
maze[this.y][this.x] = EMPTY;
onCollect('cherry', 100);
} else if (maze[this.y][this.x] === BONUS_LUDO) {
maze[this.y][this.x] = EMPTY;
onCollect('ludo', 15);
}
}
draw(ctx) {
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();
}
}
export 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(level) {
this.speed = this.baseSpeed * (1 + (level - 1) * 0.2);
}
canMove(direction, maze) {
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;
}
getDirectionToPacman(possibleDirections, pacman) {
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;
}
update(maze, gameRunning, level, pacman, cherryEatenRecently) {
if (!gameRunning) return;
this.moveInterval = Math.max(15, 30 - (level - 1) * 2);
this.moveCounter++;
if (this.moveCounter > this.moveInterval || !this.canMove(this.direction, maze)) {
const possibleDirections = [];
for (let i = 0; i < 4; i++) {
if (this.canMove(i, maze)) {
possibleDirections.push(i);
}
}
if (possibleDirections.length > 0) {
if (cherryEatenRecently) {
this.direction = this.getDirectionToPacman(possibleDirections, pacman);
} 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);
}
draw(ctx) {
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();
}
}
export 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(ctx) {
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();
}
}

196
src/utils/gameLogic.js Normal file
View File

@ -0,0 +1,196 @@
import { ROWS, COLS, CELL_SIZE, WALL, DOT, EMPTY, BONUS_CHERRY, BONUS_LUDO } from './constants';
export function countDots(maze) {
let totalDots = 0;
if (!maze || maze.length === 0) {
console.error('countDots() - maze est vide ou undefined');
return 0;
}
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++;
}
}
}
return totalDots;
}
export function fillEmptySpaces(maze) {
console.log('Remplissage de tous les espaces vides avec des chemins');
const centerX = Math.floor(COLS / 2);
const centerY = Math.floor(ROWS / 2);
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;
}
}
}
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;
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;
}
}
}
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;
}
}
}
}
}
}
}
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');
}
export function randomizeMaze(maze) {
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;
}
}
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;
}
}
}
}
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');
}
export function getBonusPositions() {
return [
{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}
];
}

34
src/utils/leaderboard.js Normal file
View File

@ -0,0 +1,34 @@
const STORAGE_KEY = 'pacmanScores';
export function saveScore(username, score) {
if (!username || username.trim() === '') {
username = 'Anonyme';
}
const scoresJson = localStorage.getItem(STORAGE_KEY);
let scores = scoresJson ? JSON.parse(scoresJson) : [];
scores.push({
name: username,
score: score,
date: new Date().toISOString()
});
scores.sort((a, b) => b.score - a.score);
const topScores = scores.slice(0, 10);
localStorage.setItem(STORAGE_KEY, JSON.stringify(topScores));
return topScores;
}
export function getLeaderboard() {
const scoresJson = localStorage.getItem(STORAGE_KEY);
if (!scoresJson) {
return [];
}
const scores = JSON.parse(scoresJson);
return scores.sort((a, b) => b.score - a.score);
}

31
src/utils/maze.js Normal file
View File

@ -0,0 +1,31 @@
import { ROWS, COLS, CELL_SIZE, WALL, DOT, EMPTY, TUNNEL } from './constants';
export function drawMaze(ctx, maze, bonuses) {
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, COLS * CELL_SIZE, ROWS * CELL_SIZE);
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(ctx);
}
}

101
src/utils/mazeData.js Normal file
View File

@ -0,0 +1,101 @@
export 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]
];
export 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]
];
export 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]
];
export const mazeVariants = [originalMaze1, originalMaze2, originalMaze3];

11
vite.config.js Normal file
View File

@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
open: true
}
})