can play game

This commit is contained in:
2026-03-02 13:39:18 -07:00
parent 9955a7f90c
commit 0dae393d0d
27 changed files with 856 additions and 474 deletions

View File

@@ -1,24 +1,23 @@
import { useEffect, useState } from "react";
import { useGameChannelContext } from "../contexts/useGameChannelContext";
import { z } from "zod";
import { BoardPlayer } from "./BoardPlayer";
interface Player {
x: number;
y: number;
}
const PlayerSchema = z.object({
x: z.number(),
y: z.number(),
});
interface GameState {
[playerName: string]: Player;
}
const GameStateSchema = z.record(z.string(), PlayerSchema);
export const BoardDisplay = ({ playerName }: {
playerName: string;
}) => {
type GameState = z.infer<typeof GameStateSchema>;
export const BoardDisplay = ({ playerName }: { playerName: string }) => {
const { channel, isJoined, joinGame } = useGameChannelContext();
const [players, setPlayers] = useState<GameState>({});
useEffect(() => {
if (channel && !isJoined) {
// Send join_game message with player name
joinGame(playerName);
}
}, [channel, isJoined, playerName, joinGame]);
@@ -26,62 +25,34 @@ export const BoardDisplay = ({ playerName }: {
useEffect(() => {
if (!channel) return;
// Listen for game state updates
const ref = channel.on("game_state", (payload: { players: GameState }) => {
setPlayers(payload.players);
});
const ref = channel.on(
"game_state",
(payload: { game_state: unknown }) => {
const result = GameStateSchema.safeParse(payload.game_state);
if (result.success) {
setPlayers(result.data);
} else {
console.error("Invalid game state received:", result.error);
setPlayers({});
}
},
);
// Cleanup listener on unmount
return () => {
channel.off("game_state", ref);
};
}, [channel]);
return (
<div
style={{
position: "relative",
width: "800px",
height: "600px",
background: "#16213e",
margin: "50px auto",
border: "2px solid #0f3460",
overflow: "hidden",
}}
>
<div className="relative w-200 h-150 bg-navy-800 my-12.5 mx-auto border-2 border-navy-700 overflow-hidden">
{Object.entries(players).map(([name, player]) => (
<div
<BoardPlayer
key={name}
style={{
position: "absolute",
left: player.x,
top: player.y,
width: "20px",
height: "20px",
borderRadius: "50%",
background: name === playerName ? "#e94560" : "#53a8b6",
border:
name === playerName ? "3px solid #ff6b6b" : "2px solid #48d6e0",
transition: "all 0.1s linear",
transform: "translate(-50%, -50%)",
boxShadow:
name === playerName ? "0 0 10px #e94560" : "0 0 5px #53a8b6",
}}
>
<div
style={{
position: "absolute",
top: "-25px",
left: "50%",
transform: "translateX(-50%)",
color: "#fff",
fontSize: "10px",
whiteSpace: "nowrap",
}}
>
{name}
</div>
</div>
name={name}
x={player.x}
y={player.y}
isCurrentPlayer={name === playerName}
/>
))}
</div>
);

View File

@@ -0,0 +1,28 @@
import { type FC } from "react";
export const BoardPlayer: FC<{
name: string;
x: number;
y: number;
isCurrentPlayer: boolean;
}> = ({ name, x, y, isCurrentPlayer }) => {
return (
<div
style={{
left: x,
top: y,
}}
className={`
transition-all duration-50
absolute w-5 h-5 rounded-full linear -translate-x-1/2 -translate-y-1/2 ${
isCurrentPlayer
? "bg-accent-600 border-[3px] border-accent-400 shadow-[0_0_10px_var(--color-accent-600)]"
: "bg-navy-400 border-2 border-navy-300 shadow-[0_0_5px_var(--color-navy-400)]"
}`}
>
<div className="absolute -top-6.25 left-1/2 -translate-x-1/2 text-navy-50 text-[10px] whitespace-nowrap">
{name}
</div>
</div>
);
};

View File

@@ -6,19 +6,7 @@ export const ConnectionStatus = () => {
const { channelStatus } = useGameChannelContext();
return (
<div
style={{
position: "absolute",
top: 10,
right: 10,
color: "white",
fontFamily: "monospace",
background: "rgba(0,0,0,0.5)",
padding: "10px",
borderRadius: "5px",
fontSize: "12px",
}}
>
<div className="text-navy-50 font-mono bg-navy-900/80 p-2.5 rounded-[5px] text-xs">
<div>WebSocket: {connectionStatus}</div>
<div>Channel: {channelStatus}</div>
</div>

View File

@@ -29,35 +29,9 @@ export const NameInput = ({ onNameSubmit }: NameInputProps) => {
};
return (
<div
style={{
width: "100vw",
height: "100vh",
background: "#1a1a2e",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div
style={{
background: "#16213e",
padding: "40px",
borderRadius: "10px",
border: "2px solid #0f3460",
maxWidth: "400px",
width: "100%",
}}
>
<h1
style={{
color: "#e94560",
fontFamily: "monospace",
fontSize: "24px",
marginBottom: "20px",
textAlign: "center",
}}
>
<div className="w-screen h-screen bg-navy-900 flex items-center justify-center">
<div className="bg-navy-800 p-10 rounded-[10px] border-2 border-navy-700 max-w-100 w-full">
<h1 className="text-accent-500 font-mono text-2xl mb-5 text-center">
Enter Your Name
</h1>
<form onSubmit={handleSubmit}>
@@ -67,34 +41,16 @@ export const NameInput = ({ onNameSubmit }: NameInputProps) => {
onChange={(e) => setName(e.target.value)}
placeholder="Your name..."
autoFocus
style={{
width: "100%",
padding: "12px",
fontSize: "16px",
background: "#0f3460",
border: "2px solid #53a8b6",
borderRadius: "5px",
color: "white",
fontFamily: "monospace",
marginBottom: "20px",
boxSizing: "border-box",
}}
className="w-full p-3 text-base bg-navy-700 border-2 border-navy-500 rounded-[5px] text-navy-50 font-mono mb-5 box-border placeholder:text-navy-400"
/>
<button
type="submit"
disabled={!name.trim()}
style={{
width: "100%",
padding: "12px",
fontSize: "16px",
background: name.trim() ? "#e94560" : "#666",
border: "none",
borderRadius: "5px",
color: "white",
fontFamily: "monospace",
cursor: name.trim() ? "pointer" : "not-allowed",
fontWeight: "bold",
}}
className={`w-full p-3 text-base border-none rounded-[5px] text-navy-50 font-mono font-bold ${
name.trim()
? "bg-accent-600 hover:bg-accent-500 cursor-pointer"
: "bg-navy-600 cursor-not-allowed"
}`}
>
Join Game
</button>

View File

@@ -0,0 +1,24 @@
import type { FC } from "react";
export const SessionOverride: FC = () => {
return (
<div className="fixed inset-0 bg-navy-900/95 flex items-center justify-center z-50">
<div className="bg-navy-800 border-2 border-accent-600 rounded-lg p-8 max-w-md text-center">
<div className="text-6xl mb-4"></div>
<h2 className="text-2xl font-bold text-accent-500 mb-4">
Session Overridden
</h2>
<p className="text-navy-200 mb-6">
This session has been replaced by a new browser connection. Your
inputs are no longer active.
</p>
<button
onClick={() => window.location.reload()}
className="bg-accent-600 hover:bg-accent-500 text-navy-900 font-bold py-2 px-6 rounded transition-colors"
>
Reload Page
</button>
</div>
</div>
);
};

View File

@@ -12,34 +12,37 @@ export const UserInput = ({ playerName }: { playerName: string }) => {
const key = e.key.toLowerCase();
if (["w", "a", "s", "d"].includes(key)) {
e.preventDefault();
keysPressed.current.add(key);
if (!keysPressed.current.has(key) && channel.state === "joined") {
keysPressed.current.add(key);
channel.push("key_down", { key, name: playerName });
}
}
};
const handleKeyUp = (e: KeyboardEvent) => {
const key = e.key.toLowerCase();
if (["w", "a", "s", "d"].includes(key)) {
keysPressed.current.delete(key);
if (keysPressed.current.has(key) && channel.state === "joined") {
keysPressed.current.delete(key);
channel.push("key_up", { key, name: playerName });
}
}
};
const handleBlur = () => {
if (channel.state === "joined") {
keysPressed.current.forEach((key) => {
channel.push("key_up", { key, name: playerName });
});
}
keysPressed.current.clear();
};
const interval = setInterval(() => {
if (keysPressed.current.size > 0 && channel.state === "joined") {
const directions = Array.from(keysPressed.current);
channel.push("move", { directions, name: playerName });
}
}, 100);
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
window.addEventListener("blur", handleBlur);
return () => {
clearInterval(interval);
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
window.removeEventListener("blur", handleBlur);