"use client" import React, { useEffect } from "react"; import anime from 'animejs/lib/anime.es.js'; type Pos = [number, number]; const cellSize = 32; // Size of each cell in the SVG const offset = 0.2 * cellSize; // Offset for the outline let radius = 0.5 * cellSize - offset; let radius_inner = offset; const findClusters = (shape: Pos, walls: Pos[]) => { const [width, height] = shape; const clusters: Pos[][] = []; const visited = new Set(); const directions = [ [0, 1], [1, 0], [0, -1], [-1, 0] ]; const inBounds = (x: number, y: number) => x >= 0 && x < width && y >= 0 && y < height; const dfs = (x: number, y: number, cluster: Pos[]) => { if (!inBounds(x, y) || visited.has(`${x},${y}`) || !walls.some(([bx, by]) => bx === x && by === y)) { return; } visited.add(`${x},${y}`); cluster.push([x, y]); for (const [dx, dy] of directions) { dfs(x + dx, y + dy, cluster); } }; for (const [x, y] of walls) { if (!visited.has(`${x},${y}`)) { const cluster: Pos[] = []; dfs(x, y, cluster); clusters.push(cluster); } } return clusters; }; // Generate the path for a single cluster using a perimeter tracing algorithm with inner and outer boundaries const createPath = (cluster: Pos[]) => { const visitedCorners = new Set(); const visitedCells = new Set(); /* Corners: Reference points +-4-----+-------+ | 0 7 | | | | | | +-2-----+-----1-+ | | | | | | 8 3 | +-------+----11-+ */ type PathMove = { next: [Pos, number], path: string }; const move = (x: number, y: number, corner: number): PathMove | undefined => { let refPoint0 = [0.5 * cellSize, offset]; let refPoint1 = [cellSize - offset, 0.5 * cellSize]; let refPoint2 = [offset, 0.5 * cellSize]; let refPoint3 = [0.5 * cellSize, cellSize - offset]; let refPoint4 = [offset, 0]; let refPoint7 = [cellSize, offset]; let refPoint8 = [0, cellSize - offset]; let refPoint11 = [cellSize - offset, cellSize]; switch (corner) { case 0: // exit if wall to the top if (cluster.some(([bx, by]) => bx === x && by === y - 1)) return; // check if there is a wall to the left if (cluster.some(([bx, by]) => bx === x - 1 && by === y)) { return { next: [[x - 1, y], 7], path: `l ${- cellSize / 2} 0` }; } else { const px = x * cellSize + refPoint2[0]; const py = y * cellSize + refPoint2[1]; return { next: [[x, y], 2], path: `A ${radius} ${radius} 0 0 0 ${px} ${py}` }; } case 1: // exit if wall to the right if (cluster.some(([bx, by]) => bx === x + 1 && by === y)) return; // check if there is a wall to the top if (cluster.some(([bx, by]) => bx === x && by === y - 1)) { return { next: [[x, y - 1], 11], path: `l 0 ${- cellSize / 2}` }; } else { const px = x * cellSize + refPoint0[0]; const py = y * cellSize + refPoint0[1]; return { next: [[x, y], 0], path: `A ${radius} ${radius} 0 0 0 ${px} ${py}` }; } case 2: // exit if wall to the left if (cluster.some(([bx, by]) => bx === x - 1 && by === y)) return; // check if there is a wall to the bottom if (cluster.some(([bx, by]) => bx === x && by === y + 1)) { return { next: [[x, y + 1], 4], path: `l 0,${cellSize / 2}` }; } else { const px = x * cellSize + refPoint3[0]; const py = y * cellSize + refPoint3[1]; return { next: [[x, y], 3], path: `A ${radius},${radius} 0 0 0 ${px},${py}` }; } case 3: // exit if wall to the bottom if (cluster.some(([bx, by]) => bx === x && by === y + 1)) return; // check if there is a wall to the right if (cluster.some(([bx, by]) => bx === x + 1 && by === y)) { return { next: [[x + 1, y], 8], path: `l ${cellSize / 2},0` }; } else { const px = x * cellSize + refPoint1[0]; const py = y * cellSize + refPoint1[1]; return { next: [[x, y], 1], path: `A ${radius},${radius} 0 0 0 ${px},${py}` }; } case 4: // check if there is a wall to the left if (cluster.some(([bx, by]) => bx === x - 1 && by === y)) { const px = (x - 1) * cellSize + refPoint7[0]; const py = y * cellSize + refPoint7[1]; return { next: [[x - 1, y], 7], path: `A ${radius_inner},${radius_inner} 0 0 1 ${px},${py}` }; } else { return { next: [[x, y], 2], path: `l 0 ${cellSize / 2}` }; } case 7: // check if there is a wall to the top if (cluster.some(([bx, by]) => bx === x && by === y - 1)) { const px = x * cellSize + refPoint11[0]; const py = (y - 1) * cellSize + refPoint11[1]; return { next: [[x, y - 1], 11], path: `A ${radius_inner},${radius_inner} 0 0 1 ${px},${py}` }; } else { return { next: [[x, y], 0], path: `l ${- cellSize / 2},0` }; } case 8: // check if there is a wall to the bottom if (cluster.some(([bx, by]) => bx === x && by === y + 1)) { const px = x * cellSize + refPoint4[0]; const py = (y + 1) * cellSize + refPoint4[1]; return { next: [[x, y + 1], 4], path: `A ${radius_inner},${radius_inner} 0 0 1 ${px},${py}` }; } else { return { next: [[x, y], 3], path: `l ${cellSize / 2},0` }; } case 11: // check if there is a wall to the right if (cluster.some(([bx, by]) => bx === x + 1 && by === y)) { const px = (x + 1) * cellSize + refPoint8[0]; const py = y * cellSize + refPoint8[1]; return { next: [[x + 1, y], 8], path: `A ${radius_inner},${radius_inner} 0 0 1 ${px},${py}` }; } else { return { next: [[x, y], 1], path: `l 0,${- cellSize / 2}` }; } } }; const pathCommands = cluster.flatMap((startCell) => { // iterate through all possible starting points in all cells (unless already visited) const paths = [0, 1, 2, 3].map(startRefPoint => { let [x, y] = startCell; if (visitedCorners.has(`${x},${y},${startRefPoint}`)) return ""; let refPoint = startRefPoint; let refPointLoc = [[0.5 * cellSize, offset], [cellSize - offset, 0.5 * cellSize], [offset, 0.5 * cellSize], [0.5 * cellSize, cellSize - offset] ]; const px = x * cellSize + refPointLoc[refPoint][0]; const py = y * cellSize + refPointLoc[refPoint][1]; let pathCommands = []; pathCommands.push([`M ${px},${py}`]); do { const node = `${x},${y},${refPoint}`; if (visitedCorners.has(node)) { break; } visitedCorners.add(node); visitedCells.add(`${x},${y}`); let pathMove = move(x, y, refPoint); if (!pathMove) return ""; let path = ""; ({ next: [[x, y], refPoint], path: path } = pathMove); pathCommands.push([path]); } while (!(x === startCell[0] && y === startCell[1] && refPoint === startRefPoint)); pathCommands.push(["Z"]); // Close the path const joined = pathCommands.join(" "); return joined; }); return paths; }); console.log(pathCommands); return pathCommands.join(" "); }; function Bot({ position, color, width }: { position: Pos, color: string, width: number }) { const leftSide = position[0] < width / 2; const inHomezone = () => { switch (color) { case "blue": return leftSide; case "red": return !leftSide; } } useEffect(() => { anime.timeline() .add({ targets: '#mazebox .blue', opacity: [0, 1], easing: 'linear', duration: 2000, }, 3500) .add({ targets: '#mazebox .red', opacity: [0, 1], easing: 'linear', duration: 2000, }, 3500);; }, []); return ( ) } function Maze({ game_uuid, shape, walls, food, a, b, x, y, whowins, gameover }: { game_uuid: string, shape: Pos, walls: Pos[], food: Pos[], a: Pos, b: Pos, x: Pos, y: Pos, whowins: number | null, gameover: boolean } ) { const [width, height] = shape; const clusters = findClusters(shape, walls); useEffect(() => { if (game_uuid) { let pathAnimation = anime.timeline() .add({ targets: '#mazebox #maze path', strokeDashoffset: [anime.setDashoffset, 0], easing: 'easeInCubic', duration: 2000, delay: function (el, i) { return i * 25 }, direction: 'alternate', loop: false }) .add({ targets: '#mazebox #maze path', //fill: ['rgb(214, 219, 220)', "#faa"], // ffa fillOpacity: [0, 0.7], // ffa easing: 'linear', duration: 2000 }, 2000) .add({ targets: '#mazebox .foodblue', opacity: [0, 1], easing: 'linear', duration: 2000, }, 3000) .add({ targets: '#mazebox .foodred', opacity: [0, 1], easing: 'linear', duration: 2000, }, 3000) .add({ targets: '#mazebox .blue', opacity: [0, 1], easing: 'linear', duration: 2000, }, 3500) .add({ targets: '#mazebox .red', opacity: [0, 1], easing: 'linear', duration: 2000, }, 3500); } }, [walls, game_uuid]); useEffect(() => { console.log(gameover, whowins); if (gameover && whowins === 0) { let pathAnimation = anime.timeline() .add({ targets: '#mazebox #maze path', fill: ["#faa", "rgb(94, 158, 217)"], easing: 'linear', duration: 200 }); }; if (gameover && whowins === 1) { let pathAnimation = anime.timeline() .add({ targets: '#mazebox #maze path', fill: ["#faa", "rgb(235, 90, 90)"], easing: 'linear', duration: 200 }); }; if (gameover && whowins === 2) { let pathAnimation = anime.timeline() .add({ targets: '#mazebox #maze path', fill: ["#faa", "#fff"], easing: 'linear', duration: 200 }); }; }, [gameover, whowins]); return (
{walls.map(([x, y], index) => ( ))} {clusters.map((cluster, index) => ( ))} {food.map(([x, y], index) => ( ))} { gameover ? ( GAME OVER ) : null }
); }; export default Maze;