ENH: Refactoring. Making the animations smoother

This commit is contained in:
Rike-Benjamin Schuppner 2024-09-04 14:59:54 +02:00
parent 2eef4e9c05
commit bfc63050cf
8 changed files with 322 additions and 319 deletions

View file

@ -5,8 +5,8 @@ import "./globals.css";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "Pelita Tournament Heraklion 2024",
description: "Generated by create next app", description: "ASPP2024",
}; };
export default function RootLayout({ export default function RootLayout({

View file

@ -2,6 +2,7 @@
import React, { import React, {
useEffect, useEffect,
useId,
useMemo, useMemo,
useRef, useRef,
useState useState
@ -261,7 +262,7 @@ function Food({ position, color }: { position: Pos, color: string }) {
opacity={1} opacity={1}
className={color} className={color}
// transition={{ duration: 1 }} // transition={{ duration: 1 }}
// initial={{ opacity: 0 }} initial={{ opacity: 1, r: cellSize / 5 }}
// animate={{ opacity: 1 }} // animate={{ opacity: 1 }}
exit={{ opacity: 0, r: cellSize }} exit={{ opacity: 0, r: cellSize }}
/> />
@ -299,13 +300,6 @@ function Pacman({ direction, mouthAngle, color }: { direction: number, mouthAngl
return ( return (
<g transform={ `rotate(${direction})` } <g transform={ `rotate(${direction})` }
// style={{
// transform: `rotate(${direction})`
// }}
// animate={{
// rotate: direction
// }}
// transition={{ type: "spring", stiffness: 100 }}
className={color}> className={color}>
<path <path
d={pacmanPath(mouthAngle)} d={pacmanPath(mouthAngle)}
@ -321,68 +315,8 @@ function Pacman({ direction, mouthAngle, color }: { direction: number, mouthAngl
</g>) </g>)
} }
function Bot({ position, color, say, width, turnsAgo }: { position: Pos, color: string, say: string, width: number, turnsAgo: number }) { function Ghost({ direction, color }: { direction: number, color: string }) {
const leftSide = position[0] < width / 2; return (<g
const inHomezone = () => {
switch (color) {
case "blue":
return leftSide;
case "red":
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(() => {
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 (
<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" id="ghost"
className={`${color} ghost`} className={`${color} ghost`}
> >
@ -410,10 +344,76 @@ 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
fill="black" fill="black"
> >
</path> </path>
</g> </g>);
) : ( }
<Pacman direction={direction} mouthAngle={mouthAngle} color={`${color} pacman`}></Pacman>
) function Bot({ position, color, say, width, turnsAgo, fadeIn }: { position: Pos, color: string, say: string, width: number, turnsAgo: number, fadeIn: boolean }) {
const leftSide = position[0] < width / 2;
const inHomezone = () => {
switch (color) {
case "blue":
return leftSide;
case "red":
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);
}
}, [position, oldPosition]);
useEffect(() => {
if (fadeIn)
anime.timeline()
.add({
targets: '.bot .blue',
opacity: [0, 1],
easing: 'linear',
duration: 2000,
}, 3500)
.add({
targets: '.bot .red',
opacity: [0, 1],
easing: 'linear',
duration: 2000,
}, 3500);
}, [fadeIn]);
return (
<motion.g
transform={ `translate(${(position[0] + 0.5) * cellSize} ${(position[1] + 0.5) * cellSize}) scale(${cellSize / 16})` }
className="bot"
animate={{
x: (position[0] + 0.5) * cellSize,
y: (position[1] + 0.5) * cellSize,
scale: cellSize / 16
}}
initial={{
x: (position[0] + 0.5) * cellSize,
y: (position[1] + 0.5) * cellSize,
scale: cellSize / 16
}}
transition={{ duration: 0.1 }}
>
{
inHomezone()
? (<Ghost direction={direction} color={`${color} ghost`}></Ghost>)
: (<Pacman direction={direction} mouthAngle={mouthAngle} color={`${color} pacman`}></Pacman>)
} }
<text y="-10" className="sayBg">{say}</text> <text y="-10" className="sayBg">{say}</text>
<text y="-10" className="say">{say}</text> <text y="-10" className="say">{say}</text>
@ -426,7 +426,7 @@ function Walls({ shape, walls }: { shape: Pos, walls: Pos[] }) {
const [width, height] = shape; const [width, height] = shape;
return ( return (
<g id="maze"> <g className="maze">
<line x1={(width) * cellSize / 2} y1={0.3 * cellSize} <line x1={(width) * cellSize / 2} y1={0.3 * cellSize}
x2={width * cellSize / 2} y2={(height - 0.3) * cellSize} className="middleLine blackLine" /> x2={width * cellSize / 2} y2={(height - 0.3) * cellSize} className="middleLine blackLine" />
<line x1={(width - 0.1) * cellSize / 2} y1={0.3 * cellSize} <line x1={(width - 0.1) * cellSize / 2} y1={0.3 * cellSize}
@ -465,7 +465,7 @@ function Walls({ shape, walls }: { shape: Pos, walls: Pos[] }) {
} }
function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, gameover, round, turn }: function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, gameover, round, turn, animate }:
{ {
game_uuid: string, game_uuid: string,
shape: Pos, shape: Pos,
@ -478,17 +478,22 @@ function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, g
gameover: boolean, gameover: boolean,
round: number, round: number,
turn: number, turn: number,
animate: boolean
} }
) { ) {
const [width, height] = shape; const [width, height] = shape;
const [a, x, b, y] = bots; const [a, x, b, y] = bots;
const [sayA, sayX, sayB, sayY] = say; const [sayA, sayX, sayB, sayY] = say;
const mazeBoxRef = useRef<HTMLDivElement>(null);
// used so that we can revert the animation
const [hasWonScreen, setHasWonScreen] = useState(false);
useEffect(() => { useEffect(() => {
if (game_uuid) { if (game_uuid && animate) {
let pathAnimation = anime.timeline() let pathAnimation = anime.timeline()
.add({ .add({
targets: '#mazebox #maze path', targets: mazeBoxRef.current?.querySelectorAll('.maze path'),
strokeDashoffset: [anime.setDashoffset, 0], strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInCubic', easing: 'easeInCubic',
duration: 2000, duration: 2000,
@ -497,59 +502,70 @@ function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, g
loop: false loop: false
}) })
.add({ .add({
targets: '#mazebox #maze path', targets: mazeBoxRef.current?.querySelectorAll('.maze path'),
//fill: ['rgb(214, 219, 220)', "#faa"], // ffa //fill: ['rgb(214, 219, 220)', "#faa"], // ffa
fillOpacity: [0, 0.7], // ffa fillOpacity: [0, 0.7], // ffa
easing: 'linear', easing: 'linear',
duration: 2000 duration: 2000
}, 2000) }, 2000)
.add({ .add({
targets: '#mazebox #maze path', targets: mazeBoxRef.current?.querySelectorAll('.maze path'),
strokeWidth: 0, strokeWidth: 0,
easing: 'linear', easing: 'linear',
duration: 2000 duration: 2000
}, 4000) }, 4000)
.add({ .add({
targets: '#mazebox .foodblue', targets: mazeBoxRef.current?.querySelectorAll('.foodblue'),
opacity: [0, 1], opacity: [0, 1],
easing: 'linear', easing: 'linear',
duration: 2000, duration: 2000,
}, 3000) }, 3000)
.add({ .add({
targets: '#mazebox .foodred', targets: mazeBoxRef.current?.querySelectorAll('.foodred'),
opacity: [0, 1], opacity: [0, 1],
easing: 'linear', easing: 'linear',
duration: 2000, duration: 2000,
}, 3000) }, 3000)
.add({ .add({
targets: '#mazebox .blue', targets: mazeBoxRef.current?.querySelectorAll('.blue'),
opacity: [0, 1], opacity: [0, 1],
easing: 'linear', easing: 'linear',
duration: 2000, duration: 2000,
}, 3500) }, 3500)
.add({ .add({
targets: '#mazebox .red', targets: mazeBoxRef.current?.querySelectorAll('.red'),
opacity: [0, 1], opacity: [0, 1],
easing: 'linear', easing: 'linear',
duration: 2000, duration: 2000,
}, 3500) }, 3500)
.add({ .add({
targets: '#mazebox .middleLine', targets: mazeBoxRef.current?.querySelectorAll('.middleLine'),
opacity: [0, 1], opacity: [0, 1],
easing: 'linear', easing: 'linear',
duration: 2000, duration: 2000,
}, 3500); }, 3500);
} }
}, [walls, game_uuid]); }, [walls, game_uuid, animate, mazeBoxRef]);
useEffect(() => { useEffect(() => {
console.log(gameover, whowins); if (!gameover && hasWonScreen) {
let pathAnimation = anime.timeline()
.add({
targets: mazeBoxRef.current?.querySelectorAll('.maze path'),
fill: ["#000"],
easing: 'linear',
duration: 200
});
};
if (gameover) {
setHasWonScreen(true);
}
if (gameover && whowins === 0) { if (gameover && whowins === 0) {
let pathAnimation = anime.timeline() let pathAnimation = anime.timeline()
.add({ .add({
targets: '#mazebox #maze path', targets: mazeBoxRef.current?.querySelectorAll('.maze path'),
fill: ["#faa", "rgb(94, 158, 217)"], fill: ["#000", "rgb(94, 158, 217)"],
easing: 'linear', easing: 'linear',
duration: 200 duration: 200
}); });
@ -557,8 +573,8 @@ function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, g
if (gameover && whowins === 1) { if (gameover && whowins === 1) {
let pathAnimation = anime.timeline() let pathAnimation = anime.timeline()
.add({ .add({
targets: '#mazebox #maze path', targets: mazeBoxRef.current?.querySelectorAll('.maze path'),
fill: ["#faa", "rgb(235, 90, 90)"], fill: ["#000", "rgb(235, 90, 90)"],
easing: 'linear', easing: 'linear',
duration: 200 duration: 200
}); });
@ -566,17 +582,17 @@ function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, g
if (gameover && whowins === 2) { if (gameover && whowins === 2) {
let pathAnimation = anime.timeline() let pathAnimation = anime.timeline()
.add({ .add({
targets: '#mazebox #maze path', targets: mazeBoxRef.current?.querySelectorAll('.maze path'),
fill: ["#faa", "#fff"], fill: ["#000", "#ffa"],
easing: 'linear', easing: 'linear',
duration: 200 duration: 200
}); });
}; };
}, [gameover, whowins]); }, [gameover, whowins, mazeBoxRef, hasWonScreen]);
return ( return (
<div id="mazebox" className="object-fill"> <div ref={mazeBoxRef} className="mazebox object-fill">
<svg <svg
// width={width * cellSize} // width={width * cellSize}
// height={height * cellSize} // height={height * cellSize}
@ -653,10 +669,10 @@ function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, g
))} ))}
</AnimatePresence> </AnimatePresence>
<Bot position={a} key="botA" color="blue" say={sayA} width={width} turnsAgo={turn}></Bot> <Bot position={a} key="botA" color="blue" say={sayA} width={width} fadeIn={animate} turnsAgo={turn}></Bot>
<Bot position={x} key="botX" color="red" say={sayX} width={width} turnsAgo={(turn + 3) % 4}></Bot> <Bot position={x} key="botX" color="red" say={sayX} width={width} fadeIn={animate} turnsAgo={(turn + 3) % 4}></Bot>
<Bot position={b} key="botB" color="blue" say={sayB} width={width} turnsAgo={(turn + 2) % 4}></Bot> <Bot position={b} key="botB" color="blue" say={sayB} width={width} fadeIn={animate} turnsAgo={(turn + 2) % 4}></Bot>
<Bot position={y} key="botY" color="red" say={sayY} width={width} turnsAgo={(turn + 1) % 4}></Bot> <Bot position={y} key="botY" color="red" say={sayY} width={width} fadeIn={animate} turnsAgo={(turn + 1) % 4}></Bot>
{ {
gameover ? (<> gameover ? (<>

View file

@ -1,7 +1,7 @@
import Pelita from "./pelita"; import PelitaTournament from "./tournament";
export default function Home() { export default function Home() {
return (<> return (<>
<Pelita></Pelita> <PelitaTournament></PelitaTournament>
</>); </>);
} }

View file

@ -1,47 +1,12 @@
"use client" "use client"
import React, {
Reducer,
useEffect,
useReducer
} from "react";
import Maze from "./maze"; import Maze from "./maze";
import ZMQReceiver from "./zmqreceiver"; import { GameState } from "./pelita_msg";
import TypewriterText from "./typewritertext";
import { GameState } from "./zmqreceiver";
import anime from "animejs";
type PelitaState = "initial" | "movie" | "intro" | "match" | "faulted"; function Pelita({ gameState, footer, animate }: { gameState: GameState, footer: string, animate: boolean }) {
type PelitaEvent = "start-movie" | "start-intro" | "game-playing" | "clear-page" | "fail";
const MAX_LINES = 20;
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; const [team1, team2] = gameState.team_names;
return <div id="main"> return <div className="pelita">
<h2 className="flex flex-row text-lg p-2"> <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/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 px-2">:</span>
@ -65,153 +30,15 @@ function PelitaMain({ gameState }: { gameState: GameState }) {
gameover={gameState.gameover} gameover={gameState.gameover}
round={gameState.round} round={gameState.round}
turn={gameState.turn} turn={gameState.turn}
animate={animate}
> >
</Maze> </Maze>
<div className="flex flex-row text-xs text-slate-600"> <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 w-64 px-2">{ footer }</div>
<div className="basis-1/2 text-right w-64 px-2">Round {gameState.round ?? "-"}/{gameState.max_rounds}</div> <div className="basis-1/2 text-right w-64 px-2">Round {gameState.round ?? "-"}/{gameState.max_rounds}</div>
</div> </div>
</div> </div>
} }
function Pelita() {
const initialState: PelitaState = "initial";
const [state, dispatch] = useReducer(reducer, initialState);
const [showPre, setShowPre] = React.useState(true);
const [showMain, setShowMain] = React.useState(true);
const [typewriterText, setTypewriterText] = React.useState<string[]>([]);
const [gameState, setGameState] = React.useState<GameState>();
const bg_color = ((state) => {
switch (state) {
case "initial":
case "movie":
case "intro":
return "#000"
case "match":
default:
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 = () => {
//setShowMain(!showMain);
//setShowPre(!showPre);
};
const updateGameState = (gameState: GameState) => {
dispatch("game-playing");
setGameState((oldState) => {
if (oldState?.game_uuid === gameState.game_uuid) {
// we keep the walls array so that the effects are not re-run
// TODO: Maybe the effect should depend on only the game_uuid having changed?
const newState = {
...gameState,
"walls": oldState.walls,
};
return newState;
}
return gameState;
});
}
const updateMessage = (msg: string) => {
let split_str = msg.split(/\r?\n/);
setTypewriterText(oldText => [...oldText, ...split_str]);
}
const clearPage = () => {
dispatch("clear-page");
setTypewriterText([]);
}
const handleClick = async () => {
switch (state) {
case "initial":
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" && gameState ?
<PelitaMain gameState={gameState}></PelitaMain>
: null
}
<ZMQReceiver url='ws://localhost:5556' sendGameState={updateGameState} sendMessage={updateMessage} sendClearPage={clearPage}></ZMQReceiver>
</div>
);
}
};
return (
<main className={`min-h-screen flex-col items-center justify-between px-24 py-12 ${crt}`}>
<div className="z-10 w-full max-w-screen items-center justify-between font-mono text-sm">
{ inner() }
</div>
</main>
);
};
export default Pelita; export default Pelita;

View file

@ -20,10 +20,11 @@ type RootMsg = {
__data__: null __data__: null
}; };
type Pos = [number, number]; export type Pos = [number, number];
export type { RootMsg }; export type { RootMsg };
export type ObserveDataL = ObserveData[];
export interface ObserveData { export interface ObserveData {
game_uuid: string; game_uuid: string;
walls: Pos[]; walls: Pos[];
@ -64,3 +65,57 @@ export interface RequestedMove {
requested_position: number[]; requested_position: number[];
success: boolean; success: boolean;
} }
export interface GameStats {
score: [number, number];
num_errors: [number, number];
kills: [number, number, number, number];
deaths: [number, number, number, number];
team_time: [number, number];
}
export interface GameState {
game_uuid: string;
shape: [number, number];
walls: [number, number][];
food: [number, number][];
bots: Tuple4<[number, number]>;
team_names: [string, string];
game_stats: GameStats;
whowins: number;
gameover: boolean;
say: Tuple4<string>;
round: number;
max_rounds: number;
turn: number;
}
export 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
};
}

160
app/tournament.tsx Normal file
View file

@ -0,0 +1,160 @@
"use client"
import React, {
Reducer,
useEffect,
useReducer
} from "react";
import ZMQReceiver from "./zmqreceiver";
import TypewriterText from "./typewritertext";
import { GameState } from "./pelita_msg";
import Pelita from "./pelita";
import anime from "animejs";
type PelitaState = "initial" | "movie" | "intro" | "match" | "faulted";
type PelitaEvent = "start-movie" | "start-intro" | "game-playing" | "clear-page" | "fail";
const MAX_LINES = 20;
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 PelitaTournament() {
const initialState: PelitaState = "initial";
const [state, dispatch] = useReducer(reducer, initialState);
const [typewriterText, setTypewriterText] = React.useState<string[]>([]);
const [gameState, setGameState] = React.useState<GameState>();
const bg_color = ((state) => {
switch (state) {
case "initial":
case "movie":
case "intro":
return "#000"
case "match":
default:
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 updateGameState = (gameState: GameState) => {
dispatch("game-playing");
setGameState((oldState) => {
if (oldState?.game_uuid === gameState.game_uuid) {
// we keep the walls array so that the effects are not re-run
// TODO: Maybe the effect should depend on only the game_uuid having changed?
const newState = {
...gameState,
"walls": oldState.walls,
};
return newState;
}
return gameState;
});
}
const updateMessage = (msg: string) => {
let split_str = msg.split(/\r?\n/);
setTypewriterText(oldText => [...oldText, ...split_str]);
}
const clearPage = () => {
dispatch("clear-page");
setTypewriterText([]);
}
const handleClick = async () => {
switch (state) {
case "initial":
dispatch("start-movie");
break;
case "movie":
dispatch("start-intro");
break
default:
break;
}
};
function showIntro() {
return <TypewriterText text={typewriterText} lines={MAX_LINES}></TypewriterText>;
};
return (
<main className={`min-h-screen flex-col items-center justify-between px-24 py-12 ${crt}`}>
<div className="z-10 w-full max-w-screen items-center justify-between font-mono text-sm">
{state == "initial" && <button onClick={handleClick}>Start Movie</button>}
{state == "movie" && <video autoPlay controls onEnded={handleClick}>
<source src={"Pelita Supercut ASPP Heraklion 2024.mp4"} type="video/mp4" />
</video>
}
{state === "intro" && showIntro()}
{state == "match" &&
<div>
<h1 className="fixed top-0 left-0 z-20 w-full px-24 py-4 text-xl">
Pelita Tournament 2024
</h1>
{ gameState && <Pelita gameState={gameState} footer="ᗧ Pelita Tournament, ASPP 2024 Ηράκλειο" animate={true}></Pelita> }
</div>
}
<ZMQReceiver url='ws://localhost:5556' sendGameState={updateGameState} sendMessage={updateMessage} sendClearPage={clearPage}></ZMQReceiver>
</div>
</main>
);
};
export default PelitaTournament;

View file

@ -75,7 +75,7 @@ function TypewriterLine( { text, cursor, lineFinished }: { text: string, cursor:
//Clearing the interval //Clearing the interval
return () => clearInterval(interval); return () => clearInterval(interval);
}, [index]); }, [index, text.length]);
if (current === "") if (current === "")
return (<div><br/></div>); return (<div><br/></div>);

View file

@ -1,67 +1,11 @@
'use client' 'use client'
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import type { Tuple4 } from './typeutils'; import { conv_game_state } from './pelita_msg';
import { ObserveData } from './pelita_msg'; import type { RootMsg, GameState } 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 {
game_uuid: string;
shape: [number, number];
walls: [number, number][];
food: [number, number][];
bots: Tuple4<[number, number]>;
team_names: [string, string];
game_stats: GameStats;
whowins: number;
gameover: boolean;
say: Tuple4<string>;
round: number;
max_rounds: number;
turn: number;
}
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: GameState) => any, sendGameState: (gs: GameState) => any,
sendMessage: (msg: string) => any, sendMessage: (msg: string) => any,
@ -83,6 +27,7 @@ const ZMQReceiver = ({ url, sendGameState, sendMessage, sendClearPage }: { url:
let parsed = JSON.parse(message) as RootMsg; let parsed = JSON.parse(message) as RootMsg;
console.log(parsed); console.log(parsed);
if (parsed.__action__ === 'SPEAK') { if (parsed.__action__ === 'SPEAK') {
// Replacing all ANSI code here
sendMessage(parsed['__data__'].replaceAll(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '')); sendMessage(parsed['__data__'].replaceAll(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''));
} else if (parsed.__action__ === 'CLEAR') { } else if (parsed.__action__ === 'CLEAR') {
sendClearPage(); sendClearPage();
@ -98,7 +43,7 @@ const ZMQReceiver = ({ url, sendGameState, sendMessage, sendClearPage }: { url:
socket.unsubscribe(''); socket.unsubscribe('');
socket.close(); socket.close();
}; };
}, [url]); }, [url, sendGameState, sendClearPage, sendMessage]);
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">