diff --git a/app/layout.tsx b/app/layout.tsx index 3314e47..47f4e82 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,8 +5,8 @@ import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Pelita Tournament Heraklion 2024", + description: "ASPP2024", }; export default function RootLayout({ diff --git a/app/maze.tsx b/app/maze.tsx index e531185..957139e 100644 --- a/app/maze.tsx +++ b/app/maze.tsx @@ -2,6 +2,7 @@ import React, { useEffect, + useId, useMemo, useRef, useState @@ -261,7 +262,7 @@ function Food({ position, color }: { position: Pos, color: string }) { opacity={1} className={color} // transition={{ duration: 1 }} - // initial={{ opacity: 0 }} + initial={{ opacity: 1, r: cellSize / 5 }} // animate={{ opacity: 1 }} exit={{ opacity: 0, r: cellSize }} /> @@ -299,13 +300,6 @@ function Pacman({ direction, mouthAngle, color }: { direction: number, mouthAngl return ( ) } -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 }) { + return ( + +{/* 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 */} + + + + + +); +} + +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) { @@ -349,71 +375,45 @@ function Bot({ position, color, say, width, turnsAgo }: { position: Pos, color: else if (dx > 0) setDirection(0); else if (dy > 0) setDirection(90); } - }, [oldPosition]); + }, [position, oldPosition]); useEffect(() => { + if (fadeIn) anime.timeline() .add({ - targets: '#mazebox .blue', + targets: '.bot .blue', opacity: [0, 1], easing: 'linear', duration: 2000, }, 3500) .add({ - targets: '#mazebox .red', + targets: '.bot .red', opacity: [0, 1], easing: 'linear', duration: 2000, - }, 3500);; - }, []); + }, 3500); + }, [fadeIn]); return ( { - inHomezone() ? ( - - -{/* 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 */} - - - - - - - ) : ( - - ) + inHomezone() + ? () + : () } {say} {say} @@ -426,7 +426,7 @@ function Walls({ shape, walls }: { shape: Pos, walls: Pos[] }) { const [width, height] = shape; return ( - + (null); + // used so that we can revert the animation + const [hasWonScreen, setHasWonScreen] = useState(false); + useEffect(() => { - if (game_uuid) { + if (game_uuid && animate) { let pathAnimation = anime.timeline() .add({ - targets: '#mazebox #maze path', + targets: mazeBoxRef.current?.querySelectorAll('.maze path'), strokeDashoffset: [anime.setDashoffset, 0], easing: 'easeInCubic', duration: 2000, @@ -497,59 +502,70 @@ function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, g loop: false }) .add({ - targets: '#mazebox #maze path', + targets: mazeBoxRef.current?.querySelectorAll('.maze path'), //fill: ['rgb(214, 219, 220)', "#faa"], // ffa fillOpacity: [0, 0.7], // ffa easing: 'linear', duration: 2000 }, 2000) .add({ - targets: '#mazebox #maze path', + targets: mazeBoxRef.current?.querySelectorAll('.maze path'), strokeWidth: 0, easing: 'linear', duration: 2000 }, 4000) .add({ - targets: '#mazebox .foodblue', + targets: mazeBoxRef.current?.querySelectorAll('.foodblue'), opacity: [0, 1], easing: 'linear', duration: 2000, }, 3000) .add({ - targets: '#mazebox .foodred', + targets: mazeBoxRef.current?.querySelectorAll('.foodred'), opacity: [0, 1], easing: 'linear', duration: 2000, }, 3000) .add({ - targets: '#mazebox .blue', + targets: mazeBoxRef.current?.querySelectorAll('.blue'), opacity: [0, 1], easing: 'linear', duration: 2000, }, 3500) .add({ - targets: '#mazebox .red', + targets: mazeBoxRef.current?.querySelectorAll('.red'), opacity: [0, 1], easing: 'linear', duration: 2000, }, 3500) .add({ - targets: '#mazebox .middleLine', + targets: mazeBoxRef.current?.querySelectorAll('.middleLine'), opacity: [0, 1], easing: 'linear', duration: 2000, }, 3500); } - }, [walls, game_uuid]); + }, [walls, game_uuid, animate, mazeBoxRef]); 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) { let pathAnimation = anime.timeline() .add({ - targets: '#mazebox #maze path', - fill: ["#faa", "rgb(94, 158, 217)"], + targets: mazeBoxRef.current?.querySelectorAll('.maze path'), + fill: ["#000", "rgb(94, 158, 217)"], easing: 'linear', duration: 200 }); @@ -557,8 +573,8 @@ function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, g if (gameover && whowins === 1) { let pathAnimation = anime.timeline() .add({ - targets: '#mazebox #maze path', - fill: ["#faa", "rgb(235, 90, 90)"], + targets: mazeBoxRef.current?.querySelectorAll('.maze path'), + fill: ["#000", "rgb(235, 90, 90)"], easing: 'linear', duration: 200 }); @@ -566,17 +582,17 @@ function Maze({ game_uuid, shape, walls, food, bots, team_names, say, whowins, g if (gameover && whowins === 2) { let pathAnimation = anime.timeline() .add({ - targets: '#mazebox #maze path', - fill: ["#faa", "#fff"], + targets: mazeBoxRef.current?.querySelectorAll('.maze path'), + fill: ["#000", "#ffa"], easing: 'linear', duration: 200 }); }; - }, [gameover, whowins]); + }, [gameover, whowins, mazeBoxRef, hasWonScreen]); return ( -
+
- - - - + + + + { gameover ? (<> diff --git a/app/page.tsx b/app/page.tsx index 4a1126d..0dff606 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,7 @@ -import Pelita from "./pelita"; +import PelitaTournament from "./tournament"; export default function Home() { return (<> - + ); } diff --git a/app/pelita.tsx b/app/pelita.tsx index 6c2084c..990357f 100644 --- a/app/pelita.tsx +++ b/app/pelita.tsx @@ -1,47 +1,12 @@ "use client" -import React, { - Reducer, - useEffect, - useReducer -} from "react"; - - import Maze from "./maze"; -import ZMQReceiver from "./zmqreceiver"; -import TypewriterText from "./typewritertext"; -import { GameState } from "./zmqreceiver"; -import anime from "animejs"; +import { GameState } from "./pelita_msg"; -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 = (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 }) { +function Pelita({ gameState, footer, animate }: { gameState: GameState, footer: string, animate: boolean }) { const [team1, team2] = gameState.team_names; - return
+ return

{team1} {gameState.game_stats.score[0]} : @@ -65,153 +30,15 @@ function PelitaMain({ gameState }: { gameState: GameState }) { gameover={gameState.gameover} round={gameState.round} turn={gameState.turn} + animate={animate} >
-
ᗧ Pelita Tournament, ASPP 2024 Ηράκλειο
+
{ footer }
Round {gameState.round ?? "-"}/{gameState.max_rounds}

} - - -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([]); - - const [gameState, setGameState] = React.useState(); - - 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 ; - } - - function inner() { - - if (state == "initial") { - return ; - } else if (state == "movie") { - return ; - } else if (state == "intro" || state == "match") { - - return ( -
-

- ᗧ Pelita Tournament 2024 -

- - {state === "intro" ? showIntro() : null} - - {state === "match" && gameState ? - - : null - } - - -
- ); - } - }; - - return ( -
-
- - { inner() } - -
-
- ); -}; export default Pelita; diff --git a/app/pelita_msg.ts b/app/pelita_msg.ts index 6e1847a..75390e7 100644 --- a/app/pelita_msg.ts +++ b/app/pelita_msg.ts @@ -20,10 +20,11 @@ type RootMsg = { __data__: null }; -type Pos = [number, number]; +export type Pos = [number, number]; export type { RootMsg }; +export type ObserveDataL = ObserveData[]; export interface ObserveData { game_uuid: string; walls: Pos[]; @@ -64,3 +65,57 @@ export interface RequestedMove { requested_position: number[]; 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; + 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 + }; +} diff --git a/app/tournament.tsx b/app/tournament.tsx new file mode 100644 index 0000000..1aac8a6 --- /dev/null +++ b/app/tournament.tsx @@ -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 = (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([]); + + const [gameState, setGameState] = React.useState(); + + 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 ; + }; + + return ( +
+
+ + {state == "initial" && } + + {state == "movie" && + } + + {state === "intro" && showIntro()} + + {state == "match" && +
+

+ ᗧ Pelita Tournament 2024 +

+ + { gameState && } +
+ } + + +
+
+ ); +}; +export default PelitaTournament; diff --git a/app/typewritertext.tsx b/app/typewritertext.tsx index f3c354a..6410ca3 100644 --- a/app/typewritertext.tsx +++ b/app/typewritertext.tsx @@ -75,7 +75,7 @@ function TypewriterLine( { text, cursor, lineFinished }: { text: string, cursor: //Clearing the interval return () => clearInterval(interval); - }, [index]); + }, [index, text.length]); if (current === "") return (

); diff --git a/app/zmqreceiver.tsx b/app/zmqreceiver.tsx index f7b98ae..63b894b 100644 --- a/app/zmqreceiver.tsx +++ b/app/zmqreceiver.tsx @@ -1,67 +1,11 @@ 'use client' import React, { useEffect, useState } from 'react'; -import type { Tuple4 } from './typeutils'; -import { ObserveData } from './pelita_msg'; -import type { RootMsg } from './pelita_msg'; +import { conv_game_state } from './pelita_msg'; +import type { RootMsg, GameState } from './pelita_msg'; 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; - 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, sendGameState: (gs: GameState) => any, sendMessage: (msg: string) => any, @@ -83,6 +27,7 @@ const ZMQReceiver = ({ url, sendGameState, sendMessage, sendClearPage }: { url: let parsed = JSON.parse(message) as RootMsg; console.log(parsed); 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, '')); } else if (parsed.__action__ === 'CLEAR') { sendClearPage(); @@ -98,7 +43,7 @@ const ZMQReceiver = ({ url, sendGameState, sendMessage, sendClearPage }: { url: socket.unsubscribe(''); socket.close(); }; - }, [url]); + }, [url, sendGameState, sendClearPage, sendMessage]); return (<> //