86211923db
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
392 lines
14 KiB
TypeScript
392 lines
14 KiB
TypeScript
/**
|
|
* GameWebSocketService Usage Examples
|
|
*
|
|
* This file demonstrates how to use the GameWebSocketService with the new
|
|
* game token authentication system and private game approval workflow.
|
|
*
|
|
* BOARD STRUCTURE:
|
|
* - Starting position: 0 (before the board)
|
|
* - Gameplay board: positions 1-100
|
|
* - Winning position: 101 (finish line)
|
|
* - Field types: 'regular', 'positive', 'negative', 'luck' (special effects to be implemented later)
|
|
*/
|
|
|
|
import { gameWebSocketService } from './src/Api/index';
|
|
|
|
// Example 1: Frontend WebSocket Connection with Game Tokens
|
|
/*
|
|
const gameSocket = io('/game');
|
|
|
|
// Step 1: Join game via REST API to get game token
|
|
const joinResponse = await fetch('/api/games/join', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
// Include authorization header if user is authenticated
|
|
'Authorization': 'Bearer jwt-token-here' // Optional for public games
|
|
},
|
|
body: JSON.stringify({
|
|
gameCode: 'ABC123',
|
|
playerName: 'Player1' // Required for public games, optional for authenticated users
|
|
})
|
|
});
|
|
|
|
const gameData = await joinResponse.json();
|
|
const gameToken = gameData.gameToken; // Game session token from REST API
|
|
|
|
// Step 2: Join WebSocket room using the game token
|
|
gameSocket.emit('game:join', {
|
|
gameToken: gameToken // Single token contains all game session info
|
|
});
|
|
|
|
// Listen for game events
|
|
gameSocket.on('game:joined', (data) => {
|
|
console.log('Successfully joined game:', data);
|
|
// { gameCode: 'ABC123', playerName: 'Player1', isAuthenticated: false, gameId: 'uuid', isGamemaster: false, timestamp: '...' }
|
|
});
|
|
|
|
// PRIVATE GAME APPROVAL WORKFLOW:
|
|
gameSocket.on('game:pending-approval', (data) => {
|
|
console.log('Waiting for gamemaster approval:', data);
|
|
// Show waiting message to player
|
|
});
|
|
|
|
gameSocket.on('game:approval-granted', (data) => {
|
|
console.log('Approved! Now joining game rooms:', data);
|
|
// Re-emit with special approved join event
|
|
gameSocket.emit('game:join-approved', {
|
|
gameToken: gameToken
|
|
});
|
|
});
|
|
|
|
gameSocket.on('game:approval-denied', (data) => {
|
|
console.log('Join request denied:', data);
|
|
// Show rejection message and reason
|
|
});
|
|
|
|
// Gamemaster events (private games only)
|
|
gameSocket.on('game:player-requesting-join', (data) => {
|
|
console.log('Player requesting to join:', data);
|
|
// Show approval/reject buttons to gamemaster
|
|
});
|
|
|
|
gameSocket.on('game:state-update', (gameState) => {
|
|
console.log('Game state updated:', gameState);
|
|
// gameState.pendingPlayers array available for private games
|
|
});
|
|
|
|
gameSocket.on('game:player-specific-event', (data) => {
|
|
console.log('Event sent specifically to me:', data);
|
|
});
|
|
*/
|
|
|
|
// Example 1.5: Gamemaster Controls (Private Games Only)
|
|
/*
|
|
// Approve a pending player
|
|
function approvePlayer(gameCode: string, playerName: string) {
|
|
gameSocket.emit('game:approve-player', {
|
|
gameCode: gameCode,
|
|
playerName: playerName
|
|
});
|
|
}
|
|
|
|
// Reject a pending player
|
|
function rejectPlayer(gameCode: string, playerName: string, reason?: string) {
|
|
gameSocket.emit('game:reject-player', {
|
|
gameCode: gameCode,
|
|
playerName: playerName,
|
|
reason: reason || 'Request denied by gamemaster'
|
|
});
|
|
}
|
|
|
|
// Example UI for gamemaster approval
|
|
gameSocket.on('game:state', (gameState) => {
|
|
if (gameState.pendingPlayers && gameState.pendingPlayers.length > 0) {
|
|
console.log('Pending players awaiting approval:', gameState.pendingPlayers);
|
|
// Display approval UI for each pending player:
|
|
// [Approve] [Reject] PlayerName
|
|
}
|
|
});
|
|
*/
|
|
|
|
// Example 2: Backend Broadcasting (from game logic services)
|
|
export class GameLogicExample {
|
|
|
|
// Broadcast to all players in a game
|
|
async notifyAllPlayers(gameCode: string, message: string): Promise<void> {
|
|
await gameWebSocketService.broadcastGameEvent(gameCode, 'game:notification', {
|
|
message,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Send event to specific player
|
|
async notifyPlayer(gameCode: string, playerName: string, action: string, data: any): Promise<void> {
|
|
await gameWebSocketService.sendToPlayer(gameCode, playerName, 'game:player-action', {
|
|
action,
|
|
data,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Handle dice roll - broadcast to all, send specific result to player
|
|
async handleDiceRoll(gameCode: string, playerName: string, diceResult: number): Promise<void> {
|
|
// Broadcast that a player rolled dice
|
|
await gameWebSocketService.broadcastGameEvent(gameCode, 'game:dice-rolled', {
|
|
playerName,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Send specific dice result to the player
|
|
await gameWebSocketService.sendToPlayer(gameCode, playerName, 'game:dice-result', {
|
|
result: diceResult,
|
|
canMove: true,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Handle turn change - notify all players and give specific instructions to current player
|
|
async handleTurnChange(gameCode: string, currentPlayer: string, nextPlayer: string): Promise<void> {
|
|
// Broadcast turn change to all players
|
|
await gameWebSocketService.broadcastGameEvent(gameCode, 'game:turn-changed', {
|
|
previousPlayer: currentPlayer,
|
|
currentPlayer: nextPlayer,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Send specific "your turn" message to next player
|
|
await gameWebSocketService.sendToPlayer(gameCode, nextPlayer, 'game:your-turn', {
|
|
message: "It's your turn! Roll the dice when ready.",
|
|
actions: ['roll-dice'],
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Send "waiting" message to other players
|
|
const connectedPlayers = await gameWebSocketService.getConnectedPlayers(gameCode);
|
|
const waitingPlayers = connectedPlayers.filter((player: string) => player !== nextPlayer);
|
|
|
|
await gameWebSocketService.sendToPlayers(gameCode, waitingPlayers, 'game:waiting-turn', {
|
|
message: `Waiting for ${nextPlayer} to play...`,
|
|
currentPlayer: nextPlayer,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Handle field effects - different messages for different players
|
|
async handleFieldEffect(gameCode: string, playerName: string, fieldType: string, effect: any): Promise<void> {
|
|
// Broadcast the field activation to all players
|
|
await gameWebSocketService.broadcastGameEvent(gameCode, 'game:field-activated', {
|
|
playerName,
|
|
fieldType,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Send specific effect to the player who landed on the field
|
|
await gameWebSocketService.sendToPlayer(gameCode, playerName, 'game:field-effect', {
|
|
fieldType,
|
|
effect,
|
|
message: `You landed on a ${fieldType} field!`,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Handle game state monitoring
|
|
async checkGameStatus(gameCode: string): Promise<void> {
|
|
const connectedPlayers = await gameWebSocketService.getConnectedPlayers(gameCode);
|
|
const readyPlayers = await gameWebSocketService.getReadyPlayers(gameCode);
|
|
|
|
console.log(`Game ${gameCode} status:`);
|
|
console.log(`- Connected players: ${connectedPlayers.join(', ')}`);
|
|
console.log(`- Ready players: ${readyPlayers.join(', ')}`);
|
|
|
|
if (connectedPlayers.length === 0) {
|
|
console.log('- Game is empty');
|
|
} else if (readyPlayers.length === connectedPlayers.length) {
|
|
console.log('- All players are ready!');
|
|
await this.startGame(gameCode);
|
|
}
|
|
}
|
|
|
|
// Start game when all players are ready
|
|
async startGame(gameCode: string): Promise<void> {
|
|
await gameWebSocketService.broadcastGameEvent(gameCode, 'game:started', {
|
|
message: 'Game is starting! Get ready to play!',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Send game board and initial state to all players
|
|
const gameState = {
|
|
status: 'active',
|
|
currentPlayer: 'Player1', // Determine first player
|
|
board: {}, // Board data
|
|
players: await gameWebSocketService.getConnectedPlayers(gameCode)
|
|
};
|
|
|
|
await gameWebSocketService.broadcastGameStateUpdate(gameCode, gameState);
|
|
}
|
|
}
|
|
|
|
// Example 3: Room Structure
|
|
/*
|
|
Dynamic Room Names:
|
|
- game_ABC123 // All players in game ABC123
|
|
- game_ABC123:Player1 // Specific to Player1 in game ABC123
|
|
- game_ABC123:Player2 // Specific to Player2 in game ABC123
|
|
- game_XYZ789 // All players in game XYZ789
|
|
- game_XYZ789:PublicPlayer // Specific to PublicPlayer in game XYZ789
|
|
|
|
Usage:
|
|
- Broadcast events: Send to game_ABC123 (all players receive)
|
|
- Player-specific events: Send to game_ABC123:Player1 (only Player1 receives)
|
|
*/
|
|
|
|
// Example 4: Game Lifecycle Events
|
|
/*
|
|
// Game start event (broadcasted when gamemaster starts the game)
|
|
gameSocket.on('game:start', (data) => {
|
|
console.log('Game has started!', data);
|
|
// data includes:
|
|
// - gameCode: string
|
|
// - gameId: string
|
|
// - boardData: { fields: GameField[] } - Complete board layout (100 gameplay fields, positions 1-100)
|
|
// - playerOrder: string[] - Turn sequence (player IDs in order)
|
|
// - currentPlayer: string - First player to move
|
|
// - currentTurn: number - Current turn index (starts at 0)
|
|
// - players: string[] - All players in game
|
|
// - startedAt: string - ISO timestamp
|
|
// - message: 'Game has started! Good luck to all players!'
|
|
|
|
// Initialize game board UI
|
|
renderGameBoard(data.boardData.fields);
|
|
|
|
// Set up turn indicator
|
|
showCurrentPlayer(data.currentPlayer, data.playerOrder);
|
|
|
|
// Show start message
|
|
displayGameMessage(data.message);
|
|
});
|
|
|
|
// Turn notification for current player
|
|
gameSocket.on('game:your-turn', (data) => {
|
|
console.log('It\'s your turn!', data);
|
|
// data: { message: 'It\'s your turn! Roll the dice!', canRoll: true, timestamp: '...' }
|
|
|
|
// Enable dice roll button for current player
|
|
enableDiceRoll();
|
|
showTurnMessage(data.message);
|
|
});
|
|
|
|
// Turn change notification for all players
|
|
gameSocket.on('game:turn-changed', (data) => {
|
|
console.log('Turn changed:', data);
|
|
// data: { currentPlayer: 'id', currentPlayerName: 'Name', turnNumber: 2, message: '...', timestamp: '...' }
|
|
|
|
// Update UI to show whose turn it is
|
|
updateCurrentPlayerIndicator(data.currentPlayerName);
|
|
showTurnMessage(data.message);
|
|
});
|
|
|
|
// Player movement notification
|
|
gameSocket.on('game:player-moved', (data) => {
|
|
console.log('Player moved:', data);
|
|
// data: { playerId: 'id', playerName: 'Name', diceValue: 4, oldPosition: 15, newPosition: 19, hasWon: false, timestamp: '...' }
|
|
// Note: positions 0 (start) → 1-100 (gameplay board) → 101 (finish/win)
|
|
|
|
// Animate player movement on board
|
|
animatePlayerMovement(data.playerName, data.oldPosition, data.newPosition);
|
|
|
|
// Show dice result
|
|
showDiceResult(data.playerName, data.diceValue);
|
|
|
|
if (data.hasWon) {
|
|
showWinnerAnimation(data.playerName);
|
|
}
|
|
});
|
|
|
|
// Game end notification
|
|
gameSocket.on('game:ended', (data) => {
|
|
console.log('Game ended:', data);
|
|
// data: { winner: 'id', winnerName: 'Name', message: '🎉 Name won!', finalPositions: [...], timestamp: '...' }
|
|
|
|
// Show game over screen
|
|
showGameOverScreen(data.winnerName, data.finalPositions);
|
|
disableAllGameActions();
|
|
});
|
|
|
|
// Frontend dice roll (when it's your turn)
|
|
function rollDice() {
|
|
const diceValue = Math.floor(Math.random() * 6) + 1; // Generate 1-6
|
|
|
|
// Send dice value to server
|
|
gameSocket.emit('game:dice-roll', {
|
|
gameCode: currentGameCode,
|
|
diceValue: diceValue
|
|
});
|
|
|
|
// Disable dice roll button until turn changes
|
|
disableDiceRoll();
|
|
showDiceAnimation(diceValue);
|
|
}
|
|
|
|
// Other game events
|
|
gameSocket.on('game:state-update', (gameState) => {
|
|
console.log('Game state updated:', gameState);
|
|
});
|
|
|
|
gameSocket.on('game:action-result', (data) => {
|
|
console.log('Player action result:', data);
|
|
// { action: 'roll-dice', playerName: 'Player1', result: { dice: 4 }, timestamp: '...' }
|
|
});
|
|
*/
|
|
|
|
// Example 5: REST API Integration (Game Token Flow + Game Start)
|
|
/*
|
|
// Step 1: REST API handles game joining and returns game token
|
|
POST /api/games/join
|
|
{
|
|
"gameCode": "ABC123",
|
|
"playerName": "NewPlayer"
|
|
}
|
|
|
|
// Response includes game data + game token
|
|
{
|
|
"id": "game-uuid",
|
|
"gamecode": "ABC123",
|
|
"players": ["player1", "player2", "NewPlayer"],
|
|
...otherGameData,
|
|
"gameToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // Game session token
|
|
}
|
|
|
|
// Step 2: Player connects to WebSocket using the game token
|
|
const gameSocket = io('/game');
|
|
gameSocket.emit('game:join', {
|
|
gameToken: gameTokenFromRestAPI // Contains gameId, gameCode, playerName, auth status
|
|
});
|
|
|
|
// Step 3: Gamemaster starts the game via REST API
|
|
POST /api/games/{gameId}/start
|
|
// Authorization: Bearer {gamemaster-jwt-token}
|
|
|
|
// Response includes game and board data
|
|
{
|
|
"message": "Game started successfully",
|
|
"gameId": "game-uuid",
|
|
"playerCount": 3,
|
|
"game": { ...gameData },
|
|
"boardData": {
|
|
"fields": [
|
|
{ "position": 1, "type": "regular" },
|
|
{ "position": 2, "type": "positive", "stepValue": 3 },
|
|
{ "position": 3, "type": "negative", "stepValue": -2 },
|
|
{ "position": 4, "type": "luck" },
|
|
{ "position": 5, "type": "regular" },
|
|
// ... continues to position 100 (100 gameplay fields)
|
|
{ "position": 100, "type": "regular" }
|
|
]
|
|
// Note: Players start at 0, play on 1-100, win by reaching 101
|
|
}
|
|
}
|
|
|
|
// Step 4: All players automatically receive game:start WebSocket event
|
|
// (No additional frontend action needed - happens automatically when gamemaster calls start endpoint)
|
|
*/ |