WIP
This commit is contained in:
parent
0ec0f82be3
commit
1047ece799
429
app/maze.tsx
Normal file
429
app/maze.tsx
Normal file
|
@ -0,0 +1,429 @@
|
|||
"use client"
|
||||
|
||||
import React, {
|
||||
useEffect,
|
||||
useState
|
||||
} from "react";
|
||||
|
||||
import anime from 'animejs/lib/anime.es.js';
|
||||
|
||||
const cellSize = 32; // Size of each cell in the SVG
|
||||
const offset = 0.15 * cellSize; // Offset for the outline
|
||||
|
||||
const findClusters = (shape: [number, number], walls: [number, number][]) => {
|
||||
const [width, height] = shape;
|
||||
const clusters: [number, number][][] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
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: [number, number][]) => {
|
||||
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: [number, number][] = [];
|
||||
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: [number, number][]) => {
|
||||
const pathCommands: string[] = [];
|
||||
const visitedCorners = new Set<string>();
|
||||
|
||||
const startPoint = cluster[0];
|
||||
let [x, y] = startPoint;
|
||||
let corner = 0;
|
||||
|
||||
const move = () => {
|
||||
switch (corner) {
|
||||
case 0:
|
||||
// check if there is a wall to the left
|
||||
if (cluster.some(([bx, by]) => bx === x - 1 && by === y)) {
|
||||
corner = 1;
|
||||
x -= 1;
|
||||
} else {
|
||||
corner = 2;
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// check if there is a wall to the top
|
||||
if (cluster.some(([bx, by]) => bx === x && by === y - 1)) {
|
||||
corner = 3;
|
||||
y -= 1;
|
||||
} else {
|
||||
corner = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// check if there is a wall to the bottom
|
||||
if (cluster.some(([bx, by]) => bx === x && by === y + 1)) {
|
||||
corner = 0;
|
||||
y += 1;
|
||||
} else {
|
||||
corner = 3;
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// check if there is a wall to the right
|
||||
if (cluster.some(([bx, by]) => bx === x + 1 && by === y)) {
|
||||
corner = 2;
|
||||
x += 1;
|
||||
} else {
|
||||
corner = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
{
|
||||
|
||||
let startCorner = 3;
|
||||
corner = startCorner;
|
||||
|
||||
const px = x * cellSize + (corner % 2 === 0 ? offset : cellSize - offset);
|
||||
const py = y * cellSize + (corner < 2 ? offset : cellSize - offset);
|
||||
|
||||
pathCommands.push(`M${px},${py}`);
|
||||
|
||||
do {
|
||||
const node = `${x},${y},${corner}`;
|
||||
if (visitedCorners.has(node)) {
|
||||
break;
|
||||
}
|
||||
|
||||
visitedCorners.add(node);
|
||||
|
||||
const px = x * cellSize + (corner % 2 === 0 ? offset : cellSize - offset);
|
||||
const py = y * cellSize + (corner < 2 ? offset : cellSize - offset);
|
||||
|
||||
pathCommands.push(`L${px} ${py}`);
|
||||
move()
|
||||
} while (!(x === startPoint[0] && y === startPoint[1] && corner === startCorner));
|
||||
|
||||
pathCommands.push('Z'); // Close the path
|
||||
};
|
||||
|
||||
const node = `${x},${y},${0}`;
|
||||
if (!visitedCorners.has(node)) {
|
||||
let startCorner = 0;
|
||||
corner = startCorner;
|
||||
|
||||
const px = x * cellSize + (corner % 2 === 0 ? offset : cellSize - offset);
|
||||
const py = y * cellSize + (corner < 2 ? offset : cellSize - offset);
|
||||
|
||||
pathCommands.push(`M${px},${py}`);
|
||||
|
||||
do {
|
||||
const node = `${x},${y},${corner}`;
|
||||
if (visitedCorners.has(node)) {
|
||||
break;
|
||||
}
|
||||
|
||||
visitedCorners.add(node);
|
||||
|
||||
const px = x * cellSize + (corner % 2 === 0 ? offset : cellSize - offset);
|
||||
const py = y * cellSize + (corner < 2 ? offset : cellSize - offset);
|
||||
|
||||
pathCommands.push(`L${px},${py}`);
|
||||
move()
|
||||
} while (!(x === startPoint[0] && y === startPoint[1] && corner === startCorner));
|
||||
|
||||
pathCommands.push('Z'); // Close the path
|
||||
};
|
||||
|
||||
//console.log(pathCommands, cluster);
|
||||
return pathCommands.join(' ');
|
||||
};
|
||||
|
||||
|
||||
function Bot({ position, color, width }: { position: [number, number], 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 (
|
||||
<use transform={`translate(${(position[0]) * cellSize}, ${(position[1]) * cellSize}) scale(3)`} href={inHomezone() ? `#ghost` : `#pacman`} className={color} />
|
||||
)
|
||||
}
|
||||
|
||||
function Maze({ game_uuid, shape, walls, food, a, b, x, y, whowins, gameover }:
|
||||
{
|
||||
game_uuid: string,
|
||||
shape: [number, number],
|
||||
walls: [number, number][],
|
||||
food: [number, number][],
|
||||
a: [number, number],
|
||||
b: [number, number],
|
||||
x: [number, number],
|
||||
y: [number, number],
|
||||
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
|
||||
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]);
|
||||
|
||||
|
||||
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 (
|
||||
<div id="mazebox">
|
||||
<svg
|
||||
width={width * cellSize}
|
||||
height={height * cellSize}
|
||||
viewBox={`0 0 ${width * cellSize} ${height * cellSize}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style type="text/css">{`
|
||||
line {
|
||||
stroke: #000000;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 3;
|
||||
}
|
||||
.foodblue {
|
||||
stroke: black;
|
||||
fill: rgb(94, 158, 217);
|
||||
}
|
||||
.foodred {
|
||||
stroke: black;
|
||||
fill: rgb(235, 90, 90);
|
||||
}
|
||||
.blue {
|
||||
fill: rgb(94, 158, 217);
|
||||
}
|
||||
.red {
|
||||
fill: rgb(235, 90, 90);
|
||||
}
|
||||
|
||||
.gameover {
|
||||
fill: #FFC903;
|
||||
stroke: #ED1B22;
|
||||
stroke-width: 10px;
|
||||
stroke-linejoin: round;
|
||||
paint-order: stroke;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
|
||||
<defs>
|
||||
<g id="pacman">
|
||||
<path d="M 9.98 7.73
|
||||
A 4.38 4.38 0 1 1 9.98 3.8
|
||||
L 6.05 5.8
|
||||
Z"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<g id="ghost">
|
||||
<path d="M 2 6
|
||||
C 2 3.79 3.8 2 6 2
|
||||
C 8.21 2 10 3.8 10 6
|
||||
L 10 10
|
||||
L 9.01 8.81
|
||||
L 8.01 10
|
||||
L 7 8.81
|
||||
L 6.01 10
|
||||
L 5.01 8.81
|
||||
L 4 10
|
||||
L 3 8.81
|
||||
L 2 10
|
||||
L 2 6
|
||||
Z
|
||||
M 4.39 6.54
|
||||
C 4.9 6.54 5.31 6.03 5.31 5.38
|
||||
C 5.31 4.74 4.9 4.22 4.39 4.22
|
||||
C 3.88 4.22 3.47 4.74 3.47 5.38
|
||||
C 3.47 6.03 3.88 6.54 4.39 6.54
|
||||
Z
|
||||
M 6.9 6.54
|
||||
C 7.41 6.54 7.82 6.03 7.82 5.38
|
||||
C 7.82 4.74 7.41 4.22 6.9 4.22
|
||||
C 6.39 4.22 5.98 4.74 5.98 5.38
|
||||
C 5.98 6.03 6.39 6.54 6.9 6.54
|
||||
Z
|
||||
M 4.25 6
|
||||
C 4.44 6 4.6 5.79 4.6 5.53
|
||||
C 4.6 5.27 4.44 5.05 4.25 5.05
|
||||
C 4.05 5.05 3.89 5.27 3.89 5.53
|
||||
C 3.89 5.79 4.05 6 4.25 6
|
||||
Z
|
||||
M 6.76 6
|
||||
C 6.95 6 7.11 5.79 7.11 5.53
|
||||
C 7.11 5.27 6.95 5.05 6.76 5.05
|
||||
C 6.56 5.05 6.4 5.27 6.4 5.53
|
||||
C 6.4 5.79 6.56 6 6.76 6
|
||||
Z"></path>
|
||||
</g>
|
||||
|
||||
</defs>
|
||||
|
||||
<g id="maze">
|
||||
{walls.map(([x, y], index) => (
|
||||
<rect
|
||||
key={`${x},${y}`}
|
||||
x={x * cellSize}
|
||||
y={y * cellSize}
|
||||
width={cellSize}
|
||||
height={cellSize}
|
||||
fill="transparent"
|
||||
/>
|
||||
))}
|
||||
{clusters.map((cluster, index) => (
|
||||
<path
|
||||
className="maze"
|
||||
key={`${cluster[0]},${cluster[1]}-${cluster.length}`}
|
||||
d={createPath(cluster)}
|
||||
stroke="darkblue"
|
||||
strokeWidth="2"
|
||||
fill="transparent"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="bevel"
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
{food.map(([x, y], index) => (
|
||||
<circle key={index} cx={(0.5 + x) * cellSize} cy={(0.5 + y) * cellSize} r={cellSize / 5} className={x < width / 2 ? "foodblue" : "foodred"}></circle>
|
||||
))}
|
||||
|
||||
<Bot position={a} key="botA" color="blue" width={width}></Bot>
|
||||
<Bot position={b} key="botB" color="blue" width={width}></Bot>
|
||||
<Bot position={x} key="botX" color="red" width={width}></Bot>
|
||||
<Bot position={y} key="botY" color="red" width={width}></Bot>
|
||||
|
||||
{
|
||||
gameover ? (<text fontSize="100" className="gameover"
|
||||
x="50%" y="50%"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
>
|
||||
GAME OVER
|
||||
</text>) : null
|
||||
}
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Maze;
|
||||
|
104
app/page.tsx
104
app/page.tsx
|
@ -1,112 +1,16 @@
|
|||
import Image from "next/image";
|
||||
import Pelita from "./pelita";
|
||||
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
Get started by editing
|
||||
<code className="font-mono font-bold">app/page.tsx</code>
|
||||
</p>
|
||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
|
||||
<a
|
||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{" "}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className="dark:invert"
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
|
||||
<Image
|
||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<Pelita>
|
||||
|
||||
<div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Docs{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Find in-depth information about Next.js features and API.
|
||||
</p>
|
||||
</a>
|
||||
</Pelita>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Learn{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Learn about Next.js in an interactive course with quizzes!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Templates{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Explore starter templates for Next.js.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className="mb-3 text-2xl font-semibold">
|
||||
Deploy{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
|
||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
|
140
app/pelita.tsx
Normal file
140
app/pelita.tsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
"use client"
|
||||
|
||||
import React, {
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef
|
||||
} from "react";
|
||||
|
||||
|
||||
import Maze from "./maze";
|
||||
import ZMQReceiver from "./zmqreceiver";
|
||||
import TypewriterText from "./typewritertext";
|
||||
import { GameState } from "./zmqreceiver";
|
||||
|
||||
interface GameStats {
|
||||
points: [number, number];
|
||||
errors: [number, number];
|
||||
kills: [number, number, number, number];
|
||||
deaths: [number, number, number, number];
|
||||
time: [number, number];
|
||||
}
|
||||
|
||||
function Pelita() {
|
||||
const [showPre, setShowPre] = React.useState(true);
|
||||
const [showMain, setShowMain] = React.useState(true);
|
||||
const [getText, setText] = React.useState<string[]>([]);
|
||||
|
||||
const [gameUUID, setGameUUID] = React.useState("");
|
||||
const [shape, setShape] = React.useState<[number, number]>([0, 0]);
|
||||
const [walls, setWalls] = React.useState<[number, number][]>([]);
|
||||
const [food, setFood] = React.useState<[number, number][]>([]);
|
||||
const [bots, setBots] = React.useState<[number, number][]>([[0, 0], [0, 0], [0, 0], [0, 0]]);
|
||||
|
||||
const [team1, setTeam1] = React.useState("");
|
||||
const [team2, setTeam2] = React.useState("");
|
||||
|
||||
const [stats, setStats] = React.useState<GameStats | null>(null);
|
||||
const [whoWins, setWhoWins] = React.useState<number | null>(null);
|
||||
const [gameOver, setGameOver] = React.useState<boolean>(false);
|
||||
|
||||
const flip = () => {
|
||||
//setShowMain(!showMain);
|
||||
//setShowPre(!showPre);
|
||||
};
|
||||
|
||||
const updateGameState = (gameState: { '__data__': GameState }) => {
|
||||
if (gameState['__data__']) {
|
||||
// if (gameUUID != gameState['__data__']['game_uuid']) {
|
||||
setGameUUID((oldUUID) => {
|
||||
if (oldUUID != gameState['__data__']['game_uuid']) {
|
||||
console.log("UUID changed", gameState['__data__']['game_uuid'], oldUUID);
|
||||
setShape(gameState['__data__']['shape'])
|
||||
setWalls(gameState['__data__']['walls']);
|
||||
}
|
||||
return gameState['__data__']['game_uuid'];
|
||||
})
|
||||
// }
|
||||
setFood(gameState['__data__']['food']);
|
||||
setBots(gameState['__data__']['bots']);
|
||||
setTeam1(gameState['__data__']['team_names'][0]);
|
||||
setTeam2(gameState['__data__']['team_names'][1]);
|
||||
setStats(
|
||||
{
|
||||
"deaths": gameState['__data__']['deaths'],
|
||||
"kills": gameState['__data__']['kills'],
|
||||
"errors": gameState['__data__']['num_errors'],
|
||||
"points": gameState['__data__']['score'],
|
||||
"time": gameState['__data__']['team_time'],
|
||||
}
|
||||
);
|
||||
setWhoWins(gameState['__data__']['whowins']);
|
||||
setGameOver(gameState['__data__']['gameover']);
|
||||
}
|
||||
}
|
||||
|
||||
const updateMessage = (msg: string) => {
|
||||
setText(oldText => [...oldText, msg]);
|
||||
}
|
||||
|
||||
const clearPage = () => {
|
||||
setText([]);
|
||||
}
|
||||
|
||||
const a = bots[0];
|
||||
const b = bots[2];
|
||||
const x = bots[1];
|
||||
const y = bots[3];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="fixed top-0 left-0 z-20 w-full p-4 bg-white border-t border-gray-200 shadow md:flex md:items-center md:justify-between md:p-6 dark:bg-gray-800 dark:border-gray-600">
|
||||
ᗧ Pelita Tournament 2024
|
||||
</h1>
|
||||
|
||||
{showPre ?
|
||||
getText.map((t, i) => (<TypewriterText key={i} text={t}></TypewriterText>))
|
||||
: null}
|
||||
|
||||
|
||||
{showMain ?
|
||||
<div id="main">
|
||||
{gameUUID ?
|
||||
(<>
|
||||
<h2 className="flex flex-row text-lg">
|
||||
<span className="basis-1/2"><b>{team1}</b> ({stats.points[0]})</span>
|
||||
<span className="basis-1/2 text-right"><b>{team2}</b> ({stats.points[1]})</span>
|
||||
</h2>
|
||||
<div className="flex flex-row">
|
||||
<div className="basis-1/2">Errors: {stats.errors[0]}, Kills: {stats.kills[0] + stats.kills[2]}, Deaths: {stats.deaths[0] + stats.deaths[2]}, Time: {stats.time[0].toFixed(2)} </div>
|
||||
<div className="basis-1/2 text-right">Errors: {stats.errors[1]}, Kills: {stats.kills[1] + stats.kills[3]}, Deaths: {stats.deaths[1] + stats.deaths[3]}, Time: {stats.time[1].toFixed(2)} </div>
|
||||
</div>
|
||||
</>)
|
||||
: null}
|
||||
|
||||
<Maze
|
||||
key={gameUUID}
|
||||
game_uuid={gameUUID}
|
||||
shape={shape}
|
||||
walls={walls}
|
||||
food={food}
|
||||
a={a}
|
||||
b={b}
|
||||
x={x}
|
||||
y={y}
|
||||
whowins={whoWins}
|
||||
gameover={gameOver}>
|
||||
</Maze>
|
||||
<p className='text-xs text-slate-600 text-right'>{gameUUID}</p>
|
||||
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
<ZMQReceiver url='ws://127.0.0.1:5556' sendGameState={updateGameState} sendMessage={updateMessage} sendClearPage={clearPage}></ZMQReceiver>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Pelita;
|
24
app/typewritertext.module.css
Normal file
24
app/typewritertext.module.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
|
||||
.text_animation {
|
||||
display: none;
|
||||
position: relative;
|
||||
font-size: 20px;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.text_animation .letter {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.text_animation .cursor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0.55em;
|
||||
/* height: 1em; */
|
||||
/* background: #f5f5f5; */
|
||||
background: black;
|
||||
z-index: 1;
|
||||
}
|
75
app/typewritertext.tsx
Normal file
75
app/typewritertext.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import React, { ReactNode, useEffect, useRef } from "react";
|
||||
|
||||
import anime from "animejs/lib/anime.es.js";
|
||||
import styles from "./typewritertext.module.css";
|
||||
|
||||
function TypewriterText({ text }: { text: string }) {
|
||||
// IMPORTANT: This code chokes on an empty string. Logic needs to be refactored
|
||||
const lettersHtml: ReactNode[] = (text ? text : " ").split("").map((c, i) => {
|
||||
return (
|
||||
<span className={`letter ${styles.letter}`} key={`letter,${i}`}>
|
||||
{c == " " ? "\u00A0" : c}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
const lineHtml = (
|
||||
<>
|
||||
<div className="letters">{lettersHtml}</div>
|
||||
<span className={`cursor ${styles.cursor}`}></span>
|
||||
</>
|
||||
);
|
||||
const text_animation_ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = text_animation_ref.current!;
|
||||
el.style.display = "block";
|
||||
const letters = Array.from(el.querySelectorAll(".letter")) as HTMLElement[];
|
||||
const TYPE_AFTER_MS = 1_000;
|
||||
const JUMP_AFTER_MS = 80;
|
||||
|
||||
const blink = anime({
|
||||
targets: el.querySelectorAll(".cursor"),
|
||||
loop: true,
|
||||
duration: 750,
|
||||
opacity: [{ value: [1, 1] }, { value: [0, 0] }],
|
||||
});
|
||||
|
||||
const tl = anime
|
||||
.timeline()
|
||||
.add(
|
||||
{
|
||||
targets: el.querySelectorAll(".cursor"),
|
||||
translateX: letters.map((letter, i) => ({
|
||||
value: letter.offsetLeft + letter.offsetWidth,
|
||||
duration: 1,
|
||||
delay: i === 0 ? 0 : JUMP_AFTER_MS,
|
||||
})),
|
||||
},
|
||||
TYPE_AFTER_MS
|
||||
)
|
||||
.add(
|
||||
{
|
||||
targets: el.querySelectorAll(".letter"),
|
||||
opacity: [0, 1],
|
||||
duration: 1,
|
||||
delay: anime.stagger(JUMP_AFTER_MS),
|
||||
changeBegin: () => {
|
||||
blink.restart();
|
||||
blink.pause();
|
||||
},
|
||||
changeComplete: () => {
|
||||
blink.restart();
|
||||
},
|
||||
},
|
||||
TYPE_AFTER_MS
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={text_animation_ref} className={styles.text_animation}>
|
||||
<h1>{lineHtml}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TypewriterText;
|
31
app/zmqmessages.tsx
Normal file
31
app/zmqmessages.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
// pages/zmq.js
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getMessages } from '../zmqServer';
|
||||
|
||||
export default function ZMQMessages() {
|
||||
const [messages, setMessages] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMessages = async () => {
|
||||
const res = await fetch('/api/messages');
|
||||
const data = await res.json();
|
||||
setMessages(data.messages);
|
||||
};
|
||||
|
||||
fetchMessages();
|
||||
|
||||
const interval = setInterval(fetchMessages, 1000); // Fetch new messages every second
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>ZMQ Messages</h1>
|
||||
<ul>
|
||||
{messages.map((msg, index) => (
|
||||
<li key={index}>{msg}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
64
app/zmqreceiver.tsx
Normal file
64
app/zmqreceiver.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as zmq from 'jszmq';
|
||||
|
||||
interface GameState {
|
||||
game_uuid: string;
|
||||
shape: [number, number];
|
||||
walls: [number, number][];
|
||||
food: [number, number][];
|
||||
bots: [number, number][];
|
||||
team_names: [string, string];
|
||||
}
|
||||
|
||||
export type { GameState };
|
||||
|
||||
const ZMQReceiver = ({ url, sendGameState, sendMessage, sendClearPage }: { url: string,
|
||||
sendGameState: (gs: {"__data__": GameState}) => any,
|
||||
sendMessage: (msg: string) => any,
|
||||
sendClearPage: () => any
|
||||
}
|
||||
|
||||
) => {
|
||||
const [messages, setMessages] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('hi');
|
||||
const socket = zmq.socket('sub');
|
||||
socket.options.reconnectInterval = 1000;
|
||||
socket.connect(url);
|
||||
socket.subscribe('');
|
||||
|
||||
socket.on('message', (message) => {
|
||||
setMessages((prevMessages) => [/*...prevMessages, */message.toString()]);
|
||||
let parsed = JSON.parse(message);
|
||||
console.log(parsed);
|
||||
if (parsed['__action__'] && parsed['__action__'] === 'SPEAK') {
|
||||
sendMessage(parsed['__data__']);
|
||||
} else if (parsed['__action__'] && parsed['__action__'] === 'CLEAR') {
|
||||
sendClearPage();
|
||||
} else {
|
||||
sendGameState(parsed);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.unsubscribe('');
|
||||
socket.close();
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<footer className="fixed bottom-0 left-0 z-20 w-full p-4 bg-white border-t border-gray-200 shadow md:flex md:items-center md:justify-between md:p-6 dark:bg-gray-800 dark:border-gray-600">
|
||||
<h2 className='text-xs text-slate-300'>ZeroMQ Messages:</h2>
|
||||
<ul>
|
||||
{messages.map((message, index) => (
|
||||
<li key={index} className='text-xs text-slate-300' style={{fontSize: 0.45 + 'rem', lineHeight: 0.5 + 'rem'}}>{message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ZMQReceiver;
|
539
package-lock.json
generated
539
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
@ -9,18 +9,22 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"animejs": "^3.2.2",
|
||||
"jszmq": "^0.1.2",
|
||||
"next": "^14.2.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.2.4"
|
||||
"zeromq": "^6.0.0-beta.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/animejs": "^3.1.12",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.4"
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue