Lots of changes
This commit is contained in:
parent
931534b990
commit
42baea3403
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--foreground-rgb: 0, 0, 0;
|
--foreground-rgb: 0, 0, 0;
|
||||||
--background-start-rgb: 214, 219, 220;
|
--background-start-rgb: 255, 255, 255;
|
||||||
--background-end-rgb: 255, 255, 255;
|
--background-end-rgb: 255, 255, 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,43 @@ body {
|
||||||
rgb(var(--background-end-rgb))
|
rgb(var(--background-end-rgb))
|
||||||
)
|
)
|
||||||
rgb(var(--background-start-rgb));
|
rgb(var(--background-start-rgb));
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crt {
|
||||||
|
background: linear-gradient(to top, #000000, #000000, #333333, #333333);
|
||||||
|
background-size: cover;
|
||||||
|
background-size: 100% 2px;
|
||||||
|
|
||||||
|
color: #00ff00;
|
||||||
|
font-size: 50px;
|
||||||
|
/* text-align: center; */
|
||||||
|
text-shadow: 0 0 10px #00ff00;
|
||||||
|
|
||||||
|
filter: blur(.8px);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-bot {
|
||||||
|
color: rgb(235, 90, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue-bot {
|
||||||
|
color: rgb(94, 158, 217);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen{
|
||||||
|
.crt {
|
||||||
|
animation: scanlines infinite 55s linear ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes scanlines {
|
||||||
|
from {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-position: 0 -10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|
368
app/maze.tsx
368
app/maze.tsx
|
@ -1,14 +1,18 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
useEffect
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import anime from 'animejs/lib/anime.es.js';
|
import anime from 'animejs/lib/anime.es.js';
|
||||||
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
|
||||||
type Pos = [number, number];
|
type Pos = [number, number];
|
||||||
|
|
||||||
const cellSize = 32; // Size of each cell in the SVG
|
const cellSize = 26; // Size of each cell in the SVG
|
||||||
const offset = 0.2 * cellSize; // Offset for the outline
|
const offset = 0.2 * cellSize; // Offset for the outline
|
||||||
let radius = 0.5 * cellSize - offset;
|
let radius = 0.5 * cellSize - offset;
|
||||||
let radius_inner = offset;
|
let radius_inner = offset;
|
||||||
|
@ -235,12 +239,89 @@ const createPath = (cluster: Pos[]) => {
|
||||||
return paths;
|
return paths;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(pathCommands);
|
|
||||||
return pathCommands.join(" ");
|
return pathCommands.join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function usePrevious(value: any) {
|
||||||
|
const ref = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value; //assign the value of ref to the argument
|
||||||
|
}, [value]); //this code will run when the value of 'value' changes
|
||||||
|
return ref.current; //in the end, return the current ref value.
|
||||||
|
}
|
||||||
|
|
||||||
function Bot({ position, color, width }: { position: Pos, color: string, width: number }) {
|
function Food({ position, color }: { position: Pos, color: string }) {
|
||||||
|
const [x, y] = position;
|
||||||
|
return (
|
||||||
|
<motion.circle
|
||||||
|
cx={(0.5 + x) * cellSize}
|
||||||
|
cy={(0.5 + y) * cellSize}
|
||||||
|
|
||||||
|
r={cellSize / 5}
|
||||||
|
opacity={1}
|
||||||
|
className={color}
|
||||||
|
// transition={{ duration: 1 }}
|
||||||
|
// initial={{ opacity: 0 }}
|
||||||
|
// animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0, r: cellSize }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// // from https://www.30secondsofcode.org/react/s/use-interval-explained/
|
||||||
|
// const useInterval = (callback: any, delay: number) => {
|
||||||
|
// const savedCallback = React.useRef();
|
||||||
|
|
||||||
|
// React.useEffect(() => {
|
||||||
|
// savedCallback.current = callback;
|
||||||
|
// }, [callback]);
|
||||||
|
|
||||||
|
// React.useEffect(() => {
|
||||||
|
// function tick() {
|
||||||
|
// savedCallback?.current();
|
||||||
|
// }
|
||||||
|
// if (delay !== null) {
|
||||||
|
// let id = setInterval(tick, delay);
|
||||||
|
// return () => clearInterval(id);
|
||||||
|
// }
|
||||||
|
// }, [delay]);
|
||||||
|
// };
|
||||||
|
|
||||||
|
function Pacman({ direction, mouthAngle, color }: { direction: number, mouthAngle: number, color: string }) {
|
||||||
|
|
||||||
|
const pacmanPath = (angle: number) => {
|
||||||
|
const angle_rad = angle / 180 * Math.PI;
|
||||||
|
const radius = 8;
|
||||||
|
const x = radius * Math.cos(angle_rad / 2);
|
||||||
|
const y = radius * Math.sin(angle_rad / 2);
|
||||||
|
return `M 0,0 L ${x},${-y} A ${radius},${radius} 0 1 0 ${x},${y} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g transform={ `rotate(${direction})` }
|
||||||
|
// style={{
|
||||||
|
// transform: `rotate(${direction})`
|
||||||
|
// }}
|
||||||
|
// animate={{
|
||||||
|
// rotate: direction
|
||||||
|
// }}
|
||||||
|
// transition={{ type: "spring", stiffness: 100 }}
|
||||||
|
className={color}>
|
||||||
|
<path
|
||||||
|
d={pacmanPath(mouthAngle)}
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth={0.2}
|
||||||
|
/>
|
||||||
|
<motion.circle cx={2.7} cy={direction < 160 ? -4.5 : 4.5} r={1.5}
|
||||||
|
className={`eye`}
|
||||||
|
stroke="black"
|
||||||
|
fill="yellow"
|
||||||
|
strokeWidth={0.2}
|
||||||
|
/>
|
||||||
|
</g>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Bot({ position, color, say, width, turnsAgo }: { position: Pos, color: string, say: string, width: number, turnsAgo: number }) {
|
||||||
const leftSide = position[0] < width / 2;
|
const leftSide = position[0] < width / 2;
|
||||||
const inHomezone = () => {
|
const inHomezone = () => {
|
||||||
switch (color) {
|
switch (color) {
|
||||||
|
@ -251,6 +332,24 @@ function Bot({ position, color, width }: { position: Pos, color: string, width:
|
||||||
return !leftSide;
|
return !leftSide;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const [direction, setDirection] = useState(leftSide ? 0 : 180);
|
||||||
|
const oldPosition = usePrevious(position);
|
||||||
|
|
||||||
|
// const [mouthAngleTL, setMouthAngleTL] = useState(0);
|
||||||
|
// useInterval(() => setMouthAngleTL(mouthAngleTL + 0.08), 10);
|
||||||
|
|
||||||
|
const mouthAngle = 50; // Math.abs(50 * Math.sin(mouthAngleTL));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (oldPosition) {
|
||||||
|
const dx = position[0] - oldPosition[0];
|
||||||
|
const dy = position[1] - oldPosition[1];
|
||||||
|
if (dx < 0) setDirection(180);
|
||||||
|
else if (dy < 0) setDirection(270);
|
||||||
|
else if (dx > 0) setDirection(0);
|
||||||
|
else if (dy > 0) setDirection(90);
|
||||||
|
}
|
||||||
|
}, [oldPosition]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
anime.timeline()
|
anime.timeline()
|
||||||
|
@ -269,27 +368,121 @@ function Bot({ position, color, width }: { position: Pos, color: string, width:
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<use transform={`translate(${(position[0]) * cellSize}, ${(position[1]) * cellSize}) scale(3)`} href={inHomezone() ? `#ghost` : `#pacman`} className={color} />
|
<motion.g
|
||||||
|
style={{
|
||||||
|
transform: `translateX(${(position[0] + 0.5) * cellSize}) translateY(${(position[0] + 0.5) * cellSize}) scale(${cellSize / 16})`,
|
||||||
|
zIndex: 50 - turnsAgo
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
x: (position[0] + 0.5) * cellSize,
|
||||||
|
y: (position[1] + 0.5) * cellSize,
|
||||||
|
scale: cellSize / 16
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
inHomezone() ? (
|
||||||
|
<g
|
||||||
|
id="ghost"
|
||||||
|
className={`${color} ghost`}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* Round path: // M -8 0 C -8 -4.4 -4.4 -8 0 -8 C 4.4 -8 8 -4.4 8 0 L 8 8 C 8 9 6.6667 5.6 6 5.6 S 4.6667 8.19 4 8.19 S 2.6667 5.6 2 5.6 S 0.6667 8.19 0 8.19 S -1.3333 5.6 -2 5.6 S -3.3333 8.19 -4 8.19 S -5.3333 5.6 -6 5.6 S -8 9 -8 8 C -8 5.3333 -8 2.6667 -8 0 Z */}
|
||||||
|
{/* Straight path: // M -8 0 C -8 -4.4 -4.4 -8 0 -8 C 4.4 -8 8 -4.4 8 0 L 8 8 L 6 5.6 L 4 8 L 2 5.6 L 0 8 L -2 5.6 L -4 8 L -6 5.6 L -8 8 L -8 0 Z */}
|
||||||
|
|
||||||
|
<path d="M -8 0 C -8 -4.4 -4.4 -8 0 -8 C 4.4 -8 8 -4.4 8 0 L 8 8 C 8 9 6.6667 5.6 6 5.6 S 4.6667 8.19 4 8.19 S 2.6667 5.6 2 5.6 S 0.6667 8.19 0 8.19 S -1.3333 5.6 -2 5.6 S -3.3333 8.19 -4 8.19 S -5.3333 5.6 -6 5.6 S -8 9 -8 8 C -8 5.3333 -8 2.6667 -8 0 Z"
|
||||||
|
|
||||||
|
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth={0.2}
|
||||||
|
opacity={0.9}
|
||||||
|
></path>
|
||||||
|
<path d="M -3.2 1.1 C -2.2 1.1 -1.4 0.1 -1.4 -1.2 C -1.4 -2.5 -2.2 -3.6 -3.2 -3.6 C -4.2 -3.6 -5.1 -2.5 -5.1 -1.2 C -5.1 0.1 -4.2 1.1 -3.2 1.1 Z
|
||||||
|
M 1.8 1.1 C 2.8 1.1 3.6 0.1 3.6 -1.2 C 3.6 -2.5 2.8 -3.6 1.8 -3.6 C 0.8 -3.6 -0 -2.5 -0 -1.2 C -0 0.1 0.8 1.1 1.8 1.1 Z"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth={0.2}
|
||||||
|
fill="white"
|
||||||
|
></path>
|
||||||
|
<path d="M -3.5 0 C -3.1 0 -2.8 -0.4 -2.8 -0.9 C -2.8 -1.5 -3.1 -1.9 -3.5 -1.9 C -3.9 -1.9 -4.2 -1.5 -4.2 -0.9 C -4.2 -0.4 -3.9 0 -3.5 0 Z
|
||||||
|
M 1.5 0 C 1.9 0 2.2 -0.4 2.2 -0.9 C 2.2 -1.5 1.9 -1.9 1.5 -1.9 C 1.1 -1.9 0.8 -1.5 0.8 -0.9 C 0.8 -0.4 1.1 0 1.5 0 Z"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth={0.2}
|
||||||
|
fill="black"
|
||||||
|
>
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
) : (
|
||||||
|
<Pacman direction={direction} mouthAngle={mouthAngle} color={`${color} pacman`}></Pacman>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<text y="-10" className="sayBg">{say}</text>
|
||||||
|
<text y="-10" className="say">{say}</text>
|
||||||
|
</motion.g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Walls({ shape, walls }: { shape: Pos, walls: Pos[] }) {
|
||||||
|
const clusters = useMemo(() => findClusters(shape, walls), [shape, walls]);
|
||||||
|
const [width, height] = shape;
|
||||||
|
|
||||||
function Maze({ game_uuid, shape, walls, food, a, b, x, y, whowins, gameover }:
|
return (
|
||||||
|
<g id="maze">
|
||||||
|
<line x1={(width) * cellSize / 2} y1={0.3 * cellSize}
|
||||||
|
x2={width * cellSize / 2} y2={(height - 0.3) * cellSize} className="middleLine blackLine" />
|
||||||
|
<line x1={(width - 0.1) * cellSize / 2} y1={0.3 * cellSize}
|
||||||
|
x2={(width - 0.1) * cellSize / 2} y2={(height - 0.3) * cellSize} className="middleLine blueLine" />
|
||||||
|
<line x1={(width + 0.1) * cellSize / 2} y1={0.3 * cellSize}
|
||||||
|
x2={(width + 0.1) * cellSize / 2} y2={(height - 0.3) * cellSize} className="middleLine redLine" />
|
||||||
|
{walls.map(([x, y], index) => (
|
||||||
|
<rect
|
||||||
|
key={`${x},${y}`}
|
||||||
|
x={x * cellSize}
|
||||||
|
y={y * cellSize}
|
||||||
|
width={cellSize}
|
||||||
|
height={cellSize}
|
||||||
|
opacity="0"
|
||||||
|
// fill="lightblue"
|
||||||
|
stroke="lightgrey"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{clusters.map((cluster, index) => (
|
||||||
|
<path
|
||||||
|
className="maze"
|
||||||
|
key={`${cluster[0]},${cluster[1]}-${cluster.length}`}
|
||||||
|
d={createPath(cluster)}
|
||||||
|
// stroke="lightblue"
|
||||||
|
// stroke={ dark_mode ? "url(#grad)" : "black" }
|
||||||
|
stroke="url(#grad)"
|
||||||
|
strokeWidth="2"
|
||||||
|
//fill={ dark_mode ? "#ffa" : "black" }
|
||||||
|
fill="black"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="bevel"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, gameover, round, turn }:
|
||||||
{
|
{
|
||||||
game_uuid: string,
|
game_uuid: string,
|
||||||
shape: Pos,
|
shape: Pos,
|
||||||
walls: Pos[],
|
walls: Pos[],
|
||||||
food: Pos[],
|
food: Pos[],
|
||||||
a: Pos,
|
bots: [Pos, Pos, Pos, Pos],
|
||||||
b: Pos,
|
team_names: [string, string],
|
||||||
x: Pos,
|
say: [string, string, string, string],
|
||||||
y: Pos,
|
|
||||||
whowins: number | null,
|
whowins: number | null,
|
||||||
gameover: boolean
|
gameover: boolean,
|
||||||
|
round: number,
|
||||||
|
turn: number,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const [width, height] = shape;
|
const [width, height] = shape;
|
||||||
const clusters = findClusters(shape, walls);
|
const [a, x, b, y] = bots;
|
||||||
|
const [sayA, sayX, sayB, sayY] = say;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (game_uuid) {
|
if (game_uuid) {
|
||||||
|
@ -310,6 +503,12 @@ function Maze({ game_uuid, shape, walls, food, a, b, x, y, whowins, gameover }:
|
||||||
easing: 'linear',
|
easing: 'linear',
|
||||||
duration: 2000
|
duration: 2000
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
.add({
|
||||||
|
targets: '#mazebox #maze path',
|
||||||
|
strokeWidth: 0,
|
||||||
|
easing: 'linear',
|
||||||
|
duration: 2000
|
||||||
|
}, 4000)
|
||||||
.add({
|
.add({
|
||||||
targets: '#mazebox .foodblue',
|
targets: '#mazebox .foodblue',
|
||||||
opacity: [0, 1],
|
opacity: [0, 1],
|
||||||
|
@ -333,6 +532,12 @@ function Maze({ game_uuid, shape, walls, food, a, b, x, y, whowins, gameover }:
|
||||||
opacity: [0, 1],
|
opacity: [0, 1],
|
||||||
easing: 'linear',
|
easing: 'linear',
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
|
}, 3500)
|
||||||
|
.add({
|
||||||
|
targets: '#mazebox .middleLine',
|
||||||
|
opacity: [0, 1],
|
||||||
|
easing: 'linear',
|
||||||
|
duration: 2000,
|
||||||
}, 3500);
|
}, 3500);
|
||||||
}
|
}
|
||||||
}, [walls, game_uuid]);
|
}, [walls, game_uuid]);
|
||||||
|
@ -371,12 +576,13 @@ function Maze({ game_uuid, shape, walls, food, a, b, x, y, whowins, gameover }:
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="mazebox">
|
<div id="mazebox" className="object-fill">
|
||||||
<svg
|
<svg
|
||||||
width={width * cellSize}
|
// width={width * cellSize}
|
||||||
height={height * cellSize}
|
// height={height * cellSize}
|
||||||
viewBox={`0 0 ${width * cellSize} ${height * cellSize}`}
|
viewBox={`0 0 ${width * cellSize} ${height * cellSize}`}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{ overflow: "visible" }}
|
||||||
>
|
>
|
||||||
<style type="text/css">{`
|
<style type="text/css">{`
|
||||||
line {
|
line {
|
||||||
|
@ -385,11 +591,11 @@ function Maze({ game_uuid, shape, walls, food, a, b, x, y, whowins, gameover }:
|
||||||
stroke-width: 3;
|
stroke-width: 3;
|
||||||
}
|
}
|
||||||
.foodblue {
|
.foodblue {
|
||||||
stroke: black;
|
// stroke: black;
|
||||||
fill: rgb(94, 158, 217);
|
fill: rgb(94, 158, 217);
|
||||||
}
|
}
|
||||||
.foodred {
|
.foodred {
|
||||||
stroke: black;
|
// stroke: black;
|
||||||
fill: rgb(235, 90, 90);
|
fill: rgb(235, 90, 90);
|
||||||
}
|
}
|
||||||
.blue {
|
.blue {
|
||||||
|
@ -398,6 +604,28 @@ function Maze({ game_uuid, shape, walls, food, a, b, x, y, whowins, gameover }:
|
||||||
.red {
|
.red {
|
||||||
fill: rgb(235, 90, 90);
|
fill: rgb(235, 90, 90);
|
||||||
}
|
}
|
||||||
|
.blueLine {
|
||||||
|
stroke: rgb(94, 158, 217);
|
||||||
|
}
|
||||||
|
.redLine {
|
||||||
|
stroke: rgb(235, 90, 90);
|
||||||
|
}
|
||||||
|
.sayBg {
|
||||||
|
stroke-width: 1.7px;
|
||||||
|
stroke: white;
|
||||||
|
font-size: 7px;
|
||||||
|
text-anchor: middle;
|
||||||
|
dominant-baseline: middle;
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
.say {
|
||||||
|
// stroke-width: 0.2px;
|
||||||
|
// stroke: white;
|
||||||
|
font-size: 7px;
|
||||||
|
text-anchor: middle;
|
||||||
|
dominant-baseline: middle;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
.gameover {
|
.gameover {
|
||||||
fill: #FFC903;
|
fill: #FFC903;
|
||||||
|
@ -416,101 +644,37 @@ function Maze({ game_uuid, shape, walls, food, a, b, x, y, whowins, gameover }:
|
||||||
<stop stopColor="red" offset="50%" />
|
<stop stopColor="red" offset="50%" />
|
||||||
<stop stopColor="red" offset="100%" />
|
<stop stopColor="red" offset="100%" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<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>
|
</defs>
|
||||||
|
|
||||||
<g id="maze">
|
<Walls shape={shape} walls={walls}></Walls>
|
||||||
{walls.map(([x, y], index) => (
|
<AnimatePresence>
|
||||||
<rect
|
{food.map(([x, y], index) => (
|
||||||
key={`${x},${y}`}
|
<Food key={`${x},${y}`} position={[x, y]} color={x < width / 2 ? "foodblue" : "foodred"}></Food>
|
||||||
x={x * cellSize}
|
|
||||||
y={y * cellSize}
|
|
||||||
width={cellSize}
|
|
||||||
height={cellSize}
|
|
||||||
opacity="0"
|
|
||||||
// fill="lightblue"
|
|
||||||
stroke="lightgrey"
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
{clusters.map((cluster, index) => (
|
</AnimatePresence>
|
||||||
<path
|
|
||||||
className="maze"
|
|
||||||
key={`${cluster[0]},${cluster[1]}-${cluster.length}`}
|
|
||||||
d={createPath(cluster)}
|
|
||||||
// stroke="lightblue"
|
|
||||||
stroke="url(#grad)"
|
|
||||||
strokeWidth="2"
|
|
||||||
fill="#ffa"
|
|
||||||
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={a} key="botA" color="blue" say={sayA} width={width} turnsAgo={turn}></Bot>
|
||||||
<Bot position={b} key="botB" color="blue" width={width}></Bot>
|
<Bot position={x} key="botX" color="red" say={sayX} width={width} turnsAgo={(turn + 3) % 4}></Bot>
|
||||||
<Bot position={x} key="botX" color="red" width={width}></Bot>
|
<Bot position={b} key="botB" color="blue" say={sayB} width={width} turnsAgo={(turn + 2) % 4}></Bot>
|
||||||
<Bot position={y} key="botY" color="red" width={width}></Bot>
|
<Bot position={y} key="botY" color="red" say={sayY} width={width} turnsAgo={(turn + 1) % 4}></Bot>
|
||||||
|
|
||||||
{
|
{
|
||||||
gameover ? (<text fontSize="100" className="gameover"
|
gameover ? (<>
|
||||||
x="50%" y="50%"
|
<text fontSize="100" className="gameover"
|
||||||
|
x="50%" y="25%"
|
||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
>
|
>
|
||||||
GAME OVER
|
GAME OVER
|
||||||
</text>) : null
|
</text>
|
||||||
|
<text fontSize="100" className="gameover"
|
||||||
|
x="50%" y="75%"
|
||||||
|
dominantBaseline="middle"
|
||||||
|
textAnchor="middle"
|
||||||
|
>
|
||||||
|
{ whowins == 2 ? "DRAW" : `${team_names[whowins]} wins!` }
|
||||||
|
</text>
|
||||||
|
</>) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
16
app/page.tsx
16
app/page.tsx
|
@ -1,17 +1,7 @@
|
||||||
import Image from "next/image";
|
|
||||||
import Pelita from "./pelita";
|
import Pelita from "./pelita";
|
||||||
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (<>
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
<Pelita></Pelita>
|
||||||
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
|
</>);
|
||||||
|
|
||||||
<Pelita>
|
|
||||||
|
|
||||||
</Pelita>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
273
app/pelita.tsx
273
app/pelita.tsx
|
@ -1,9 +1,9 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
ReactNode,
|
Reducer,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef
|
useReducer
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,130 +11,207 @@ import Maze from "./maze";
|
||||||
import ZMQReceiver from "./zmqreceiver";
|
import ZMQReceiver from "./zmqreceiver";
|
||||||
import TypewriterText from "./typewritertext";
|
import TypewriterText from "./typewritertext";
|
||||||
import { GameState } from "./zmqreceiver";
|
import { GameState } from "./zmqreceiver";
|
||||||
|
import anime from "animejs";
|
||||||
|
|
||||||
interface GameStats {
|
type PelitaState = "initial" | "movie" | "intro" | "match" | "faulted";
|
||||||
points: [number, number];
|
type PelitaEvent = "start-movie" | "start-intro" | "game-playing" | "clear-page" | "fail";
|
||||||
errors: [number, number];
|
|
||||||
kills: [number, number, number, number];
|
const MAX_LINES = 20;
|
||||||
deaths: [number, number, number, number];
|
|
||||||
time: [number, number];
|
const reducer: Reducer<PelitaState, PelitaEvent> = (state, event) => {
|
||||||
|
switch (state) {
|
||||||
|
case "initial":
|
||||||
|
if (event === "start-movie") return "movie";
|
||||||
|
break;
|
||||||
|
case "movie":
|
||||||
|
if (event === "start-intro") return "intro";
|
||||||
|
break;
|
||||||
|
case "intro":
|
||||||
|
if (event === "game-playing") return "match";
|
||||||
|
if (event === "clear-page") return "intro";
|
||||||
|
break;
|
||||||
|
case "match":
|
||||||
|
if (event === "game-playing") return "match";
|
||||||
|
if (event === "clear-page") return "intro";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
function PelitaMain({ gameState }: { gameState: GameState }) {
|
||||||
|
const [team1, team2] = gameState.team_names;
|
||||||
|
|
||||||
|
return <div id="main">
|
||||||
|
<h2 className="flex flex-row text-lg p-2">
|
||||||
|
<span className="basis-1/2 text-right w-64 blue-bot"><b>{team1}</b> {gameState.game_stats.score[0]}</span>
|
||||||
|
<span className="basis-1 px-2">:</span>
|
||||||
|
<span className="basis-1/2 text-left w-64 red-bot">{gameState.game_stats.score[1]} <b>{team2}</b></span>
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-row text-xs">
|
||||||
|
<div className="basis-1/2 w-64 px-2">Errors: {gameState.game_stats.num_errors[0]}, Kills: {gameState.game_stats.kills[0] + gameState.game_stats.kills[2]}, Deaths: {gameState.game_stats.deaths[0] + gameState.game_stats.deaths[2]}, Time: {gameState.game_stats.team_time[0].toFixed(2)} </div>
|
||||||
|
<div className="basis-1/2 text-right w-64 px-2">Errors: {gameState.game_stats.num_errors[1]}, Kills: {gameState.game_stats.kills[1] + gameState.game_stats.kills[3]}, Deaths: {gameState.game_stats.deaths[1] + gameState.game_stats.deaths[3]}, Time: {gameState.game_stats.team_time[1].toFixed(2)} </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Maze
|
||||||
|
key={gameState.game_uuid}
|
||||||
|
game_uuid={gameState.game_uuid}
|
||||||
|
shape={gameState.shape}
|
||||||
|
walls={gameState.walls}
|
||||||
|
food={gameState.food}
|
||||||
|
bots={gameState.bots}
|
||||||
|
team_names={gameState.team_names}
|
||||||
|
say={gameState.say}
|
||||||
|
whowins={gameState.whowins}
|
||||||
|
gameover={gameState.gameover}
|
||||||
|
round={gameState.round}
|
||||||
|
turn={gameState.turn}
|
||||||
|
>
|
||||||
|
</Maze>
|
||||||
|
|
||||||
|
<div className="flex flex-row text-xs text-slate-600">
|
||||||
|
<div className="basis-1/2 w-64 px-2">ᗧ Pelita Tournament, ASPP 2024 Ηράκλειο</div>
|
||||||
|
<div className="basis-1/2 text-right w-64 px-2">Round {gameState.round ?? "-"}/{gameState.max_rounds}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function Pelita() {
|
function Pelita() {
|
||||||
|
const initialState: PelitaState = "initial";
|
||||||
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
|
||||||
const [showPre, setShowPre] = React.useState(true);
|
const [showPre, setShowPre] = React.useState(true);
|
||||||
const [showMain, setShowMain] = React.useState(true);
|
const [showMain, setShowMain] = React.useState(true);
|
||||||
const [getText, setText] = React.useState<string[]>([]);
|
const [typewriterText, setTypewriterText] = React.useState<string[]>([]);
|
||||||
|
|
||||||
const [gameUUID, setGameUUID] = React.useState("");
|
const [gameState, setGameState] = React.useState<GameState>();
|
||||||
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 bg_color = ((state) => {
|
||||||
const [team2, setTeam2] = React.useState("");
|
switch (state) {
|
||||||
|
case "initial":
|
||||||
|
case "movie":
|
||||||
|
case "intro":
|
||||||
|
return "#000"
|
||||||
|
|
||||||
const [stats, setStats] = React.useState<GameStats | null>(null);
|
case "match":
|
||||||
const [whoWins, setWhoWins] = React.useState<number | null>(null);
|
default:
|
||||||
const [gameOver, setGameOver] = React.useState<boolean>(false);
|
return "#fff";
|
||||||
|
}
|
||||||
|
|
||||||
|
})(state);
|
||||||
|
|
||||||
|
|
||||||
|
const crt = ((state) => {
|
||||||
|
switch (state) {
|
||||||
|
case "initial":
|
||||||
|
case "movie":
|
||||||
|
case "intro":
|
||||||
|
return "crt"
|
||||||
|
|
||||||
|
case "match":
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
})(state);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
anime.timeline()
|
||||||
|
.add({
|
||||||
|
targets: 'body',
|
||||||
|
background: bg_color,
|
||||||
|
easing: 'linear',
|
||||||
|
duration: 2000,
|
||||||
|
}, 3000)
|
||||||
|
}, [bg_color]);
|
||||||
|
|
||||||
const flip = () => {
|
const flip = () => {
|
||||||
//setShowMain(!showMain);
|
//setShowMain(!showMain);
|
||||||
//setShowPre(!showPre);
|
//setShowPre(!showPre);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateGameState = (gameState: { '__data__': GameState }) => {
|
const updateGameState = (gameState: GameState) => {
|
||||||
if (gameState['__data__']) {
|
dispatch("game-playing");
|
||||||
// if (gameUUID != gameState['__data__']['game_uuid']) {
|
setGameState((oldState) => {
|
||||||
setGameUUID((oldUUID) => {
|
if (oldState?.game_uuid === gameState.game_uuid) {
|
||||||
if (oldUUID != gameState['__data__']['game_uuid']) {
|
// we keep the walls array so that the effects are not re-run
|
||||||
console.log("UUID changed", gameState['__data__']['game_uuid'], oldUUID);
|
// TODO: Maybe the effect should depend on only the game_uuid having changed?
|
||||||
setShape(gameState['__data__']['shape'])
|
const newState = {
|
||||||
setWalls(gameState['__data__']['walls']);
|
...gameState,
|
||||||
}
|
"walls": oldState.walls,
|
||||||
return gameState['__data__']['game_uuid'];
|
};
|
||||||
})
|
return newState;
|
||||||
// }
|
}
|
||||||
setFood(gameState['__data__']['food']);
|
return gameState;
|
||||||
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) => {
|
const updateMessage = (msg: string) => {
|
||||||
setText(oldText => [...oldText, msg]);
|
let split_str = msg.split(/\r?\n/);
|
||||||
|
setTypewriterText(oldText => [...oldText, ...split_str]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearPage = () => {
|
const clearPage = () => {
|
||||||
setText([]);
|
dispatch("clear-page");
|
||||||
|
setTypewriterText([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = bots[0];
|
const handleClick = async () => {
|
||||||
const b = bots[2];
|
switch (state) {
|
||||||
const x = bots[1];
|
case "initial":
|
||||||
const y = bots[3];
|
dispatch("start-movie");
|
||||||
|
break;
|
||||||
|
case "movie":
|
||||||
|
dispatch("start-intro");
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function showIntro() {
|
||||||
|
return <TypewriterText text={typewriterText} lines={MAX_LINES}></TypewriterText>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inner() {
|
||||||
|
|
||||||
|
if (state == "initial") {
|
||||||
|
return <button onClick={handleClick}>Start Movie</button>;
|
||||||
|
} else if (state == "movie") {
|
||||||
|
return <video autoPlay controls onEnded={handleClick}>
|
||||||
|
<source src={"Pelita Supercut ASPP Heraklion 2024.mp4"} type="video/mp4" />
|
||||||
|
</video>;
|
||||||
|
} else if (state == "intro" || state == "match") {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="fixed top-0 left-0 z-20 w-full px-24 py-4 text-xl">
|
||||||
|
ᗧ Pelita Tournament 2024
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{state === "intro" ? showIntro() : null}
|
||||||
|
|
||||||
|
{state === "match" ?
|
||||||
|
<PelitaMain gameState={gameState}></PelitaMain>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
<ZMQReceiver url='ws://localhost:5556' sendGameState={updateGameState} sendMessage={updateMessage} sendClearPage={clearPage}></ZMQReceiver>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<main className={`min-h-screen flex-col items-center justify-between px-24 py-12 ${crt}`}>
|
||||||
<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">
|
<div className="z-10 w-full max-w-screen items-center justify-between font-mono text-sm">
|
||||||
ᗧ 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>
|
|
||||||
|
|
||||||
|
{ inner() }
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default Pelita;
|
export default Pelita;
|
||||||
|
|
66
app/pelita_msg.ts
Normal file
66
app/pelita_msg.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
|
||||||
|
import type { Tuple4 } from './typeutils';
|
||||||
|
|
||||||
|
// export interface RootMsg {
|
||||||
|
// __action__: string;
|
||||||
|
// __data__: ObserveData|null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
type RootMsg = {
|
||||||
|
__action__: "observe",
|
||||||
|
__data__: ObserveData
|
||||||
|
} | {
|
||||||
|
__action__: "SPEAK",
|
||||||
|
__data__: string
|
||||||
|
} | {
|
||||||
|
__action__: "CLEAR",
|
||||||
|
__data__: null
|
||||||
|
} | {
|
||||||
|
__action__: "INIT",
|
||||||
|
__data__: null
|
||||||
|
};
|
||||||
|
|
||||||
|
type Pos = [number, number];
|
||||||
|
|
||||||
|
export type { RootMsg };
|
||||||
|
|
||||||
|
export interface ObserveData {
|
||||||
|
game_uuid: string;
|
||||||
|
walls: Pos[];
|
||||||
|
shape: Pos;
|
||||||
|
food: Pos[];
|
||||||
|
food_age: (number[] | number)[][];
|
||||||
|
turn: number;
|
||||||
|
round: number;
|
||||||
|
gameover: boolean;
|
||||||
|
whowins?: any;
|
||||||
|
bots: Tuple4<Pos>;
|
||||||
|
score: [number, number];
|
||||||
|
fatal_errors: any[][];
|
||||||
|
errors: null[];
|
||||||
|
max_rounds: number;
|
||||||
|
timeout: number;
|
||||||
|
noise_radius: number;
|
||||||
|
sight_distance: number;
|
||||||
|
max_food_age: number;
|
||||||
|
shadow_distance: number;
|
||||||
|
layout_name: string;
|
||||||
|
team_names: [string, string];
|
||||||
|
team_infos: null[];
|
||||||
|
team_time: [number, number];
|
||||||
|
deaths: Tuple4<number>;
|
||||||
|
kills: Tuple4<number>;
|
||||||
|
bot_was_killed: boolean[];
|
||||||
|
noisy_positions: (number[] | null)[];
|
||||||
|
requested_moves: RequestedMove[];
|
||||||
|
say: Tuple4<string>;
|
||||||
|
timeout_length: number;
|
||||||
|
error_limit: number;
|
||||||
|
num_errors: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestedMove {
|
||||||
|
previous_position: number[];
|
||||||
|
requested_position: number[];
|
||||||
|
success: boolean;
|
||||||
|
}
|
6
app/typeutils.ts
Normal file
6
app/typeutils.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
type TupleOf<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;
|
||||||
|
type _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>;
|
||||||
|
|
||||||
|
type Tuple4<T> = TupleOf<T, 4>;
|
||||||
|
|
||||||
|
export type { Tuple4 }
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
.text_animation {
|
.text_animation {
|
||||||
display: none;
|
/* display: none; */
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
|
@ -9,16 +9,20 @@
|
||||||
|
|
||||||
.text_animation .letter {
|
.text_animation .letter {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
opacity: 0;
|
/* opacity: 0; */
|
||||||
|
/* color: white; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.text_animation .cursor {
|
.text_animation .cursor {
|
||||||
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
/* top: 0; */
|
||||||
bottom: 0;
|
/* bottom: 0; */
|
||||||
width: 0.55em;
|
width: 0.55em;
|
||||||
/* height: 1em; */
|
height: 1em;
|
||||||
/* background: #f5f5f5; */
|
/* background: #f5f5f5; */
|
||||||
background: black;
|
/* color: white; */
|
||||||
|
/* background: #ffffff; */
|
||||||
|
/* background: green; */
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +1,141 @@
|
||||||
import React, { ReactNode, useEffect, useRef } from "react";
|
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import anime from "animejs/lib/anime.es.js";
|
import anime from "animejs/lib/anime.es.js";
|
||||||
import styles from "./typewritertext.module.css";
|
import styles from "./typewritertext.module.css";
|
||||||
|
|
||||||
function TypewriterText({ text }: { text: string }) {
|
|
||||||
// IMPORTANT: This code chokes on an empty string. Logic needs to be refactored
|
function Cursor() {
|
||||||
const lettersHtml: ReactNode[] = (text ? text : " ").split("").map((c, i) => {
|
const self = useRef<HTMLDivElement>(null);
|
||||||
return (
|
|
||||||
<span className={`letter ${styles.letter}`} key={`letter,${i}`}>
|
useEffect(() => {
|
||||||
{c == " " ? "\u00A0" : c}
|
const blink = anime({
|
||||||
</span>
|
targets: self.current,
|
||||||
);
|
loop: true,
|
||||||
|
duration: 750,
|
||||||
|
opacity: [{ value: [1, 1] }, { value: [0, 0] }],
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (<span ref={self} className={`cursor ${styles.cursor}`}>█</span>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FadingLetter({ char }: { char: string }) {
|
||||||
|
const letter = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
anime({
|
||||||
|
targets: letter.current,
|
||||||
|
loop: false,
|
||||||
|
//opacity: [{ value: [1, 1] }, { value: [0, 0] }],
|
||||||
|
color: ['#ffffff', '#ffffff', '#ffffff', '#ffffff', '#eeeeee'],
|
||||||
|
duration: 100,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (<span ref={letter} className={`letter ${styles.letter}`}>
|
||||||
|
{char == " " ? "\u00A0" : char}
|
||||||
|
</span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TypewriterLine( { text, cursor, lineFinished }: { text: string, cursor: boolean, lineFinished: () => any }) {
|
||||||
|
const TYPE_AFTER_MS = 1_000;
|
||||||
|
const JUMP_AFTER_MS = 80;
|
||||||
|
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [hasCursor, setHasCursor] = useState(true);
|
||||||
|
|
||||||
|
const current = text.slice(0, index) || "";
|
||||||
|
const lettersHtml = (!current) ? (<br/>) : current.split("").map((c, i) => {
|
||||||
|
return (<FadingLetter key={`letter,${i}`} char={c}></FadingLetter>);
|
||||||
});
|
});
|
||||||
|
|
||||||
const lineHtml = (
|
const lineHtml = (
|
||||||
<>
|
<>
|
||||||
<div className="letters">{lettersHtml}</div>
|
<p className="letters">{lettersHtml}{
|
||||||
<span className={`cursor ${styles.cursor}`}></span>
|
(hasCursor || cursor) ? (<Cursor></Cursor>) : null
|
||||||
|
}</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
const text_animation_ref = useRef<HTMLDivElement>(null);
|
const text_animation_ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = text_animation_ref.current!;
|
const interval = setInterval(() => {
|
||||||
el.style.display = "block";
|
setIndex(oldIndex => {
|
||||||
const letters = Array.from(el.querySelectorAll(".letter")) as HTMLElement[];
|
if (oldIndex < text.length) {
|
||||||
const TYPE_AFTER_MS = 1_000;
|
return oldIndex + 1;
|
||||||
const JUMP_AFTER_MS = 80;
|
} else {
|
||||||
|
clearInterval(interval);
|
||||||
|
setHasCursor(false);
|
||||||
|
//lineFinished();
|
||||||
|
// const cursorRemove = setInterval(() => { setHasCursor(false); return () => clearInterval(cursorRemove); }, 500);
|
||||||
|
return oldIndex;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, JUMP_AFTER_MS);
|
||||||
|
|
||||||
const blink = anime({
|
//Clearing the interval
|
||||||
targets: el.querySelectorAll(".cursor"),
|
return () => clearInterval(interval);
|
||||||
loop: true,
|
}, [index]);
|
||||||
duration: 750,
|
|
||||||
opacity: [{ value: [1, 1] }, { value: [0, 0] }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const tl = anime
|
if (current === "")
|
||||||
.timeline()
|
return (<div><br/></div>);
|
||||||
.add(
|
|
||||||
{
|
return (
|
||||||
targets: el.querySelectorAll(".cursor"),
|
<div ref={text_animation_ref} className={styles.text_animation}>
|
||||||
translateX: letters.map((letter, i) => ({
|
{lineHtml}
|
||||||
value: letter.offsetLeft + letter.offsetWidth,
|
</div>
|
||||||
duration: 1,
|
);
|
||||||
delay: i === 0 ? 0 : JUMP_AFTER_MS,
|
}
|
||||||
})),
|
|
||||||
},
|
function TypewriterText({ text, lines }: { text: string[], lines: number }) {
|
||||||
TYPE_AFTER_MS
|
// IMPORTANT: This code chokes on an empty string. Logic needs to be refactored
|
||||||
)
|
const len = text.length;
|
||||||
.add(
|
return text.map((v, idx) => <TypewriterLine key={idx} text={v} cursor={idx + 1 === len} lineFinished={() => {}}></TypewriterLine>).slice(-lines);
|
||||||
{
|
/*
|
||||||
targets: el.querySelectorAll(".letter"),
|
const TYPE_AFTER_MS = 1_000;
|
||||||
opacity: [0, 1],
|
const JUMP_AFTER_MS = 80;
|
||||||
duration: 1,
|
|
||||||
delay: anime.stagger(JUMP_AFTER_MS),
|
const [index, setIndex] = useState(0);
|
||||||
changeBegin: () => {
|
const [hasCursor, setHasCursor] = useState(true);
|
||||||
blink.restart();
|
|
||||||
blink.pause();
|
const current = text.slice(0, index) || "";
|
||||||
},
|
const lettersHtml: ReactNode[] = (!current) ? (<br/>) : current.split("").map((c, i) => {
|
||||||
changeComplete: () => {
|
return (<FadingLetter key={`letter,${i}`} char={c}></FadingLetter>);
|
||||||
blink.restart();
|
});
|
||||||
},
|
|
||||||
},
|
const lineHtml = (
|
||||||
TYPE_AFTER_MS
|
<>
|
||||||
);
|
<div className="letters">{lettersHtml}{
|
||||||
}, []);
|
hasCursor ? (<Cursor></Cursor>) : null
|
||||||
|
}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const text_animation_ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//Implementing the setInterval method
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setIndex(oldIndex => {
|
||||||
|
if (oldIndex < text.length) {
|
||||||
|
return oldIndex + 1;
|
||||||
|
} else {
|
||||||
|
clearInterval(interval);
|
||||||
|
// const cursorRemove = setInterval(() => { setHasCursor(false); return () => clearInterval(cursorRemove); }, 500);
|
||||||
|
return oldIndex;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
//Clearing the interval
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [index]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={text_animation_ref} className={styles.text_animation}>
|
<div ref={text_animation_ref} className={styles.text_animation}>
|
||||||
<h1>{lineHtml}</h1>
|
<h1>{lineHtml}</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TypewriterText;
|
export default TypewriterText;
|
||||||
|
|
|
@ -1,21 +1,69 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import type { Tuple4 } from './typeutils';
|
||||||
|
import { ObserveData } from './pelita_msg';
|
||||||
|
import type { RootMsg } from './pelita_msg';
|
||||||
import * as zmq from 'jszmq';
|
import * as zmq from 'jszmq';
|
||||||
|
|
||||||
|
|
||||||
|
interface GameStats {
|
||||||
|
score: [number, number];
|
||||||
|
num_errors: [number, number];
|
||||||
|
kills: [number, number, number, number];
|
||||||
|
deaths: [number, number, number, number];
|
||||||
|
team_time: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
interface GameState {
|
interface GameState {
|
||||||
game_uuid: string;
|
game_uuid: string;
|
||||||
shape: [number, number];
|
shape: [number, number];
|
||||||
walls: [number, number][];
|
walls: [number, number][];
|
||||||
food: [number, number][];
|
food: [number, number][];
|
||||||
bots: [number, number][];
|
bots: Tuple4<[number, number]>;
|
||||||
team_names: [string, string];
|
team_names: [string, string];
|
||||||
|
game_stats: GameStats;
|
||||||
|
whowins: number;
|
||||||
|
gameover: boolean;
|
||||||
|
say: Tuple4<string>;
|
||||||
|
round: number;
|
||||||
|
max_rounds: number;
|
||||||
|
turn: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { GameState };
|
export type { GameState };
|
||||||
|
|
||||||
|
function conv_game_state(gs: ObserveData): GameState {
|
||||||
|
// const bot_directions = gs.bots.map((pos, idx) => {
|
||||||
|
//
|
||||||
|
// });
|
||||||
|
|
||||||
|
return {
|
||||||
|
"game_uuid": gs.game_uuid,
|
||||||
|
"shape": gs.shape,
|
||||||
|
"walls": gs.walls,
|
||||||
|
"food": gs.food,
|
||||||
|
"bots": gs.bots,
|
||||||
|
// "bot_directions": bot_directions,
|
||||||
|
"say": gs.say,
|
||||||
|
"turn": gs.turn,
|
||||||
|
"round": gs.round,
|
||||||
|
"max_rounds": gs.max_rounds,
|
||||||
|
"team_names": gs.team_names,
|
||||||
|
"game_stats": {
|
||||||
|
"score": gs.score,
|
||||||
|
"num_errors": gs.num_errors,
|
||||||
|
"kills": gs.kills,
|
||||||
|
"deaths": gs.deaths,
|
||||||
|
"team_time": gs.team_time,
|
||||||
|
},
|
||||||
|
"whowins": gs.whowins,
|
||||||
|
"gameover": gs.gameover
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const ZMQReceiver = ({ url, sendGameState, sendMessage, sendClearPage }: { url: string,
|
const ZMQReceiver = ({ url, sendGameState, sendMessage, sendClearPage }: { url: string,
|
||||||
sendGameState: (gs: {"__data__": GameState}) => any,
|
sendGameState: (gs: GameState) => any,
|
||||||
sendMessage: (msg: string) => any,
|
sendMessage: (msg: string) => any,
|
||||||
sendClearPage: () => any
|
sendClearPage: () => any
|
||||||
}
|
}
|
||||||
|
@ -32,14 +80,17 @@ const ZMQReceiver = ({ url, sendGameState, sendMessage, sendClearPage }: { url:
|
||||||
|
|
||||||
socket.on('message', (message) => {
|
socket.on('message', (message) => {
|
||||||
setMessages((prevMessages) => [/*...prevMessages, */message.toString()]);
|
setMessages((prevMessages) => [/*...prevMessages, */message.toString()]);
|
||||||
let parsed = JSON.parse(message);
|
let parsed = JSON.parse(message) as RootMsg;
|
||||||
console.log(parsed);
|
console.log(parsed);
|
||||||
if (parsed['__action__'] && parsed['__action__'] === 'SPEAK') {
|
if (parsed.__action__ === 'SPEAK') {
|
||||||
sendMessage(parsed['__data__']);
|
sendMessage(parsed['__data__'].replaceAll(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''));
|
||||||
} else if (parsed['__action__'] && parsed['__action__'] === 'CLEAR') {
|
} else if (parsed.__action__ === 'CLEAR') {
|
||||||
|
sendClearPage();
|
||||||
|
} else if (parsed.__action__ === 'observe') {
|
||||||
|
let conv = conv_game_state(parsed.__data__);
|
||||||
|
sendGameState(conv);
|
||||||
|
} else if (parsed.__action__ === 'INIT') {
|
||||||
sendClearPage();
|
sendClearPage();
|
||||||
} else {
|
|
||||||
sendGameState(parsed);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,15 +100,15 @@ const ZMQReceiver = ({ url, sendGameState, sendMessage, sendClearPage }: { url:
|
||||||
};
|
};
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
return (
|
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">
|
// <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>
|
// <h2 className='text-xs text-slate-300'>ZeroMQ Messages:</h2>
|
||||||
<ul>
|
// <ul>
|
||||||
{messages.map((message, index) => (
|
// {messages.map((message, index) => (
|
||||||
<li key={index} className='text-xs text-slate-300' style={{fontSize: 0.45 + 'rem', lineHeight: 0.5 + 'rem'}}>{message}</li>
|
// <li key={index} className='text-xs text-slate-300' style={{fontSize: 0.45 + 'rem', lineHeight: 0.5 + 'rem'}}>{message}</li>
|
||||||
))}
|
// ))}
|
||||||
</ul>
|
// </ul>
|
||||||
</footer>
|
// </footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
821
package-lock.json
generated
821
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -4,12 +4,16 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
"dev-remote": "next dev -H 0.0.0.0 -p 3000",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vercel/nft": "^0.27.3",
|
||||||
"animejs": "^3.2.2",
|
"animejs": "^3.2.2",
|
||||||
|
"framer-motion": "^11.3.17",
|
||||||
|
"http-proxy-middleware": "^3.0.0",
|
||||||
"jszmq": "^0.1.2",
|
"jszmq": "^0.1.2",
|
||||||
"next": "^14.2.5",
|
"next": "^14.2.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
|
Loading…
Reference in a new issue