#!/usr/bin/env node const WebSocket = require('ws'); const readline = require('readline'); /** * Interactive Command Line Client for Game Server * Provides a full-featured CLI interface for testing server functionality */ class CLIClient { constructor() { this.ws = null; this.isConnected = false; this.user = null; this.currentRoom = null; this.rooms = []; this.roomUsers = []; this.rl = null; this.commandHistory = []; this.autoReconnect = false; this.setupReadline(); this.setupCommands(); } setupReadline() { this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ', historySize: 100 }); this.rl.on('line', (input) => { this.handleCommand(input.trim()); }); this.rl.on('SIGINT', () => { this.disconnect(); process.exit(0); }); } setupCommands() { this.commands = { // Connection commands connect: { description: 'Connect to server', usage: 'connect [avatar]', handler: this.cmdConnect.bind(this) }, disconnect: { description: 'Disconnect from server', usage: 'disconnect', handler: this.cmdDisconnect.bind(this) }, status: { description: 'Show connection status', usage: 'status', handler: this.cmdStatus.bind(this) }, // Room commands rooms: { description: 'List all rooms', usage: 'rooms', handler: this.cmdRooms.bind(this) }, join: { description: 'Join a room', usage: 'join ', handler: this.cmdJoin.bind(this) }, leave: { description: 'Leave current room', usage: 'leave', handler: this.cmdLeave.bind(this) }, create: { description: 'Create a new room', usage: 'create [type] [maxUsers]', handler: this.cmdCreate.bind(this) }, users: { description: 'List users in current room', usage: 'users', handler: this.cmdUsers.bind(this) }, // Chat commands say: { description: 'Send a chat message', usage: 'say ', handler: this.cmdSay.bind(this) }, msg: { description: 'Alias for say', usage: 'msg ', handler: this.cmdSay.bind(this) }, // Game commands ready: { description: 'Mark yourself as ready for game', usage: 'ready', handler: this.cmdReady.bind(this) }, move: { description: 'Make a game move', usage: 'move [data]', handler: this.cmdMove.bind(this) }, reset: { description: 'Reset the game', usage: 'reset', handler: this.cmdReset.bind(this) }, // Utility commands help: { description: 'Show this help message', usage: 'help [command]', handler: this.cmdHelp.bind(this) }, clear: { description: 'Clear the screen', usage: 'clear', handler: this.cmdClear.bind(this) }, history: { description: 'Show command history', usage: 'history', handler: this.cmdHistory.bind(this) }, echo: { description: 'Echo text', usage: 'echo ', handler: this.cmdEcho.bind(this) }, raw: { description: 'Send raw JSON message', usage: 'raw ', handler: this.cmdRaw.bind(this) }, // Auto commands auto: { description: 'Run automated test sequence', usage: 'auto [scenario]', handler: this.cmdAuto.bind(this) } }; // Add aliases this.commands.h = this.commands.help; this.commands.r = this.commands.rooms; this.commands.j = this.commands.join; this.commands.l = this.commands.leave; this.commands.c = this.commands.connect; this.commands.d = this.commands.disconnect; this.commands.s = this.commands.say; this.commands.u = this.commands.users; } start() { this.showWelcome(); this.rl.prompt(); } showWelcome() { console.log('\nšŸŽ® Game Server CLI Client'); console.log('═══════════════════════════════'); console.log('Type "help" for available commands'); console.log('Type "connect " to get started'); console.log('Ctrl+C to exit\n'); } handleCommand(input) { if (!input) { this.rl.prompt(); return; } // Add to history this.commandHistory.push(input); if (this.commandHistory.length > 50) { this.commandHistory.shift(); } const [command, ...args] = input.split(' '); const cmd = this.commands[command.toLowerCase()]; if (cmd) { try { cmd.handler(args); } catch (error) { this.error(`Error executing command: ${error.message}`); } } else { this.error(`Unknown command: ${command}. Type "help" for available commands.`); } this.rl.prompt(); } // Connection Commands cmdConnect(args) { if (this.isConnected) { this.warning('Already connected. Use "disconnect" first.'); return; } const username = args[0]; const avatar = args[1] || 'šŸ¤–'; if (!username) { this.error('Usage: connect [avatar]'); return; } this.connect(username, avatar); } cmdDisconnect() { if (!this.isConnected) { this.warning('Not connected.'); return; } this.disconnect(); } cmdStatus() { if (this.isConnected) { this.info(`Connected as: ${this.user?.name} ${this.user?.avatar}`); this.info(`Current room: ${this.currentRoom?.name || 'None'}`); this.info(`Available rooms: ${this.rooms.length}`); this.info(`Users in room: ${this.roomUsers.length}`); } else { this.info('Disconnected'); } } // Room Commands cmdRooms() { if (!this.isConnected) { this.error('Not connected.'); return; } if (this.rooms.length === 0) { this.info('No rooms available.'); return; } console.log('\nšŸ“‹ Available Rooms:'); console.log('─'.repeat(60)); this.rooms.forEach(room => { const current = this.currentRoom?.id === room.id ? ' [CURRENT]' : ''; const gameStatus = room.hasGame ? ` (${room.gameStatus})` : ''; console.log(`${room.id.padEnd(12)} | ${room.name.padEnd(20)} | ${room.type.padEnd(8)} | ${room.userCount}/${room.maxUsers}${gameStatus}${current}`); }); console.log(''); } cmdJoin(args) { if (!this.isConnected) { this.error('Not connected.'); return; } const roomId = args[0]; if (!roomId) { this.error('Usage: join '); return; } this.send({ type: 'join-room', roomId: roomId }); } cmdLeave() { if (!this.currentRoom) { this.warning('Not in any room.'); return; } this.send({ type: 'leave-room', roomId: this.currentRoom.id }); } cmdCreate(args) { if (!this.isConnected) { this.error('Not connected.'); return; } const name = args[0]; const type = args[1] || 'chat'; const maxUsers = parseInt(args[2]) || (type === 'game' ? 4 : 20); if (!name) { this.error('Usage: create [type] [maxUsers]'); return; } this.send({ type: 'create-room', name: name, type: type, maxUsers: maxUsers }); } cmdUsers() { if (!this.currentRoom) { this.warning('Not in any room.'); return; } if (this.roomUsers.length === 0) { this.info('No users in room.'); return; } console.log(`\nšŸ‘„ Users in ${this.currentRoom.name}:`); console.log('─'.repeat(30)); this.roomUsers.forEach((user, index) => { const you = user.id === this.user?.id ? ' (you)' : ''; console.log(`${(index + 1).toString().padStart(2)}. ${user.avatar} ${user.name}${you}`); }); console.log(''); } // Chat Commands cmdSay(args) { if (!this.currentRoom) { this.error('Not in any room.'); return; } const message = args.join(' '); if (!message) { this.error('Usage: say '); return; } this.send({ type: 'chat', roomId: this.currentRoom.id, text: message }); } // Game Commands cmdReady() { if (!this.currentRoom || !this.currentRoom.hasGame) { this.error('Not in a game room.'); return; } this.send({ type: 'game-action', roomId: this.currentRoom.id, action: 'ready' }); } cmdMove(args) { if (!this.currentRoom || !this.currentRoom.hasGame) { this.error('Not in a game room.'); return; } let data = { action: 'test' }; if (args.length > 0) { try { data = JSON.parse(args.join(' ')); } catch { data = { move: args.join(' ') }; } } this.send({ type: 'game-action', roomId: this.currentRoom.id, action: 'move', data: data }); } cmdReset() { if (!this.currentRoom || !this.currentRoom.hasGame) { this.error('Not in a game room.'); return; } this.send({ type: 'game-action', roomId: this.currentRoom.id, action: 'reset' }); } // Utility Commands cmdHelp(args) { const command = args[0]; if (command && this.commands[command]) { const cmd = this.commands[command]; console.log(`\nšŸ“– ${command}:`); console.log(` ${cmd.description}`); console.log(` Usage: ${cmd.usage}\n`); return; } console.log('\nšŸ“– Available Commands:'); console.log('═'.repeat(50)); const categories = { 'Connection': ['connect', 'disconnect', 'status'], 'Rooms': ['rooms', 'join', 'leave', 'create', 'users'], 'Chat': ['say', 'msg'], 'Game': ['ready', 'move', 'reset'], 'Utility': ['help', 'clear', 'history', 'echo', 'raw', 'auto'] }; for (const [category, commands] of Object.entries(categories)) { console.log(`\n${category}:`); commands.forEach(cmdName => { if (this.commands[cmdName]) { const cmd = this.commands[cmdName]; console.log(` ${cmdName.padEnd(12)} - ${cmd.description}`); } }); } console.log('\nAliases: h=help, r=rooms, j=join, l=leave, c=connect, d=disconnect, s=say, u=users'); console.log('Type "help " for detailed usage.\n'); } cmdClear() { console.clear(); this.showWelcome(); } cmdHistory() { if (this.commandHistory.length === 0) { this.info('No command history.'); return; } console.log('\nšŸ“œ Command History:'); console.log('─'.repeat(30)); this.commandHistory.slice(-10).forEach((cmd, index) => { console.log(`${(index + 1).toString().padStart(2)}. ${cmd}`); }); console.log(''); } cmdEcho(args) { console.log(args.join(' ')); } cmdRaw(args) { if (!this.isConnected) { this.error('Not connected.'); return; } try { const json = JSON.parse(args.join(' ')); this.send(json); this.info('Raw message sent.'); } catch (error) { this.error('Invalid JSON: ' + error.message); } } cmdAuto(args) { const scenario = args[0] || 'basic'; this.runAutoTest(scenario); } // WebSocket Connection connect(username, avatar) { this.info('Connecting to ws://localhost:8080...'); try { this.ws = new WebSocket('ws://localhost:8080'); this.ws.on('open', () => { this.isConnected = true; this.success('Connected to server!'); // Authenticate this.send({ type: 'auth', name: username, avatar: avatar }); }); this.ws.on('message', (data) => { const message = JSON.parse(data); this.handleMessage(message); }); this.ws.on('close', () => { this.isConnected = false; this.warning('Disconnected from server.'); this.resetState(); }); this.ws.on('error', (error) => { this.error('Connection error: ' + error.message); }); } catch (error) { this.error('Failed to connect: ' + error.message); } } disconnect() { if (this.ws) { this.ws.close(); } this.resetState(); this.info('Disconnected.'); } send(message) { if (this.ws && this.isConnected) { this.ws.send(JSON.stringify(message)); } } resetState() { this.user = null; this.currentRoom = null; this.rooms = []; this.roomUsers = []; } // Message Handling handleMessage(message) { switch (message.type) { case 'welcome': this.rooms = message.rooms; break; case 'auth-success': this.user = message.user; this.rooms = message.rooms; this.success(`Authenticated as ${this.user.name} ${this.user.avatar}`); break; case 'joined-room': this.currentRoom = message.room; this.roomUsers = message.users; this.success(`Joined room: ${message.room.name}`); break; case 'left-room': if (this.currentRoom?.id === message.roomId) { this.info(`Left room: ${this.currentRoom.name}`); this.currentRoom = null; this.roomUsers = []; } break; case 'user-joined': if (this.currentRoom?.id === message.roomId) { this.roomUsers.push(message.user); this.info(`${message.user.avatar} ${message.user.name} joined the room`); } break; case 'user-left': if (this.currentRoom?.id === message.roomId) { this.roomUsers = this.roomUsers.filter(u => u.id !== message.user.id); this.info(`${message.user.avatar} ${message.user.name} left the room`); } break; case 'chat-message': if (this.currentRoom?.id === message.roomId) { const time = new Date(message.timestamp).toLocaleTimeString(); console.log(`\nšŸ’¬ [${time}] ${message.user.avatar} ${message.user.name}: ${message.text}`); } break; case 'game-state': this.handleGameState(message); break; case 'game-started': if (this.currentRoom?.id === message.roomId) { this.success('šŸŽ® Game started!'); } break; case 'room-created': this.rooms.push(message.room); this.success(`Room created: ${message.room.name}`); break; case 'announcement': this.announcement(`šŸ“¢ ${message.message}`); break; case 'error': this.error(`Server error: ${message.message}`); break; case 'kicked': this.error(`āŒ Kicked: ${message.reason}`); setTimeout(() => this.disconnect(), 1000); break; default: this.debug(`Unknown message type: ${message.type}`); } this.rl.prompt(); } handleGameState(message) { if (this.currentRoom?.id === message.roomId) { const gameState = message.gameState; let statusText = `šŸŽ® Game Status: ${gameState.status}`; if (gameState.turn) { const turnUser = gameState.players.find(p => p.userId === gameState.turn); if (turnUser) { statusText += ` - ${turnUser.user.name}'s turn`; } } this.info(statusText); } } // Auto Testing async runAutoTest(scenario) { this.info(`šŸ¤– Running automated test: ${scenario}`); switch (scenario) { case 'basic': await this.runBasicTest(); break; case 'chat': await this.runChatTest(); break; case 'game': await this.runGameTest(); break; default: this.error('Unknown scenario. Available: basic, chat, game'); } } async runBasicTest() { const steps = [ () => this.handleCommand('status'), () => this.handleCommand('connect TestBot'), () => this.sleep(1000), () => this.handleCommand('rooms'), () => this.handleCommand('join lobby'), () => this.sleep(500), () => this.handleCommand('users'), () => this.handleCommand('say Hello from automated test!'), () => this.sleep(1000), () => this.handleCommand('leave'), () => this.handleCommand('disconnect') ]; for (const step of steps) { await step(); } this.success('āœ… Basic test completed!'); } async runChatTest() { this.info('Starting chat test...'); // Add chat-specific test logic here } async runGameTest() { this.info('Starting game test...'); // Add game-specific test logic here } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Output Utilities info(message) { console.log(`\n ā„¹ļø ${message}`); } success(message) { console.log(`\n āœ… ${message}`); } warning(message) { console.log(`\n āš ļø ${message}`); } error(message) { console.log(`\n āŒ ${message}`); } announcement(message) { console.log(`\n šŸ“¢ ${message}`); } debug(message) { console.log(`\n šŸ› ${message}`); } } // Main execution if (require.main === module) { const client = new CLIClient(); client.start(); } module.exports = { CLIClient };