diff --git a/demo-react/functions/auth.ts b/demo-react/functions/auth.ts index f2f974f..1defb15 100644 --- a/demo-react/functions/auth.ts +++ b/demo-react/functions/auth.ts @@ -1,5 +1,9 @@ import { AccessToken, PeerClaims, PeerPolicy } from "@pulsebeam/server/workerd"; +// This is an example Cloudflare Page Function for Serving PulseBeam Tokens +// For more details, see +// https://pulsebeam.dev/docs/guides/token/#cloudflare-page-functions + interface Env { PULSEBEAM_API_KEY: string; PULSEBEAM_API_SECRET: string; diff --git a/demo-react/src/peer.ts b/demo-react/src/peer.ts index 2228c6a..fbd24f7 100644 --- a/demo-react/src/peer.ts +++ b/demo-react/src/peer.ts @@ -93,7 +93,7 @@ export const usePeerStore = create((set, get) => ({ p.onsession = (s) => { // For you app consider your UI/UX in what you want to support - // In this app, we only support multiple sessions at a time. + // In this app, we support multiple sessions at a time. const id = `${s.other.peerId}:${s.other.connId}`; s.ontrack = ({ streams }) => { diff --git a/multiplayer-games/battleship/.gitignore b/multiplayer-games/battleship/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/multiplayer-games/battleship/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/multiplayer-games/battleship/README b/multiplayer-games/battleship/README new file mode 100644 index 0000000..4e938be --- /dev/null +++ b/multiplayer-games/battleship/README @@ -0,0 +1,48 @@ +# Battleship Game + +A peer-to-peer battleship game using WebRTC and the PulseBeam SDK. + +## Overview + +This application allows two players to connect via WebRTC and play a real-time battleship game. One player controls a battleship that can move around the board, while the other player controls a cannon trying to hit the battleship. + +## Features + +- Peer-to-peer connection using WebRTC +- Real-time gameplay with no server requirement (after initial connection) +- Simple and intuitive UI +- Responsive design +- Debug panel for troubleshooting + +## Local Development Setup + +### Prerequisites + +- Node.js (v16 or higher) +- npm or yarn + +### Installation + +1. Clone the repository: + \`\`\` + git clone [repository-url] + cd battleship-game + \`\`\` + +2. Install dependencies: + \`\`\` + npm install --force + \`\`\` + + > **Note:** The `--force` flag is necessary due to some dependency version conflicts. + +3. Start the development server: + \`\`\` + npm run dev + \`\`\` + +4. Open your browser and navigate to: + \`\`\` + http://localhost:3000 + \`\`\` + diff --git a/multiplayer-games/battleship/app/globals.css b/multiplayer-games/battleship/app/globals.css new file mode 100644 index 0000000..52cf467 --- /dev/null +++ b/multiplayer-games/battleship/app/globals.css @@ -0,0 +1,77 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + diff --git a/multiplayer-games/battleship/app/layout.tsx b/multiplayer-games/battleship/app/layout.tsx new file mode 100644 index 0000000..c4129af --- /dev/null +++ b/multiplayer-games/battleship/app/layout.tsx @@ -0,0 +1,34 @@ +import type React from "react" +import "./globals.css" +import type { Metadata } from "next" +import { Inter } from "next/font/google" +import { ThemeProvider } from "@/components/theme-provider" + +const inter = Inter({ subsets: ["latin"] }) + +// Update the metadata +export const metadata: Metadata = { + title: "Battleship Game", + description: "A peer-to-peer battleship game using WebRTC", + generator: 'v0.dev' +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + + {children} + + + + ) +} + + + +import './globals.css' \ No newline at end of file diff --git a/multiplayer-games/battleship/app/page.tsx b/multiplayer-games/battleship/app/page.tsx new file mode 100644 index 0000000..8f342e0 --- /dev/null +++ b/multiplayer-games/battleship/app/page.tsx @@ -0,0 +1,55 @@ +"use client" +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Bug } from "lucide-react" +import { StartPage } from "@/components/start-page" +import { DebugPanel } from "@/components/debug-panel" +import GameRoom from "@/components/game-room" +import { DEFAULT_GROUP } from "@/lib/token-service" +import { Logger } from "@/lib/logger" +// Remove the direct import of usePeerStore + +export default function Home() { + const [isStarted, setIsStarted] = useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const [showDebug, setShowDebug] = useState(false) + + // The rest of your component remains the same + const handleGameStart = () => { + setIsStarted(true) + } + + const toggleDebug = () => { + setShowDebug(!showDebug) + Logger.log(`Debug panel ${!showDebug ? "opened" : "closed"}`) + } + + return ( +
+
+
+

