"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 | | | | | | +-1-----+-----3-+ | | | | | | 5 2 | +-------+-----6-+ */ const refPoint0 = [0.5 * cellSize, offset]; const refPoint1 = [offset, 0.5 * cellSize]; const refPoint2 = [0.5 * cellSize, cellSize - offset]; const refPoint3 = [cellSize - offset, 0.5 * cellSize]; const refPoint4 = [offset, 0]; const refPoint5 = [0, cellSize - offset]; const refPoint6 = [cellSize - offset, cellSize]; const refPoint7 = [cellSize, offset]; const refPoints = [ refPoint0, refPoint1, refPoint2, refPoint3, refPoint4, refPoint5, refPoint6, refPoint7 ]; type PathMove = { next: [Pos, number], path: string }; const makeNextLine = (x: number, y: number, nextRefPoint: number): PathMove => { const px = x * cellSize + refPoints[nextRefPoint][0]; const py = y * cellSize + refPoints[nextRefPoint][1]; return { next: [[x, y], nextRefPoint], path: `L ${px},${py}` }; } const makeNextCurve = (x: number, y: number, nextRefPoint: number): PathMove => { const px = x * cellSize + refPoints[nextRefPoint][0]; const py = y * cellSize + refPoints[nextRefPoint][1]; return { next: [[x, y], nextRefPoint], path: `A ${radius},${radius} 0 0 0 ${px},${py}` }; } const makeNextCurveInner = (x: number, y: number, nextRefPoint: number): PathMove => { const px = x * cellSize + refPoints[nextRefPoint][0]; const py = y * cellSize + refPoints[nextRefPoint][1]; return { next: [[x, y], nextRefPoint], path: `A ${radius_inner},${radius_inner} 0 0 1 ${px},${py}` }; } const move = (x: number, y: number, corner: number): PathMove | undefined => { 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 makeNextLine(x - 1, y, 7); } else { return makeNextCurve(x, y, 1); } case 1: // 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 makeNextLine(x, y + 1, 4); } else { return makeNextCurve(x, y, 2); } case 2: // 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 makeNextLine(x + 1, y, 5); } else { return makeNextCurve(x, y, 3); } case 3: // 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 makeNextLine(x, y - 1, 6); } else { return makeNextCurve(x, y, 0); } case 4: // check if there is a wall to the left if (cluster.some(([bx, by]) => bx === x - 1 && by === y)) { return makeNextCurveInner(x - 1, y, 7); } else { return makeNextLine(x, y, 1); } case 5: // check if there is a wall to the bottom if (cluster.some(([bx, by]) => bx === x && by === y + 1)) { return makeNextCurveInner(x, y + 1, 4); } else { return makeNextLine(x, y, 2); } case 6: // check if there is a wall to the right if (cluster.some(([bx, by]) => bx === x + 1 && by === y)) { return makeNextCurveInner(x + 1, y, 5); } else { return makeNextLine(x, y, 3); } case 7: // check if there is a wall to the top if (cluster.some(([bx, by]) => bx === x && by === y - 1)) { return makeNextCurveInner(x, y - 1, 6); } else { return makeNextLine(x, y, 0); } } }; if (cluster.length === 1) { // Widen the dot so that it does not look like food const [x, y] = cluster[0]; const px = x * cellSize + 0.5 * cellSize; const py = y * cellSize + offset; return [ `M ${px},${py}`, `l ${-offset} 0`, `a 1,1 0 0 0 0,${cellSize - 2 * offset}`, `l ${2 * offset} 0`, `a 1,1 0 0 0 0,${- cellSize + 2 * offset}`, `Z` ].join(" "); } 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; const px = x * cellSize + refPoints[refPoint][0]; const py = y * cellSize + refPoints[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;