Skip to content

Commit

Permalink
Refac pong board component (#291)
Browse files Browse the repository at this point in the history
* Remnove `useStateCallback

* Remove unncesary info

* Remove game.setUserMode

* UserModeType -> UserMode

* Create useGame hook

* Move userMode to useGame

* Move player info to useGamte

* Create usePlayers

* Remove memoization

* Move theme change logic to useGame

* Create useGameSocket

* Move useGameSocket to useGame

* Move game ticking logic to useGame

* Create useGameKeyboard

* create useGameTheme

* Create useGetGame

* Define DEFAULT_COLOR in const

* Create lib/hooks/game/ folder

* Format
  • Loading branch information
takumihara authored Mar 4, 2024
1 parent ab33f00 commit 6ce09a6
Show file tree
Hide file tree
Showing 12 changed files with 422 additions and 393 deletions.
1 change: 1 addition & 0 deletions backend/src/events/events.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export class EventsGateway implements OnGatewayDisconnect {
if (opponent) {
this.emitUpdateStatus(client, 'ready', {
user: this.users[opponent],
playerNumber: this.players[gameId][opponent],
});
}
this.lostPoints[client.id] = 0;
Expand Down
57 changes: 57 additions & 0 deletions frontend/app/lib/hooks/game/useGame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useRef, useState } from "react";
import { useUserMode } from "../useUserMode";
import { UserEntity } from "../../dtos";
import usePlayers from "../usePlayers";
import useGameSocket from "./useGameSocket";
import { TARGET_FRAME_MS } from "@/app/pong/[id]/const";
import useGameKeyboard from "./useGameKeyboard";
import useGameTheme from "./useGameTheme";
import useGetGame from "./useGetGame";

export default function useGame(
id: string,
currentUser?: UserEntity,
resolvedTheme?: string,
) {
const canvasRef = useRef<HTMLCanvasElement | null>(null); // only initialized once
const [userMode, setUserMode] = useUserMode();
const { getGame } = useGetGame(canvasRef, userMode);
const [logs, setLogs] = useState<string[]>([]);
const [startDisabled, setStartDisabled] = useState(true);

const { leftPlayer, rightPlayer, getPlayerSetterFromPlayerNumber } =
usePlayers(userMode, currentUser);

useGameTheme(getGame, resolvedTheme);
useGameKeyboard(getGame);

const { start } = useGameSocket(
id,
getGame,
setLogs,
userMode,
setUserMode,
getPlayerSetterFromPlayerNumber,
setStartDisabled,
currentUser,
);

useEffect(() => {
const game = getGame();
game.draw_canvas();
const intervalId = setInterval(game.update, TARGET_FRAME_MS);

return () => clearInterval(intervalId);
}, [getGame]);

return {
getGame,
canvasRef,
userMode,
leftPlayer,
rightPlayer,
logs,
start,
startDisabled,
};
}
28 changes: 28 additions & 0 deletions frontend/app/lib/hooks/game/useGameKeyboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PongGame } from "@/app/pong/[id]/PongGame";
import { useEffect } from "react";

export default function useGameKeyboard(getGame: () => PongGame) {
useEffect(() => {
const game = getGame();

const handleKeyUp = (event: KeyboardEvent) => {
if (event.key == "ArrowDown" || event.key == "ArrowUp") {
game.setMovingDirection("none");
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key == "ArrowDown") {
game.setMovingDirection("right");
} else if (event.key == "ArrowUp") {
game.setMovingDirection("left");
}
};

document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("keyup", handleKeyUp);
};
}, [getGame]);
}
244 changes: 244 additions & 0 deletions frontend/app/lib/hooks/game/useGameSocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { PongGame } from "@/app/pong/[id]/PongGame";
import { useCallback, useEffect, useRef } from "react";
import { Socket, io } from "socket.io-client";
import { UserMode } from "../useUserMode";
import { UserEntity } from "../../dtos";
import { POINT_TO_WIN } from "@/app/pong/[id]/const";

type Status =
| "too-many-players"
| "joined-as-player"
| "joined-as-viewer"
| "ready"
| "login-required"
| "friend-joined"
| "friend-left"
| "won"
| "lost"
| "finish";

interface HandleActionProps {
playerNumber: number;
}

const getLogFromStatus = (status: Status): string => {
switch (status) {
case "too-many-players":
return "There are too many players. You can only watch the game";
case "joined-as-player":
return "You have joined as player";
case "joined-as-viewer":
return "You have joined as viewer";
case "ready":
return "Your friend is already here. The game is ready to start";
case "login-required":
return "You need to login to play.";
case "friend-joined":
return "Your friend has joined the game";
case "friend-left":
return "Your friend has left";
case "won":
return "You won!";
case "lost":
return "You lost!";
case "finish":
return "The game has finished";
}
};

export default function useGameSocket(
id: string,
getGame: () => PongGame,
setLogs: (fun: (logs: string[]) => string[]) => void,
userMode: UserMode,
setUserMode: (mode: UserMode) => void,
getPlayerSetterFromPlayerNumber: (
playerNumber: number,
) => (user: any) => void,
setStartDisabled: (disabled: boolean) => void,
currentUser?: UserEntity,
) {
const socketRef = useRef<Socket | null>(null); // updated on `id` change

const start = useCallback(() => {
if (!userMode) return;
const game = getGame();

setStartDisabled(true);

const { vx, vy } = game.start({ vx: undefined, vy: undefined });
socketRef.current?.emit("start", {
vx: -vx,
vy: -vy,
});
}, [getGame, userMode, setStartDisabled]);

const runSideEffectForStatusUpdate = useCallback(
(status: Status, payload: any) => {
const game = getGame();

switch (status) {
case "too-many-players":
// TODO: users cannot really see the log
setUserMode("viewer");
break;
case "login-required":
// TODO: instead of redirect. Show modal to login
setUserMode("viewer");
break;
case "friend-joined":
const { playerNumber, user } = payload;
const setter = getPlayerSetterFromPlayerNumber(playerNumber);
setter(user);
currentUser && setStartDisabled(false);
game.resetPlayerPosition();
break;
case "friend-left":
{
const { playerNumber } = payload;
const setter = getPlayerSetterFromPlayerNumber(playerNumber);
setter(undefined);
setStartDisabled(true);
}
break;
case "joined-as-viewer":
{
const { players } = payload;
players.forEach(({ playerNumber, user }: any) => {
const setter = getPlayerSetterFromPlayerNumber(playerNumber);
setter(user);
});
}
break;
case "ready": {
{
const { user, playerNumber } = payload;
const setter = getPlayerSetterFromPlayerNumber(playerNumber);
setter(user);
}
break;
}
}
},
[
currentUser,
setUserMode,
getGame,
getPlayerSetterFromPlayerNumber,
setStartDisabled,
],
);

useEffect(() => {
const socket = io("/pong", {
query: { game_id: id, is_player: userMode === "player" },
forceNew: true,
});
socketRef.current = socket;

const game = getGame();
game.onAction = (action: string) => {
socket.emit(action);
};

const handleUpdateStatus = ({
status,
payload,
}: {
status: Status;
payload: any;
}) => {
runSideEffectForStatusUpdate(status, payload);
const log = getLogFromStatus(status);
setLogs((logs) => [...logs, log]);
};
const handleConnect = () => {
console.log(`Connected: ${socketRef.current?.id}`);
const log = "Connected to server";
setLogs((logs) => [...logs, log]);
};

const handleStart = (data: { vx: number; vy: number }) => {
game.start(data);
setStartDisabled(true);
};

const handleRight = ({ playerNumber }: HandleActionProps) => {
if (userMode !== "player" && playerNumber == 1) {
game.movePlayer1Left();
} else {
game.movePlayer2Left();
}
};

const handleLeft = ({ playerNumber }: HandleActionProps) => {
if (userMode !== "player" && playerNumber == 1) {
game.movePlayer1Right();
} else {
game.movePlayer2Right();
}
};

const handleBounce = ({ playerNumber }: HandleActionProps) => {
if (userMode !== "player" && playerNumber == 1) {
game.bounceOffPaddlePlayer1();
} else {
game.bounceOffPaddlePlayer2();
}
};

const handleCollide = (msg: HandleActionProps) => {
const { playerNumber } = msg;
if (userMode === "player") {
const score = game.increaseScorePlayer1();
if (score != POINT_TO_WIN) {
setTimeout(() => start(), 1000);
}
} else {
if (playerNumber == 1) {
game.increaseScorePlayer2();
} else {
game.increaseScorePlayer1();
}
}
game.endRound();
};

const handleFinish = () => {
const game = getGame();
game.stop();
};

socket.on("connect", handleConnect);
socket.on("start", handleStart);
socket.on("right", handleRight);
socket.on("left", handleLeft);
socket.on("bounce", handleBounce);
socket.on("collide", handleCollide);
socket.on("update-status", handleUpdateStatus);
socket.on("finish", handleFinish);

return () => {
socket.off("connect", handleConnect);
socket.off("start", handleStart);
socket.off("right", handleRight);
socket.off("left", handleLeft);
socket.off("bounce", handleBounce);
socket.off("collide", handleCollide);
socket.off("update-status", handleUpdateStatus);
socket.off("finish", handleFinish);
socket.disconnect();
};
}, [
id,
getGame,
setLogs,
start,
userMode,
setUserMode,
runSideEffectForStatusUpdate,
setStartDisabled,
]);

return { socketRef, start };
}
17 changes: 17 additions & 0 deletions frontend/app/lib/hooks/game/useGameTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PongGame } from "@/app/pong/[id]/PongGame";
import { useEffect } from "react";

export default function useGameTheme(
getGame: () => PongGame,
resolvedTheme?: string,
) {
useEffect(() => {
// TODO: Use --foreground color from CSS
// Somehow it didn't work (theme is changed but not yet committed to CSS/DOM?)
const game = getGame();
const color =
resolvedTheme === "dark" ? "hsl(0, 0%, 100%)" : "hsl(0, 0%, 0%)";
game.setColor(color);
game.draw_canvas();
}, [resolvedTheme, getGame]);
}
26 changes: 26 additions & 0 deletions frontend/app/lib/hooks/game/useGetGame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PongGame } from "@/app/pong/[id]/PongGame";
import { MutableRefObject, useCallback, useRef } from "react";
import { UserMode } from "../useUserMode";
import { DEFAULT_COLOR } from "@/app/pong/[id]/const";

export default function useGetGame(
canvasRef: MutableRefObject<HTMLCanvasElement | null>,
userMode: UserMode,
) {
const gameRef = useRef<PongGame | null>(null); // only initialized once

const getGame = useCallback(() => {
const ctx = canvasRef.current?.getContext("2d");
if (!ctx) {
throw new Error("canvas not ready");
}
if (!gameRef.current) {
const game = new PongGame(ctx, DEFAULT_COLOR, DEFAULT_COLOR, userMode);
gameRef.current = game;
return game;
}
return gameRef.current;
}, [canvasRef, userMode]);

return { getGame };
}
Loading

0 comments on commit 6ce09a6

Please sign in to comment.