# 🎮 SerpentRace Frontend Developer Guide ## 📋 Table of Contents 1. [Quick Start](#-quick-start) 2. [Authentication System](#-authentication-system) 3. [Game Integration](#-game-integration) 4. [API Reference](#-api-reference) 5. [WebSocket Events](#-websocket-events) 6. [Data Models](#-data-models) 7. [Error Handling](#-error-handling) 8. [Performance Tips](#-performance-tips) 9. [Security Guidelines](#-security-guidelines) 10. [Troubleshooting](#-troubleshooting) --- ## 🚀 Quick Start ### **Base Configuration** ```typescript // config.ts export const API_CONFIG = { baseURL: 'http://localhost:3000/api', wsURL: 'http://localhost:3000', timeout: 10000, retryAttempts: 3 }; ``` ### **API Client Setup** ```typescript // apiClient.ts import axios from 'axios'; const apiClient = axios.create({ baseURL: API_CONFIG.baseURL, timeout: API_CONFIG.timeout, withCredentials: true, // Important for cookie-based auth headers: { 'Content-Type': 'application/json' } }); // Request interceptor for auth token apiClient.interceptors.request.use((config) => { const token = localStorage.getItem('auth_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // Response interceptor for token refresh apiClient.interceptors.response.use( (response) => response, async (error) => { if (error.response?.status === 401) { // Handle token expiration localStorage.removeItem('auth_token'); window.location.href = '/login'; } return Promise.reject(error); } ); ``` --- ## 🔐 Authentication System ### **1. User Registration** ```typescript interface RegisterRequest { username: string; email: string; password: string; fname?: string; lname?: string; phone?: string; } async function registerUser(userData: RegisterRequest) { const response = await apiClient.post('/users/create', userData); return response.data; // Returns user data without password } ``` ### **2. User Login** ```typescript interface LoginRequest { username: string; password: string; } interface LoginResponse { token: string; user: { id: string; username: string; email: string; state: number; // 0=NOT_VERIFIED, 1=VERIFIED_REGULAR, 2=VERIFIED_PREMIUM, 3=ADMIN orgId?: string; }; } async function loginUser(credentials: LoginRequest): Promise { const response = await apiClient.post('/users/login', credentials); // Store token for future requests localStorage.setItem('auth_token', response.data.token); return response.data; } ``` ### **3. Token Management** ```typescript class AuthManager { private token: string | null = null; setToken(token: string) { this.token = token; localStorage.setItem('auth_token', token); } getToken(): string | null { return this.token || localStorage.getItem('auth_token'); } clearToken() { this.token = null; localStorage.removeItem('auth_token'); } isAuthenticated(): boolean { return !!this.getToken(); } } export const authManager = new AuthManager(); ``` --- ## 🎮 Game Integration ### **1. Create Game** ```typescript interface CreateGameRequest { deckids: string[]; // Array of deck UUIDs maxplayers: number; // 2-8 players logintype: number; // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION } interface GameResponse { id: string; gamecode: string; // 6-character join code maxplayers: number; state: number; // 0=WAITING, 1=ACTIVE, 2=FINISHED, 3=CANCELLED players: string[]; gameToken?: string; // For immediate joining } async function createGame(gameData: CreateGameRequest): Promise { const response = await apiClient.post('/games/start', gameData); return response.data; } ``` ### **2. Join Game** ```typescript interface JoinGameRequest { gameCode: string; // 6-character code playerName?: string; // Required for public games, optional for authenticated } interface JoinGameResponse extends GameResponse { gameToken: string; // Use this for WebSocket authentication playerName: string; isGamemaster: boolean; pendingApproval?: boolean; // True for private games awaiting approval } async function joinGame(joinData: JoinGameRequest): Promise { const response = await apiClient.post('/games/join', joinData); return response.data; } ``` ### **3. WebSocket Game Connection** ```typescript import io, { Socket } from 'socket.io-client'; class GameClient { private gameSocket: Socket | null = null; private gameToken: string = ''; private eventListeners = new Map(); async connectToGame(gameToken: string): Promise { this.gameToken = gameToken; // Connect to game namespace this.gameSocket = io('/game', { transports: ['websocket'] }); this.setupEventHandlers(); // Join specific game with token this.gameSocket.emit('game:join', { gameToken }); return new Promise((resolve, reject) => { this.gameSocket!.once('game:joined', (data) => { console.log('Successfully joined game:', data); resolve(); }); this.gameSocket!.once('game:error', (error) => { console.error('Game connection error:', error); reject(new Error(error.message)); }); }); } private setupEventHandlers() { if (!this.gameSocket) return; // Game state updates this.addListener('game:state-update', (gameState) => { console.log('Game state updated:', gameState); // Update UI with new game state }); // Player movements this.addListener('game:player-moved', (moveData) => { console.log('Player moved:', moveData); // Update board visualization }); // Field effects this.addListener('game:field-effect', (effectData) => { console.log('Field effect triggered:', effectData); // Show effect animation/notification }); // Chat messages this.addListener('game:chat-message', (chatData) => { console.log('Game chat:', chatData); // Display chat message }); } addListener(event: string, handler: Function) { if (!this.gameSocket) return; this.gameSocket.on(event, handler); this.eventListeners.set(event, handler); } removeAllListeners() { this.eventListeners.forEach((handler, event) => { this.gameSocket?.off(event, handler); }); this.eventListeners.clear(); } rollDice(diceValue: number) { if (!this.gameSocket) return; this.gameSocket.emit('game:dice-roll', { gameCode: this.gameCode, // Extract from gameToken diceValue }); } sendChatMessage(message: string) { if (!this.gameSocket) return; this.gameSocket.emit('game:chat', { gameCode: this.gameCode, message }); } disconnect() { this.removeAllListeners(); this.gameSocket?.disconnect(); this.gameSocket = null; } } ``` ### **4. Private Game Approval Flow** ```typescript // For gamemaster - handle approval requests gameSocket.on('game:player-requesting-join', (data) => { console.log('Player requesting to join:', data); // Show approval UI with player name showApprovalDialog(data.playerName, data.gameCode); }); function approvePlayer(gameCode: string, playerName: string) { gameSocket.emit('game:approve-player', { gameCode, playerName }); } function rejectPlayer(gameCode: string, playerName: string, reason?: string) { gameSocket.emit('game:reject-player', { gameCode, playerName, reason }); } // For joining player - handle approval response gameSocket.on('game:pending-approval', (data) => { console.log('Waiting for gamemaster approval:', data); // Show waiting message }); gameSocket.on('game:approval-granted', (data) => { console.log('Approved! Joining game:', data); // Automatically join game rooms gameSocket.emit('game:join-approved', { gameToken }); }); gameSocket.on('game:approval-denied', (data) => { console.log('Join request denied:', data); // Show rejection message and reason }); ``` --- ## 📡 API Reference ### **User Endpoints** | Endpoint | Method | Auth | Description | |----------|---------|------|-------------| | `/users/login` | POST | No | User authentication | | `/users/create` | POST | No | User registration | | `/users/logout` | POST | Yes | User logout | | `/users/profile` | GET | Yes | Get user profile | | `/users/profile` | PATCH | Yes | Update user profile | | `/users/verify-email` | POST | No | Verify email token | | `/users/request-password-reset` | POST | No | Request password reset | | `/users/reset-password` | POST | No | Reset password with token | ### **Game Endpoints** | Endpoint | Method | Auth | Description | |----------|---------|------|-------------| | `/games/start` | POST | Yes | Create new game | | `/games/join` | POST | Optional* | Join existing game | | `/games/{gameId}/start` | POST | Yes | Start game (gamemaster only) | | `/games/my-games` | GET | Yes | Get user's games | | `/games/active` | GET | No | Get active public games | *Auth required for private/organization games ### **Deck Endpoints** | Endpoint | Method | Auth | Description | |----------|---------|------|-------------| | `/decks` | GET | Optional | Get available decks | | `/decks` | POST | Yes | Create new deck | | `/decks/{id}` | GET | Optional | Get deck details | | `/decks/{id}` | PUT | Yes | Update deck (owner only) | | `/decks/{id}` | DELETE | Yes | Delete deck (owner only) | ### **Organization Endpoints** | Endpoint | Method | Auth | Description | |----------|---------|------|-------------| | `/organizations` | GET | Yes | Get user's organization | | `/organizations/{id}/join` | POST | Yes | Request to join organization | --- ## 🔌 WebSocket Events ### **Connection Events** ```typescript // Connect to main chat namespace const chatSocket = io('/', { auth: { token: authToken }, transports: ['websocket'] }); // Connect to game namespace const gameSocket = io('/game', { transports: ['websocket'] }); ``` ### **Game Events (Client → Server)** | Event | Data | Description | |-------|------|-------------| | `game:join` | `{ gameToken: string }` | Join game with token | | `game:leave` | `{ gameCode: string }` | Leave current game | | `game:dice-roll` | `{ gameCode: string, diceValue: number }` | Roll dice (1-6) | | `game:chat` | `{ gameCode: string, message: string }` | Send chat message | | `game:ready` | `{ gameCode: string, ready: boolean }` | Toggle ready status | | `game:approve-player` | `{ gameCode: string, playerName: string }` | Approve join request | | `game:reject-player` | `{ gameCode: string, playerName: string, reason?: string }` | Reject join request | ### **Game Events (Server → Client)** | Event | Data | Description | |-------|------|-------------| | `game:joined` | `GameJoinedData` | Successfully joined game | | `game:left` | `GameLeftData` | Successfully left game | | `game:player-moved` | `PlayerMoveData` | Player moved on board | | `game:field-effect` | `FieldEffectData` | Field effect triggered | | `game:chat-message` | `ChatMessageData` | Game chat message | | `game:state-update` | `GameStateData` | Game state changed | | `game:player-joined` | `PlayerJoinedData` | New player joined | | `game:player-left` | `PlayerLeftData` | Player left game | | `game:game-started` | `GameStartedData` | Game started | | `game:game-ended` | `GameEndedData` | Game finished | | `game:error` | `{ message: string }` | Game-related error | --- ## 📊 Data Models ### **Game State Model** ```typescript interface GameState { gameId: string; gameCode: string; state: GameStateEnum; // 0=WAITING, 1=ACTIVE, 2=FINISHED, 3=CANCELLED maxPlayers: number; currentPlayers: PlayerState[]; gamemaster: string; // User ID board: BoardField[]; currentTurn?: string; // Player ID whose turn it is turnOrder: string[]; // Player IDs in turn sequence startedAt?: Date; finishedAt?: Date; winner?: string; // Player ID } interface PlayerState { playerId: string; playerName: string; boardPosition: number; // 0-101 (0=start, 101=finish) isReady: boolean; isOnline: boolean; joinedAt: Date; turnOrder: number; } interface BoardField { position: number; // 1-100 type: 'regular' | 'positive' | 'negative' | 'luck'; effect?: string; // Description of field effect } ``` ### **Move Data Model** ```typescript interface PlayerMoveData { playerId: string; playerName: string; diceValue: number; oldPosition: number; newPosition: number; hasWon: boolean; cardEffect?: { applied: boolean; description: string; positionChange: number; extraTurn: boolean; turnEffect?: 'LOSE_TURN' | 'EXTRA_TURN'; effects: string[]; }; timestamp: string; } ``` ### **Field Effect Model** ```typescript interface FieldEffectData { playerId: string; playerName: string; fieldNumber: number; card?: GameCard; consequence?: { type: ConsequenceType; value?: number; description: string; }; newPosition?: number; turnEffect?: 'LOSE_TURN' | 'EXTRA_TURN'; requiresInput?: boolean; inputPrompt?: string; timestamp: string; } interface GameCard { id: string; text: string; // Question or content type: CardType; // 0=QUIZ, 1=SENTENCE_PAIRING, 2=OWN_ANSWER, 3=TRUE_FALSE, 4=CLOSER answer?: string; consequence?: { type: ConsequenceType; // 0=MOVE_FORWARD, 1=MOVE_BACKWARD, 2=LOSE_TURN, 3=EXTRA_TURN, 5=GO_TO_START value?: number; }; } ``` --- ## ⚠️ Error Handling ### **API Error Response Format** ```typescript interface APIError { error: string; details?: string; code?: string; timestamp?: string; } // Common HTTP Status Codes // 400 - Bad Request (validation errors) // 401 - Unauthorized (authentication required) // 403 - Forbidden (insufficient permissions) // 404 - Not Found // 409 - Conflict (duplicate data) // 500 - Internal Server Error ``` ### **Error Handling Pattern** ```typescript async function handleAPICall(apiCall: () => Promise): Promise { try { return await apiCall(); } catch (error) { if (axios.isAxiosError(error)) { const response = error.response; switch (response?.status) { case 400: throw new Error(`Validation Error: ${response.data.error}`); case 401: // Handle authentication error authManager.clearToken(); window.location.href = '/login'; throw new Error('Authentication required'); case 403: throw new Error(`Access Denied: ${response.data.error}`); case 404: throw new Error('Resource not found'); case 409: throw new Error(`Conflict: ${response.data.error}`); case 500: throw new Error('Server error. Please try again later.'); default: throw new Error(`Network error: ${error.message}`); } } throw error; } } // Usage try { const user = await handleAPICall(() => loginUser(credentials)); console.log('Login successful:', user); } catch (error) { console.error('Login failed:', error.message); showErrorMessage(error.message); } ``` ### **WebSocket Error Handling** ```typescript gameSocket.on('game:error', (error) => { console.error('Game error:', error); switch (error.message) { case 'Game not found': showError('The game you\'re trying to join no longer exists.'); break; case 'Game is full': showError('This game is full. Please try another game.'); break; case 'Invalid or expired game token': showError('Your game session has expired. Please rejoin.'); break; default: showError(`Game error: ${error.message}`); } }); gameSocket.on('disconnect', (reason) => { console.log('Disconnected from game:', reason); if (reason === 'io server disconnect') { // Server disconnected the client showError('Disconnected from game server'); } else { // Client disconnected or network issue showWarning('Connection lost. Attempting to reconnect...'); } }); ``` --- ## 🚀 Performance Optimization ### **1. Connection Management** ```typescript class ConnectionManager { private static chatSocket: Socket | null = null; private static gameSocket: Socket | null = null; static getChatSocket(): Socket { if (!this.chatSocket) { this.chatSocket = io('/', { auth: { token: authManager.getToken() }, transports: ['websocket'] }); } return this.chatSocket; } static getGameSocket(): Socket { if (!this.gameSocket) { this.gameSocket = io('/game', { transports: ['websocket'] }); } return this.gameSocket; } static disconnect() { this.chatSocket?.disconnect(); this.gameSocket?.disconnect(); this.chatSocket = null; this.gameSocket = null; } } ``` ### **2. Event Listener Cleanup** ```typescript class GameComponent { private eventCleanup: (() => void)[] = []; componentDidMount() { const gameSocket = ConnectionManager.getGameSocket(); // Track listeners for cleanup const addListener = (event: string, handler: Function) => { gameSocket.on(event, handler); this.eventCleanup.push(() => gameSocket.off(event, handler)); }; addListener('game:player-moved', this.handlePlayerMove); addListener('game:state-update', this.handleStateUpdate); } componentWillUnmount() { // Clean up all event listeners this.eventCleanup.forEach(cleanup => cleanup()); this.eventCleanup = []; } } ``` ### **3. API Caching** ```typescript class APICache { private cache = new Map(); async get(key: string, fetcher: () => Promise, ttl = 300000): Promise { const cached = this.cache.get(key); if (cached && Date.now() - cached.timestamp < cached.ttl) { return cached.data; } const data = await fetcher(); this.cache.set(key, { data, timestamp: Date.now(), ttl }); return data; } invalidate(pattern?: string) { if (pattern) { for (const key of this.cache.keys()) { if (key.includes(pattern)) { this.cache.delete(key); } } } else { this.cache.clear(); } } } const apiCache = new APICache(); // Usage const decks = await apiCache.get( 'available-decks', () => apiClient.get('/decks').then(res => res.data), 300000 // 5 minutes ); ``` --- ## 🔒 Security Guidelines ### **1. Token Security** ```typescript // ❌ DON'T: Store tokens in localStorage for sensitive apps localStorage.setItem('auth_token', token); // ✅ DO: Use secure, httpOnly cookies when possible // This requires server-side cookie configuration // ✅ DO: Clear tokens on logout function logout() { localStorage.removeItem('auth_token'); apiCache.invalidate(); ConnectionManager.disconnect(); window.location.href = '/login'; } ``` ### **2. Input Validation** ```typescript function validateGameCode(gameCode: string): boolean { // Game codes are exactly 6 alphanumeric characters return /^[A-Z0-9]{6}$/.test(gameCode); } function validatePlayerName(playerName: string): boolean { // Player names: 3-50 characters, alphanumeric + spaces return /^[a-zA-Z0-9\s]{3,50}$/.test(playerName.trim()); } function sanitizeMessage(message: string): string { // Remove HTML tags and limit length return message .replace(/<[^>]*>/g, '') .substring(0, 500) .trim(); } ``` ### **3. Error Message Security** ```typescript // ❌ DON'T: Expose sensitive information console.error('Database error:', fullErrorDetails); // ✅ DO: Log safely and show user-friendly messages function handleError(error: any) { console.error('API Error:', error.response?.status || 'Unknown'); const userMessage = error.response?.data?.error || 'An unexpected error occurred'; showUserMessage(userMessage); } ``` --- ## 🔧 Troubleshooting ### **Common Issues & Solutions** #### **1. WebSocket Connection Failed** ```typescript // Problem: Cannot connect to WebSocket // Solution: Check URL and add reconnection logic const gameSocket = io('/game', { transports: ['websocket'], timeout: 10000, forceNew: true, reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000 }); gameSocket.on('connect_error', (error) => { console.error('Connection failed:', error); showError('Unable to connect to game server. Please check your connection.'); }); ``` #### **2. Authentication Token Expired** ```typescript // Problem: 401 errors on API calls // Solution: Implement token refresh or redirect to login apiClient.interceptors.response.use( (response) => response, async (error) => { if (error.response?.status === 401) { console.log('Token expired, redirecting to login'); authManager.clearToken(); window.location.href = '/login'; } return Promise.reject(error); } ); ``` #### **3. Game State Out of Sync** ```typescript // Problem: Game state doesn't match server // Solution: Request fresh game state function requestGameStateRefresh(gameCode: string) { gameSocket.emit('game:request-state', { gameCode }); } gameSocket.on('game:state-refresh', (gameState) => { console.log('Received fresh game state:', gameState); updateGameUI(gameState); }); ``` #### **4. Memory Leaks in Game Component** ```typescript // Problem: Event listeners not cleaned up // Solution: Proper cleanup pattern useEffect(() => { const gameSocket = ConnectionManager.getGameSocket(); const handlers = { 'game:player-moved': handlePlayerMove, 'game:state-update': handleStateUpdate, 'game:chat-message': handleChatMessage }; // Add listeners Object.entries(handlers).forEach(([event, handler]) => { gameSocket.on(event, handler); }); // Cleanup function return () => { Object.entries(handlers).forEach(([event, handler]) => { gameSocket.off(event, handler); }); }; }, []); ``` --- ## 📞 Support & Documentation ### **Additional Resources** - **API Documentation**: Available at `/api-docs` (Swagger UI) - **WebSocket Events**: Complete event reference in game-websocket-examples.ts - **Backend Repository**: Full source code and additional documentation ### **Development Tips** 1. Use browser dev tools Network tab to debug API calls 2. Enable WebSocket debugging: `localStorage.debug = 'socket.io-client:socket'` 3. Check server logs for detailed error information 4. Use the included Postman collection for API testing ### **Performance Monitoring** - Monitor WebSocket connection status - Track API response times - Watch for memory leaks in game components - Monitor token refresh frequency --- *Last updated: September 21, 2025* *Backend Version: 1.0.0* *API Version: v1*