Battleship Game

+

A peer-to-peer battleship game using WebRTC

+ + +
+ + {showDebug && ( + + )} + +
+ {!isStarted ? ( + + ) : ( + + )} +
+
+
+ ) +} + diff --git a/multiplayer-games/battleship/components.json b/multiplayer-games/battleship/components.json new file mode 100644 index 0000000..d9ef0ae --- /dev/null +++ b/multiplayer-games/battleship/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/multiplayer-games/battleship/components/battleship-game-board.tsx b/multiplayer-games/battleship/components/battleship-game-board.tsx new file mode 100644 index 0000000..e9d50d6 --- /dev/null +++ b/multiplayer-games/battleship/components/battleship-game-board.tsx @@ -0,0 +1,220 @@ +"use client" +import { useState, useEffect } from "react" +import { usePeerStore, type GameState, type Position } from "@/lib/peer-store" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Ship, Target, Crosshair } from "lucide-react" +import { Logger } from "@/lib/logger" + +interface BattleshipGameBoardProps { + gameState: GameState + sessionId: string + isMyTurn: boolean // Keep for compatibility + myPeerId: string + logger?: any +} + +export default function BattleshipGameBoard({ + gameState, + sessionId, + isMyTurn, + myPeerId, + logger = Logger, +}: BattleshipGameBoardProps) { + const peer = usePeerStore() + const [hoveredCell, setHoveredCell] = useState(null) + const cellSize = 40 // Size of each grid cell in pixels + const gridSize = gameState.boardSize // Number of cells in the grid + + // Determine if I am player 1 (battleship) or player 2 (cannon) + // The game creator is always the battleship player + const isBattleshipPlayer = gameState.currentTurn === myPeerId + const isCannonPlayer = !isBattleshipPlayer + + // Log component mount and props + useEffect(() => { + if (logger) { + logger.log("BattleshipGameBoard: Component mounted", { + gameId: gameState.gameId, + sessionId, + isMyTurn, + myPeerId, + role: isBattleshipPlayer ? "Battleship" : "Cannon", + }) + } + + return () => { + if (logger) { + logger.log("BattleshipGameBoard: Component unmounted") + } + } + }, [gameState.gameId, sessionId, isMyTurn, myPeerId, isBattleshipPlayer, logger]) + + const handleCellClick = (x: number, y: number) => { + if (gameState.gameOver) return + + const position: Position = { x, y } + + if (isBattleshipPlayer) { + // Player 1 moves the battleship + peer.moveBattleship(gameState.gameId, sessionId, position) + if (logger) { + logger.log(`Moving battleship to (${x}, ${y})`) + } + } else { + // Player 2 fires the cannon + peer.fireCannon(gameState.gameId, sessionId, position) + if (logger) { + logger.log(`Firing cannon at (${x}, ${y})`) + } + } + } + + const getLastActionText = () => { + if (!gameState.lastAction) return null + + const isMe = gameState.lastAction.player === myPeerId + const playerText = isMe ? "You" : "Opponent" + + switch (gameState.lastAction.type) { + case "move": + return `${playerText} moved the battleship to (${gameState.lastAction.position?.x}, ${gameState.lastAction.position?.y})` + case "fire": + return `${playerText} fired at (${gameState.lastAction.position?.x}, ${gameState.lastAction.position?.y})` + default: + return null + } + } + + const lastActionText = getLastActionText() + + return ( +
+ + +
+ Battleship Game #{gameState.gameId.split("-")[1]} + + {gameState.gameOver + ? gameState.winner === myPeerId + ? "You Won!" + : "You Lost!" + : isBattleshipPlayer + ? "You are the Battleship" + : "You are the Cannon"} + +
+ You are the {isBattleshipPlayer ? "Battleship" : "Cannon"} player + {lastActionText &&
Last action: {lastActionText}
} + {gameState.gameOver && ( +
+ {gameState.winner === myPeerId ? "Congratulations! You won!" : "Game over! Your opponent won."} +
+ )} +
+
+ + {/* Game board */} + + + Game Board + + {isBattleshipPlayer ? "Click to move your battleship" : "Click to fire your cannon"} + + + +
+ {Array.from({ length: gridSize * gridSize }).map((_, index) => { + const x = index % gridSize + const y = Math.floor(index / gridSize) + + // Check if this cell contains the battleship + const hasBattleship = x === gameState.battleshipPosition.x && y === gameState.battleshipPosition.y + + // Check if this cell has a cannon shot + const cannonShot = gameState.cannonShots.find((shot) => shot.position.x === x && shot.position.y === y) + + // Determine cell styling + const isHovered = hoveredCell?.x === x && hoveredCell?.y === y + + return ( +
!gameState.gameOver && handleCellClick(x, y)} + onMouseEnter={() => setHoveredCell({ x, y })} + onMouseLeave={() => setHoveredCell(null)} + > + {hasBattleship && (isBattleshipPlayer || cannonShot) && ( + + )} + {cannonShot && !hasBattleship && ( +
+ )} + {isHovered && + !gameState.gameOver && + !hasBattleship && + !cannonShot && + (isBattleshipPlayer ? ( + + ) : ( + + ))} +
+ ) + })} +
+ + + + {/* Game instructions */} + + + How to Play (Real-time Mode) + + +
+ {isBattleshipPlayer ? ( + <> +

+ You control the battleship . +

+

Click on any cell to move your battleship there.

+

Avoid getting hit by the cannon shots!

+

This is real-time mode - you can move at any time!

+ + ) : ( + <> +

+ You control the cannon . +

+

Click on any cell to fire your cannon there.

+

Try to hit the battleship to win!

+

This is real-time mode - you can fire at any time!

+ + )} +
+
+
+
+ ) +} + diff --git a/multiplayer-games/battleship/components/debug-panel.tsx b/multiplayer-games/battleship/components/debug-panel.tsx new file mode 100644 index 0000000..456b746 --- /dev/null +++ b/multiplayer-games/battleship/components/debug-panel.tsx @@ -0,0 +1,153 @@ +"use client" +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Logger } from "@/lib/logger" +import { TokenService } from "@/lib/token-service" +import { usePeerStore } from "@/lib/peer-store" + +interface DebugPanelProps { + onClose: () => void + errorMessage: string | null + setErrorMessage: (message: string | null) => void +} + +export function DebugPanel({ onClose, errorMessage, setErrorMessage }: DebugPanelProps) { + const [logs, setLogs] = useState([]) + const peer = usePeerStore() + + // Update logs periodically + useEffect(() => { + const updateLogs = () => { + setLogs([...Logger.getLogs()]) + } + + // Initial update + updateLogs() + + // Set up listener for log changes + const removeListener = Logger.addListener(updateLogs) + + // Set up interval for periodic updates (as a backup) + const interval = setInterval(updateLogs, 1000) + + return () => { + clearInterval(interval) + removeListener() + } + }, []) + + const clearLogs = () => { + Logger.clear() + } + + const testCloudflareWorker = async () => { + const result = await TokenService.testWorker() + setErrorMessage(result.message) + } + + // Add a check before accessing navigator + const getEnvironmentInfo = () => { + if (typeof window === "undefined" || typeof navigator === "undefined") { + return { + userAgent: "SSR", + language: "SSR", + platform: "SSR", + webRTC: { + RTCPeerConnection: false, + RTCSessionDescription: false, + RTCIceCandidate: false, + }, + url: "SSR", + protocol: "SSR", + host: "SSR", + } + } + + return { + userAgent: navigator.userAgent, + language: navigator.language, + platform: navigator.platform, + webRTC: { + RTCPeerConnection: typeof RTCPeerConnection !== "undefined", + RTCSessionDescription: typeof RTCSessionDescription !== "undefined", + RTCIceCandidate: typeof RTCIceCandidate !== "undefined", + }, + url: window.location.href, + protocol: window.location.protocol, + host: window.location.host, + } + } + + return ( + + +
+ Debug Information +
+ + +
+
+
+ +
+
+

Application Logs ({logs.length})

+
+ {logs.map((log, index) => ( +
+ {log} +
+ ))} + {logs.length === 0 &&
No logs yet
} +
+
+ +
+

Application State

+
+
+                {JSON.stringify(
+                  {
+                    errorMessage,
+                    peerState: {
+                      loading: peer.loading,
+                      peerId: peer.peerId,
+                      hasRef: !!peer.ref,
+                      sessionCount: Object.keys(peer.sessions).length,
+                    },
+                  },
+                  null,
+                  2,
+                )}
+              
+
+
+ +
+

Environment Information

+
+
{JSON.stringify(getEnvironmentInfo(), null, 2)}
+
+
+ +
+

Worker Test

+ +

+ This will send a test request to your Cloudflare Worker to check if it's working properly. +

+
+
+
+
+ ) +} + diff --git a/multiplayer-games/battleship/components/game-room.tsx b/multiplayer-games/battleship/components/game-room.tsx new file mode 100644 index 0000000..3f57955 --- /dev/null +++ b/multiplayer-games/battleship/components/game-room.tsx @@ -0,0 +1,328 @@ +"use client" +import { useState, useEffect } from "react" +import type React from "react" + +import { usePeerStore, setPeerStoreLogger } from "@/lib/peer-store" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Card, CardContent } from "@/components/ui/card" +import BattleshipGameBoard from "@/components/battleship-game-board" +import { AlertCircle, Loader2 } from "lucide-react" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Logger } from "@/lib/logger" + +interface GameRoomProps { + groupId: string + logger?: any +} + +export default function GameRoom({ groupId, logger = Logger }: GameRoomProps) { + const peer = usePeerStore() + const [otherPeerId, setOtherPeerId] = useState("") + const [gameCreated, setGameCreated] = useState(false) + const [connecting, setConnecting] = useState(false) + const [connectionError, setConnectionError] = useState(null) + const [waitingForConnection, setWaitingForConnection] = useState(false) + + // Set up logger if provided + useEffect(() => { + if (logger) { + setPeerStoreLogger(logger) + logger.log("GameRoom: Logger set for peer store") + } + }, [logger]) + + // Log component mount + useEffect(() => { + if (logger) { + logger.log("GameRoom: Component mounted", { groupId }) + } + + return () => { + if (logger) { + logger.log("GameRoom: Component unmounted") + } + } + }, [groupId, logger]) + + // Check if peer is still valid + useEffect(() => { + if (!peer.ref) { + if (logger) { + logger.error("GameRoom: Peer reference lost, reloading page") + } + // If peer reference is lost, reload the page + window.location.reload() + } + }, [peer.ref, logger]) + + const handleConnect = async (e: React.FormEvent) => { + e.preventDefault() + + if (!otherPeerId) { + if (logger) { + logger.error("GameRoom: Cannot connect - no peer ID provided") + } + return + } + + if (logger) { + logger.log(`GameRoom: Connecting to peer ${otherPeerId} in group ${groupId}`) + } + + setConnecting(true) + setConnectionError(null) + + try { + // Pass the group ID to the connect function + await peer.connect(otherPeerId, groupId) + if (logger) { + logger.log(`GameRoom: Successfully connected to peer ${otherPeerId}`) + } + + // Set a waiting period to ensure the connection is fully established + setWaitingForConnection(true) + setTimeout(() => { + setWaitingForConnection(false) + }, 3000) // Wait 3 seconds for the connection to stabilize + } catch (error) { + if (logger) { + logger.error("GameRoom: Connection error", error) + } + setConnectionError(error instanceof Error ? error.message : "Failed to connect to peer") + } finally { + setConnecting(false) + } + } + + const handleCreateGame = () => { + const sessionId = Object.keys(peer.sessions)[0] + if (!sessionId) { + if (logger) { + logger.error("GameRoom: Cannot create game - no active session") + } + return + } + + if (logger) { + logger.log("GameRoom: Creating new game") + } + + // Check if the session is ready for sending data + const session = peer.sessions[sessionId] + if (session.loading || session.sess.connectionState !== "connected") { + if (logger) { + logger.error("GameRoom: Cannot create game - connection not ready", { + loading: session.loading, + connectionState: session.sess.connectionState, + }) + } + setConnectionError("Connection not fully established. Please wait a moment and try again.") + return + } + + // Check if the data channel is available and open + if (!session.dataChannel || session.dataChannel.readyState !== "open") { + if (logger) { + logger.error("GameRoom: Cannot create game - data channel not ready", { + hasDataChannel: !!session.dataChannel, + dataChannelState: session.dataChannel ? session.dataChannel.readyState : "none", + }) + } + setConnectionError("WebRTC data channel not ready. Please wait a moment and try again.") + return + } + + try { + const gameState = peer.createGame() + peer.sendGameState(gameState, sessionId) + setGameCreated(true) + + if (logger) { + logger.log(`GameRoom: Game created with ID ${gameState.gameId}`) + } + } catch (error) { + if (logger) { + logger.error("GameRoom: Error creating game", error) + } + setConnectionError(error instanceof Error ? error.message : "Failed to create game. Please try again.") + } + } + + const handleEndSession = () => { + if (logger) { + logger.log("GameRoom: Ending session") + } + + // Properly close all connections + peer.stop() + + // Clear any local storage that might be persisting state + try { + // Clear any session-related data from localStorage + const keysToRemove = [] + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key && (key.includes("peer") || key.includes("session") || key.includes("webrtc"))) { + keysToRemove.push(key) + } + } + + keysToRemove.forEach((key) => localStorage.removeItem(key)) + + if (logger) { + logger.log(`GameRoom: Cleared ${keysToRemove.length} localStorage items`) + } + } catch (error) { + if (logger) { + logger.error("GameRoom: Error clearing localStorage", error) + } + } + + // Redirect to home instead of reloading to ensure a fresh start + window.location.href = window.location.pathname + } + + const sessions = Object.entries(peer.sessions) + const hasActiveSession = sessions.length > 0 + const sessionId = hasActiveSession ? sessions[0][0] : null + const gameState = sessionId ? peer.sessions[sessionId].gameState : null + const sessionInfo = sessionId ? peer.sessions[sessionId] : null + + // Log state changes + useEffect(() => { + if (logger) { + logger.log("GameRoom: State updated", { + hasActiveSession, + sessionId, + hasGameState: !!gameState, + peerDebugInfo: peer.debugInfo(), + }) + } + }, [hasActiveSession, sessionId, gameState, peer, logger]) + + // Check if the data channel is ready + const isDataChannelReady = sessionInfo && sessionInfo.dataChannel && sessionInfo.dataChannel.readyState === "open" + + return ( +
+
+

Battleship Game

+ + +
+ + + + Your Peer ID + + {peer.peerId} +

+ Share this ID with others so they can connect to you +

+
+
+ + {/* Connection status */} + + + {hasActiveSession ? ( +
+
+ Connected to: {sessions[0][1].sess.other.peerId} + {sessionInfo && ( +
+ Connection state: {sessionInfo.sess.connectionState} + {sessionInfo.loading && " (still establishing...)"} + {sessionInfo.dataChannel &&
Data channel state: {sessionInfo.dataChannel.readyState}
} +
+ )} +
+ + {!gameState && ( +
+ + {(waitingForConnection || !isDataChannelReady) && ( +

+ Please wait while the WebRTC connection is being established... +

+ )} +
+ )} + + {connectionError && ( + + + Error + {connectionError} + + )} +
+ ) : ( +
+
Not connected to anyone yet
+ + {connectionError && ( + + + Connection Error + {connectionError} + + )} + +
+ setOtherPeerId(e.target.value)} + className="flex-1" + disabled={connecting || peer.loading} + /> + + +
+
+ )} +
+
+ + {/* Game board */} + {gameState && sessionId && ( + + )} +
+ ) +} + diff --git a/multiplayer-games/battleship/components/start-page.tsx b/multiplayer-games/battleship/components/start-page.tsx new file mode 100644 index 0000000..60bc206 --- /dev/null +++ b/multiplayer-games/battleship/components/start-page.tsx @@ -0,0 +1,142 @@ +"use client" +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Copy, Loader2 } from "lucide-react" +import { Logger } from "@/lib/logger" +import { TokenService, DEFAULT_GROUP } from "@/lib/token-service" +import { usePeerStore } from "@/lib/peer-store" + +interface StartPageProps { + onGameStart: () => void + errorMessage: string | null + setErrorMessage: (message: string | null) => void +} + +export function StartPage({ onGameStart, errorMessage, setErrorMessage }: StartPageProps) { + const [peerId, setPeerId] = useState("") + const [startLoading, setStartLoading] = useState(false) + const peer = usePeerStore() + + const handleStart = async () => { + if (!peerId) { + Logger.error("Missing peer ID") + setErrorMessage("Please enter a peer ID") + return + } + + try { + setStartLoading(true) + setErrorMessage(null) + + Logger.log(`Starting peer connection process for peer: ${peerId}`) + + // Get token from Cloudflare Worker + const token = await TokenService.getToken(peerId, DEFAULT_GROUP) + + // Start the peer with the generated token + Logger.log("Starting peer with generated token") + try { + await peer.start(peerId, token) + Logger.log("Peer started successfully") + } catch (peerError) { + Logger.error("Error starting peer", peerError) + throw peerError + } + + Logger.log("Peer connection process completed successfully") + onGameStart() + } catch (error) { + Logger.error("Failed to start peer", error) + setErrorMessage(error instanceof Error ? error.message : "Unknown error occurred. Check console for details.") + } finally { + setStartLoading(false) + } + } + + // Add a check before accessing navigator.clipboard + const handleCopyPeerId = () => { + if (typeof navigator !== "undefined" && navigator.clipboard) { + navigator.clipboard.writeText(peerId) + Logger.log(`Copied peer ID to clipboard: ${peerId}`) + } else { + Logger.log(`Cannot copy to clipboard: browser API not available`) + } + } + + // Generate a random peer ID + const generateRandomPeerId = () => { + const randomId = `player-${Math.floor(Math.random() * 10000)}` + setPeerId(randomId) + Logger.log(`Generated random peer ID: ${randomId}`) + } + + return ( + + + Start a Game + Enter your peer ID to begin + + + {errorMessage && ( + + Error + {errorMessage} + + )} + +
+ +
+ setPeerId(e.target.value)} + placeholder="e.g., player123" + disabled={startLoading} + /> + + {peerId && ( + + )} +
+
+ + +
+ +

This application uses WebRTC for peer-to-peer communication. Make sure your browser supports WebRTC.

+
+
+ ) +} + diff --git a/multiplayer-games/battleship/components/theme-provider.tsx b/multiplayer-games/battleship/components/theme-provider.tsx new file mode 100644 index 0000000..55c2f6e --- /dev/null +++ b/multiplayer-games/battleship/components/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/multiplayer-games/battleship/components/ui/accordion.tsx b/multiplayer-games/battleship/components/ui/accordion.tsx new file mode 100644 index 0000000..24c788c --- /dev/null +++ b/multiplayer-games/battleship/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/multiplayer-games/battleship/components/ui/alert-dialog.tsx b/multiplayer-games/battleship/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..25e7b47 --- /dev/null +++ b/multiplayer-games/battleship/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/multiplayer-games/battleship/components/ui/alert.tsx b/multiplayer-games/battleship/components/ui/alert.tsx new file mode 100644 index 0000000..41fa7e0 --- /dev/null +++ b/multiplayer-games/battleship/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/multiplayer-games/battleship/components/ui/aspect-ratio.tsx b/multiplayer-games/battleship/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..d6a5226 --- /dev/null +++ b/multiplayer-games/battleship/components/ui/aspect-ratio.tsx @@ -0,0 +1,7 @@ +"use client" + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/multiplayer-games/battleship/components/ui/avatar.tsx b/multiplayer-games/battleship/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/multiplayer-games/battleship/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/multiplayer-games/battleship/components/ui/badge.tsx b/multiplayer-games/battleship/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/multiplayer-games/battleship/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/multiplayer-games/battleship/components/ui/breadcrumb.tsx b/multiplayer-games/battleship/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/multiplayer-games/battleship/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>