{'>'} Max score: 4,294,967,296
diff --git a/src/pages/SnakeMultiplayer.tsx b/src/pages/SnakeMultiplayer.tsx
new file mode 100644
index 0000000..86c3b97
--- /dev/null
+++ b/src/pages/SnakeMultiplayer.tsx
@@ -0,0 +1,364 @@
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { motion } from 'framer-motion';
+import { useSettings } from '@/contexts/SettingsContext';
+import { Link } from 'react-router-dom';
+import { Maximize2, Minimize2 } from 'lucide-react';
+import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
+
+const GRID_SIZE = 20;
+const TICK_SPEED = 120;
+
+type Direction = 'up' | 'down' | 'left' | 'right';
+type Position = { x: number; y: number };
+
+interface Player {
+ snake: Position[];
+ direction: Direction;
+ score: number;
+ alive: boolean;
+ color: string;
+ name: string;
+}
+
+const SnakeMultiplayer = () => {
+ const { playSound } = useSettings();
+ const [players, setPlayers] = useState<[Player, Player]>([
+ { snake: [{ x: 5, y: 10 }], direction: 'right', score: 0, alive: true, color: 'hsl(var(--primary))', name: 'P1' },
+ { snake: [{ x: 15, y: 10 }], direction: 'left', score: 0, alive: true, color: 'hsl(280 70% 50%)', name: 'P2' },
+ ]);
+ const [food, setFood] = useState({ x: 10, y: 10 });
+ const [gameOver, setGameOver] = useState(false);
+ const [winner, setWinner] = useState(null);
+ const [gameStarted, setGameStarted] = useState(false);
+ const [isPaused, setIsPaused] = useState(false);
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const gameRef = useRef(null);
+
+ const directionQueues = useRef<[Direction[], Direction[]]>([[], []]);
+ const currentDirections = useRef<[Direction, Direction]>(['right', 'left']);
+
+ const { enterFullscreen, exitFullscreen } = useBrowserFullscreen();
+
+ const getCellSize = useCallback(() => {
+ if (typeof window === 'undefined') return 20;
+ const isMobile = window.innerWidth < 768;
+ if (isMobile) {
+ const maxWidth = window.innerWidth - 40;
+ const maxHeight = window.innerHeight - 300;
+ return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 16);
+ }
+ return isFullscreen ? 24 : 20;
+ }, [isFullscreen]);
+
+ const [cellSize, setCellSize] = useState(getCellSize);
+ const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
+
+ useEffect(() => {
+ const handleResize = () => setCellSize(getCellSize());
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, [getCellSize]);
+
+ useEffect(() => {
+ setCellSize(getCellSize());
+ }, [isFullscreen, getCellSize]);
+
+ const toggleFullscreen = async () => {
+ if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); }
+ else { setIsFullscreen(false); await exitFullscreen(); }
+ playSound('click');
+ };
+
+ useEffect(() => {
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === 'Escape' && isFullscreen) { setIsFullscreen(false); exitFullscreen(); }
+ };
+ window.addEventListener('keydown', handleEscape);
+ return () => window.removeEventListener('keydown', handleEscape);
+ }, [isFullscreen, exitFullscreen]);
+
+ const spawnFood = useCallback((snakes: Position[][]): Position => {
+ const occupied = new Set(snakes.flat().map(s => `${s.x},${s.y}`));
+ const available: Position[] = [];
+ for (let x = 0; x < GRID_SIZE; x++) {
+ for (let y = 0; y < GRID_SIZE; y++) {
+ if (!occupied.has(`${x},${y}`)) available.push({ x, y });
+ }
+ }
+ if (available.length === 0) return { x: 10, y: 10 };
+ return available[Math.floor(Math.random() * available.length)];
+ }, []);
+
+ const startGame = () => {
+ const p1Start = [{ x: 5, y: 10 }];
+ const p2Start = [{ x: 15, y: 10 }];
+ setPlayers([
+ { snake: p1Start, direction: 'right', score: 0, alive: true, color: 'hsl(var(--primary))', name: 'P1' },
+ { snake: p2Start, direction: 'left', score: 0, alive: true, color: 'hsl(280 70% 50%)', name: 'P2' },
+ ]);
+ currentDirections.current = ['right', 'left'];
+ directionQueues.current = [[], []];
+ setFood(spawnFood([p1Start, p2Start]));
+ setGameOver(false);
+ setWinner(null);
+ setIsPaused(false);
+ setGameStarted(true);
+ playSound('success');
+ gameRef.current?.focus();
+ };
+
+ useEffect(() => {
+ if (!gameStarted || gameOver || isPaused) return;
+
+ const interval = setInterval(() => {
+ const opposite: Record = { up: 'down', down: 'up', left: 'right', right: 'left' };
+
+ setPlayers(prev => {
+ const newPlayers: [Player, Player] = [{ ...prev[0] }, { ...prev[1] }];
+
+ for (let i = 0; i < 2; i++) {
+ if (!newPlayers[i].alive) continue;
+
+ // Process direction queue
+ let nextDir = currentDirections.current[i];
+ while (directionQueues.current[i].length > 0) {
+ const queuedDir = directionQueues.current[i].shift()!;
+ if (queuedDir !== opposite[currentDirections.current[i]]) {
+ nextDir = queuedDir;
+ break;
+ }
+ }
+ currentDirections.current[i] = nextDir;
+ newPlayers[i].direction = nextDir;
+
+ // Move snake
+ const head = newPlayers[i].snake[0];
+ let newHead: Position;
+ switch (nextDir) {
+ case 'up': newHead = { x: head.x, y: head.y - 1 }; break;
+ case 'down': newHead = { x: head.x, y: head.y + 1 }; break;
+ case 'left': newHead = { x: head.x - 1, y: head.y }; break;
+ case 'right': newHead = { x: head.x + 1, y: head.y }; break;
+ }
+
+ // Check wall collision
+ if (newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE) {
+ newPlayers[i].alive = false;
+ playSound('error');
+ continue;
+ }
+
+ // Check self collision
+ if (newPlayers[i].snake.some(s => s.x === newHead.x && s.y === newHead.y)) {
+ newPlayers[i].alive = false;
+ playSound('error');
+ continue;
+ }
+
+ // Check collision with other snake
+ const otherIndex = i === 0 ? 1 : 0;
+ if (newPlayers[otherIndex].snake.some(s => s.x === newHead.x && s.y === newHead.y)) {
+ newPlayers[i].alive = false;
+ playSound('error');
+ continue;
+ }
+
+ const newSnake = [newHead, ...newPlayers[i].snake];
+
+ // Check food
+ if (newHead.x === food.x && newHead.y === food.y) {
+ playSound('success');
+ newPlayers[i].score += 10;
+ setFood(spawnFood([newPlayers[0].snake, newPlayers[1].snake]));
+ } else {
+ newSnake.pop();
+ }
+
+ newPlayers[i].snake = newSnake;
+ }
+
+ // Check win condition
+ const alive = newPlayers.filter(p => p.alive);
+ if (alive.length === 0) {
+ setGameOver(true);
+ setWinner('Draw!');
+ } else if (alive.length === 1) {
+ setGameOver(true);
+ setWinner(`${alive[0].name} Wins!`);
+ }
+
+ return newPlayers;
+ });
+ }, TICK_SPEED);
+
+ return () => clearInterval(interval);
+ }, [gameStarted, gameOver, isPaused, food, playSound, spawnFood]);
+
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (!gameStarted) return;
+
+ // Player 1: WASD
+ if (e.key === 'w' || e.key === 'W') { e.preventDefault(); if (directionQueues.current[0].length < 3) directionQueues.current[0].push('up'); }
+ if (e.key === 's' || e.key === 'S') { e.preventDefault(); if (directionQueues.current[0].length < 3) directionQueues.current[0].push('down'); }
+ if (e.key === 'a' || e.key === 'A') { e.preventDefault(); if (directionQueues.current[0].length < 3) directionQueues.current[0].push('left'); }
+ if (e.key === 'd' || e.key === 'D') { e.preventDefault(); if (directionQueues.current[0].length < 3) directionQueues.current[0].push('right'); }
+
+ // Player 2: Arrow keys
+ if (e.key === 'ArrowUp') { e.preventDefault(); if (directionQueues.current[1].length < 3) directionQueues.current[1].push('up'); }
+ if (e.key === 'ArrowDown') { e.preventDefault(); if (directionQueues.current[1].length < 3) directionQueues.current[1].push('down'); }
+ if (e.key === 'ArrowLeft') { e.preventDefault(); if (directionQueues.current[1].length < 3) directionQueues.current[1].push('left'); }
+ if (e.key === 'ArrowRight') { e.preventDefault(); if (directionQueues.current[1].length < 3) directionQueues.current[1].push('right'); }
+
+ if (e.key === 'p' || e.key === 'P') { e.preventDefault(); if (!gameOver) setIsPaused(p => !p); }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [gameStarted, gameOver]);
+
+ const renderGrid = () => {
+ const cells = [];
+ const p1Set = new Set(players[0].snake.map(s => `${s.x},${s.y}`));
+ const p2Set = new Set(players[1].snake.map(s => `${s.x},${s.y}`));
+ const p1Head = players[0].snake[0];
+ const p2Head = players[1].snake[0];
+
+ for (let y = 0; y < GRID_SIZE; y++) {
+ for (let x = 0; x < GRID_SIZE; x++) {
+ const isP1 = p1Set.has(`${x},${y}`);
+ const isP2 = p2Set.has(`${x},${y}`);
+ const isP1Head = p1Head.x === x && p1Head.y === y && players[0].alive;
+ const isP2Head = p2Head.x === x && p2Head.y === y && players[1].alive;
+ const isFood = food.x === x && food.y === y;
+
+ let bgClass = 'bg-background/50 border-primary/10';
+ let style: React.CSSProperties = { width: cellSize, height: cellSize };
+
+ if (isP1Head) {
+ bgClass = 'border-primary';
+ style.backgroundColor = players[0].color;
+ style.boxShadow = `0 0 10px ${players[0].color}`;
+ } else if (isP1 && players[0].alive) {
+ bgClass = 'border-primary/60';
+ style.backgroundColor = players[0].color;
+ style.opacity = 0.8;
+ } else if (isP2Head) {
+ bgClass = 'border-purple-500';
+ style.backgroundColor = players[1].color;
+ style.boxShadow = `0 0 10px ${players[1].color}`;
+ } else if (isP2 && players[1].alive) {
+ bgClass = 'border-purple-500/60';
+ style.backgroundColor = players[1].color;
+ style.opacity = 0.8;
+ } else if (isFood) {
+ bgClass = 'bg-destructive/80 border-destructive/60';
+ }
+
+ cells.push(
+
+ );
+ }
+ }
+ return cells;
+ };
+
+ return (
+
+
+
+ {'<'} Back
+
Snake 2P
+
+
+
+
+
+
+
+
+ {/* Player 1 Stats */}
+
+
+
{players[0].score}
+ {!players[0].alive &&
DEAD
}
+
+
+ {/* Player 2 Stats */}
+
+
+
{players[1].score}
+ {!players[1].alive &&
DEAD
}
+
+
+
+
CONTROLS
+
P1: W A S D
+
P2: ↑ ↓ ← →
+
P: Pause
+
+
+ {!gameStarted || gameOver ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Game Over / Pause Overlay */}
+ {(gameOver || isPaused) && gameStarted && (
+
+
+
+ {gameOver ? winner : 'PAUSED'}
+
+ {gameOver && (
+
+
+
P1 Score
+
{players[0].score}
+
+
+
P2 Score
+
{players[1].score}
+
+
+ )}
+
+
+
+ )}
+
+ );
+};
+
+export default SnakeMultiplayer;
\ No newline at end of file