UNPKG

@ursamu/core

Version:

Ursamu - Modular MUD Engine with sandboxed scripting and plugin system

1,679 lines (1,358 loc) 43.4 kB
# Command Development Guide This comprehensive guide covers everything you need to know about creating, testing, and deploying commands for Ursamu. ## Table of Contents - [Getting Started](#getting-started) - [Command Architecture](#command-architecture) - [Creating Basic Commands](#creating-basic-commands) - [Command Types](#command-types) - [Argument Parsing](#argument-parsing) - [Permission System](#permission-system) - [Context and State](#context-and-state) - [Error Handling](#error-handling) - [Advanced Features](#advanced-features) - [Testing Commands](#testing-commands) - [Best Practices](#best-practices) - [Performance Optimization](#performance-optimization) - [Debugging](#debugging) - [Command Templates](#command-templates) ## Getting Started ### Command System Overview The MUD engine provides a sophisticated command system that supports: - Dynamic command registration - Flexible argument parsing - Permission-based access control - Context-aware execution - Hot-reload capabilities - Comprehensive error handling ### Basic Command Structure Every command implements the `Command` interface: ```typescript interface Command { name: string; aliases?: string[]; description: string; usage: string; category?: string; permissions?: string[]; cooldown?: number; handler: CommandHandler; validator?: ArgumentValidator; autocomplete?: AutocompleteHandler; } type CommandHandler = (player: Player, args: string[], context: CommandContext) => Promise<CommandResult>; ``` ## Command Architecture ### Core Components ```typescript // Command Registry - Central command management interface CommandRegistry { register(command: Command): void; unregister(name: string): void; get(name: string): Command | undefined; getAll(): Command[]; findByAlias(alias: string): Command | undefined; } // Command Processor - Execution engine interface CommandProcessor { execute(player: Player, input: string): Promise<CommandResult>; parseCommand(input: string): ParsedCommand; validatePermissions(player: Player, command: Command): boolean; checkCooldown(player: Player, command: Command): boolean; } // Command Context - Execution environment interface CommandContext { player: Player; room: Room; world: World; engine: MUDEngine; timestamp: number; session: PlayerSession; } ``` ### Command Flow ``` Input Parse Lookup Validate Execute Response "look" "look" Command Perms Handler Result [] Object Check Call Message ``` ## Creating Basic Commands ### 1. Simple Information Command ```typescript import { Command, CommandHandler, CommandResult, Player } from '../types.js'; export const lookCommand: Command = { name: 'look', aliases: ['l', 'examine'], description: 'Look at your surroundings or examine an object', usage: 'look [target]', category: 'information', handler: lookHandler }; const lookHandler: CommandHandler = async (player, args, context) => { const target = args[0]; if (!target) { // Look at room const room = context.room; const description = formatRoomDescription(room); return { success: true, message: description }; } // Look at specific target const object = findTarget(target, context.room, player); if (!object) { return { success: false, message: `You don't see '${target}' here.` }; } return { success: true, message: object.getDescription() }; }; function formatRoomDescription(room: Room): string { let description = `${room.title}\n${room.description}\n`; // Add visible objects const objects = room.getVisibleObjects(); if (objects.length > 0) { description += '\nYou see: ' + objects.map(obj => obj.name).join(', '); } // Add other players const players = room.getPlayers().filter(p => p !== context.player); if (players.length > 0) { description += '\nPlayers here: ' + players.map(p => p.name).join(', '); } // Add exits const exits = room.getExits(); if (exits.length > 0) { description += '\nExits: ' + exits.join(', '); } return description; } ``` ### 2. Action Command with Validation ```typescript export const moveCommand: Command = { name: 'go', aliases: ['move', 'walk'], description: 'Move in a direction', usage: 'go <direction>', category: 'movement', handler: moveHandler, validator: validateMoveArgs }; const validateMoveArgs = (args: string[]): ArgumentValidationResult => { if (args.length === 0) { return { valid: false, error: 'You must specify a direction to move.' }; } const direction = args[0].toLowerCase(); const validDirections = ['north', 'south', 'east', 'west', 'up', 'down', 'ne', 'nw', 'se', 'sw']; if (!validDirections.includes(direction)) { return { valid: false, error: `'${direction}' is not a valid direction. Valid directions: ${validDirections.join(', ')}` }; } return { valid: true }; }; const moveHandler: CommandHandler = async (player, args, context) => { const direction = args[0].toLowerCase(); const currentRoom = context.room; // Check if exit exists const exit = currentRoom.getExit(direction); if (!exit) { return { success: false, message: `There is no exit ${direction} from here.` }; } // Check if exit is blocked if (exit.isBlocked && !exit.canPass(player)) { return { success: false, message: exit.blockedMessage || 'The way is blocked.' }; } // Get destination room const destinationRoom = context.world.getRoom(exit.destinationId); if (!destinationRoom) { return { success: false, message: 'Something is wrong with that exit.' }; } // Attempt to move try { await player.moveTo(destinationRoom); // Announce departure to current room currentRoom.broadcast(`${player.name} leaves ${direction}.`, player); // Announce arrival to destination room destinationRoom.broadcast(`${player.name} arrives.`, player); // Send new room description to player const newDescription = formatRoomDescription(destinationRoom); return { success: true, message: newDescription }; } catch (error) { return { success: false, message: 'You cannot move there right now.' }; } }; ``` ### 3. Interactive Command with Multiple Steps ```typescript export const craftCommand: Command = { name: 'craft', description: 'Craft items using materials', usage: 'craft <recipe> [quantity]', category: 'crafting', handler: craftHandler }; const craftHandler: CommandHandler = async (player, args, context) => { if (args.length === 0) { return showCraftingMenu(player); } const recipeName = args[0]; const quantity = parseInt(args[1]) || 1; // Find recipe const recipe = context.engine.recipeManager.getRecipe(recipeName); if (!recipe) { return { success: false, message: `Unknown recipe: ${recipeName}. Type 'craft' to see available recipes.` }; } // Check skill requirements if (!player.hasSkillLevel(recipe.skill, recipe.minLevel)) { return { success: false, message: `You need ${recipe.skill} level ${recipe.minLevel} to craft ${recipe.name}.` }; } // Check materials const missingMaterials = checkMaterials(player, recipe, quantity); if (missingMaterials.length > 0) { return { success: false, message: `Missing materials: ${missingMaterials.join(', ')}` }; } // Start crafting process return await startCrafting(player, recipe, quantity, context); }; async function startCrafting(player: Player, recipe: Recipe, quantity: number, context: CommandContext): Promise<CommandResult> { const totalTime = recipe.craftingTime * quantity; // Remove materials recipe.materials.forEach(material => { player.inventory.removeItem(material.item, material.quantity * quantity); }); // Start crafting timer player.setActivity('crafting', { recipe: recipe.name, quantity, timeRemaining: totalTime, startTime: Date.now() }); // Announce start context.room.broadcast(`${player.name} begins crafting ${recipe.name}.`); // Schedule completion setTimeout(async () => { await completeCrafting(player, recipe, quantity, context); }, totalTime); return { success: true, message: `You begin crafting ${recipe.name}. This will take ${formatTime(totalTime)}.` }; } ``` ## Command Types ### 1. Information Commands Provide information without changing game state: ```typescript export const whoCommand: Command = { name: 'who', aliases: ['users', 'players'], description: 'List online players', usage: 'who', category: 'information', handler: async (player, args, context) => { const onlinePlayers = context.engine.playerManager.getOnlinePlayers(); if (onlinePlayers.length === 0) { return { success: true, message: 'No other players are currently online.' }; } const playerList = onlinePlayers.map(p => { const level = p.getLevel(); const status = p.isIdle() ? '(idle)' : ''; return `${p.name} [${level}] ${status}`; }).join('\n'); return { success: true, message: `Online Players (${onlinePlayers.length}):\n${playerList}` }; } }; ``` ### 2. Action Commands Perform actions that change game state: ```typescript export const attackCommand: Command = { name: 'attack', aliases: ['att', 'kill', 'fight'], description: 'Attack a target', usage: 'attack <target>', category: 'combat', cooldown: 3000, // 3 second cooldown handler: async (player, args, context) => { const targetName = args[0]; if (!targetName) { return { success: false, message: 'Attack who?' }; } const target = findCombatTarget(targetName, context.room); if (!target) { return { success: false, message: `'${targetName}' is not here.` }; } if (target === player) { return { success: false, message: 'You cannot attack yourself!' }; } // Start combat const combatResult = await context.engine.combatManager.initiateCombat(player, target); return { success: true, message: combatResult.message }; } }; ``` ### 3. Communication Commands Handle player communication: ```typescript export const sayCommand: Command = { name: 'say', aliases: ["'"], description: 'Say something to everyone in the room', usage: 'say <message>', category: 'communication', handler: async (player, args, context) => { const message = args.join(' '); if (!message) { return { success: false, message: 'Say what?' }; } // Filter message for inappropriate content const filteredMessage = context.engine.chatFilter.filter(message); // Broadcast to room const fullMessage = `${player.name} says: ${filteredMessage}`; context.room.broadcast(fullMessage, player); // Confirm to sender return { success: true, message: `You say: ${filteredMessage}` }; } }; export const tellCommand: Command = { name: 'tell', aliases: ['whisper', 'pm'], description: 'Send private message to a player', usage: 'tell <player> <message>', category: 'communication', handler: async (player, args, context) => { if (args.length < 2) { return { success: false, message: 'Usage: tell <player> <message>' }; } const targetName = args[0]; const message = args.slice(1).join(' '); const target = context.engine.playerManager.findOnlinePlayer(targetName); if (!target) { return { success: false, message: `${targetName} is not online.` }; } if (target === player) { return { success: false, message: 'You talk to yourself quietly.' }; } // Check if target has blocked sender if (target.hasBlocked(player)) { return { success: false, message: 'Your message was not delivered.' }; } const filteredMessage = context.engine.chatFilter.filter(message); // Send to target target.send(`${player.name} tells you: ${filteredMessage}`); // Store last tell for reply target.setLastTeller(player); return { success: true, message: `You tell ${target.name}: ${filteredMessage}` }; } }; ``` ### 4. Administrative Commands Commands for admin/builder use: ```typescript export const gotoCommand: Command = { name: 'goto', description: 'Teleport to a room (admin only)', usage: 'goto <room_id>', category: 'admin', permissions: ['admin', 'builder'], handler: async (player, args, context) => { const roomId = args[0]; if (!roomId) { return { success: false, message: 'Usage: goto <room_id>' }; } const targetRoom = context.world.getRoom(roomId); if (!targetRoom) { return { success: false, message: `Room '${roomId}' not found.` }; } const currentRoom = context.room; // Announce departure currentRoom.broadcast(`${player.name} disappears in a flash of light.`, player); // Move player await player.moveTo(targetRoom); // Announce arrival targetRoom.broadcast(`${player.name} appears in a flash of light.`, player); // Send room description return { success: true, message: formatRoomDescription(targetRoom) }; } }; ``` ## Argument Parsing ### Basic Argument Parsing ```typescript const parseArguments = (input: string) => { // Split by spaces, but preserve quoted strings const regex = /[^\s"]+|"([^"]*)"/gi; const args: string[] = []; let match; while ((match = regex.exec(input)) !== null) { args.push(match[1] ? match[1] : match[0]); } return args; }; // Example usage const args = parseArguments('craft "iron sword" 5 --material steel'); // Results in: ["craft", "iron sword", "5", "--material", "steel"] ``` ### Advanced Argument Parsing ```typescript interface ParsedArguments { command: string; args: string[]; flags: Map<string, string | boolean>; target?: string; message?: string; } export const parseComplexCommand = (input: string): ParsedArguments => { const tokens = parseArguments(input); const command = tokens[0]; const args: string[] = []; const flags = new Map<string, string | boolean>(); for (let i = 1; i < tokens.length; i++) { const token = tokens[i]; if (token.startsWith('--')) { // Long flag: --flag=value or --flag const [flagName, flagValue] = token.slice(2).split('='); flags.set(flagName, flagValue || true); } else if (token.startsWith('-') && token.length === 2) { // Short flag: -f flags.set(token.slice(1), true); } else { args.push(token); } } return { command, args, flags }; }; // Example usage in command export const enhancedCraftCommand: Command = { name: 'craft', handler: async (player, args, context) => { const parsed = parseComplexCommand(context.originalInput); const recipe = parsed.args[0]; const quantity = parseInt(parsed.args[1]) || 1; const useHighQuality = parsed.flags.has('quality') || parsed.flags.has('q'); const fastCraft = parsed.flags.has('fast') || parsed.flags.has('f'); // Use parsed arguments... } }; ``` ### Argument Validation ```typescript interface ArgumentSchema { name: string; type: 'string' | 'number' | 'boolean' | 'player' | 'item' | 'room'; required?: boolean; min?: number; max?: number; choices?: string[]; validator?: (value: any) => boolean; } const createArgumentValidator = (schema: ArgumentSchema[]) => { return (args: string[]): ArgumentValidationResult => { const errors: string[] = []; for (let i = 0; i < schema.length; i++) { const spec = schema[i]; const value = args[i]; // Check required if (spec.required && !value) { errors.push(`${spec.name} is required`); continue; } if (!value) continue; // Optional and not provided // Type validation switch (spec.type) { case 'number': const num = parseInt(value); if (isNaN(num)) { errors.push(`${spec.name} must be a number`); } else { if (spec.min && num < spec.min) { errors.push(`${spec.name} must be at least ${spec.min}`); } if (spec.max && num > spec.max) { errors.push(`${spec.name} must be at most ${spec.max}`); } } break; case 'choices': if (spec.choices && !spec.choices.includes(value)) { errors.push(`${spec.name} must be one of: ${spec.choices.join(', ')}`); } break; case 'player': // Validate player exists const player = context.engine.playerManager.findOnlinePlayer(value); if (!player) { errors.push(`Player '${value}' not found`); } break; } // Custom validator if (spec.validator && !spec.validator(value)) { errors.push(`${spec.name} validation failed`); } } return { valid: errors.length === 0, errors }; }; }; // Usage export const transferCommand: Command = { name: 'transfer', description: 'Transfer gold to another player', usage: 'transfer <player> <amount>', validator: createArgumentValidator([ { name: 'player', type: 'player', required: true }, { name: 'amount', type: 'number', required: true, min: 1, max: 10000 } ]), handler: transferHandler }; ``` ## Permission System ### Role-Based Permissions ```typescript enum PlayerRole { PLAYER = 'player', HELPER = 'helper', BUILDER = 'builder', ADMIN = 'admin', SUPERADMIN = 'superadmin' } interface PermissionConfig { roles: PlayerRole[]; level?: number; custom?: (player: Player, context: CommandContext) => boolean; } export const checkPermissions = (player: Player, permissions: PermissionConfig, context: CommandContext): boolean => { // Check role-based permissions if (permissions.roles) { const hasRole = permissions.roles.some(role => player.hasRole(role)); if (!hasRole) return false; } // Check level requirements if (permissions.level && player.getLevel() < permissions.level) { return false; } // Check custom permission function if (permissions.custom && !permissions.custom(player, context)) { return false; } return true; }; // Example usage export const banCommand: Command = { name: 'ban', description: 'Ban a player from the game', usage: 'ban <player> [reason]', permissions: { roles: [PlayerRole.ADMIN, PlayerRole.SUPERADMIN], custom: (player, context) => { // Additional check: can't ban higher level admins const args = context.args; const targetName = args[0]; const target = context.engine.playerManager.findPlayer(targetName); if (target && target.hasRole(PlayerRole.SUPERADMIN) && !player.hasRole(PlayerRole.SUPERADMIN)) { return false; } return true; } }, handler: banHandler }; ``` ### Dynamic Permissions ```typescript export const editRoomCommand: Command = { name: 'edit', description: 'Edit room properties', usage: 'edit <property> <value>', permissions: { custom: (player, context) => { const room = context.room; // Room owners can always edit if (room.ownerId === player.id) return true; // Builders can edit in building zones if (player.hasRole(PlayerRole.BUILDER) && room.zone?.allowBuilding) { return true; } // Admins can edit anywhere if (player.hasRole(PlayerRole.ADMIN)) return true; return false; } }, handler: editRoomHandler }; ``` ## Context and State ### Command Context Usage ```typescript export const inventoryCommand: Command = { name: 'inventory', aliases: ['inv', 'i'], description: 'View your inventory', usage: 'inventory [filter]', handler: async (player, args, context) => { const filter = args[0]; const inventory = player.getInventory(); // Use context to determine display format const isVerbose = context.session.getPreference('verbose_inventory', false); const showWeight = context.session.getPreference('show_weight', true); let items = inventory.getItems(); // Apply filter if provided if (filter) { items = items.filter(item => item.name.toLowerCase().includes(filter.toLowerCase()) || item.type.toLowerCase().includes(filter.toLowerCase()) ); } if (items.length === 0) { return { success: true, message: filter ? `No items matching '${filter}' found.` : 'Your inventory is empty.' }; } // Format based on preferences const itemList = items.map(item => { let line = `${item.name} (${item.quantity})`; if (isVerbose) { line += ` - ${item.description}`; } if (showWeight) { line += ` [${item.weight * item.quantity} lbs]`; } return line; }).join('\n'); const totalWeight = inventory.getTotalWeight(); const maxWeight = player.getMaxCarryWeight(); let message = `Inventory (${items.length} items):\n${itemList}`; if (showWeight) { message += `\n\nTotal Weight: ${totalWeight}/${maxWeight} lbs`; if (totalWeight > maxWeight) { message += ' (Overencumbered!)'; } } return { success: true, message }; } }; ``` ### Stateful Commands ```typescript interface ShopState { browsing: boolean; category?: string; page: number; selectedItems: Map<string, number>; } export const shopCommand: Command = { name: 'shop', description: 'Browse and purchase items from a shop', usage: 'shop [category] [action]', handler: async (player, args, context) => { const room = context.room; const shop = room.getShop(); if (!shop) { return { success: false, message: 'There is no shop here.' }; } // Get or create shop state for this player let state = player.getTemporaryState<ShopState>('shop') || { browsing: false, page: 1, selectedItems: new Map() }; const action = args[0]?.toLowerCase(); switch (action) { case 'browse': case undefined: state.browsing = true; state.category = args[1]; state.page = 1; player.setTemporaryState('shop', state); return displayShopCatalog(shop, state); case 'next': if (!state.browsing) { return { success: false, message: 'You are not browsing the shop.' }; } state.page++; player.setTemporaryState('shop', state); return displayShopCatalog(shop, state); case 'prev': if (!state.browsing) { return { success: false, message: 'You are not browsing the shop.' }; } state.page = Math.max(1, state.page - 1); player.setTemporaryState('shop', state); return displayShopCatalog(shop, state); case 'buy': const itemName = args[1]; const quantity = parseInt(args[2]) || 1; return processPurchase(player, shop, itemName, quantity); case 'cart': return displayShoppingCart(state.selectedItems); case 'clear': state.selectedItems.clear(); player.setTemporaryState('shop', state); return { success: true, message: 'Shopping cart cleared.' }; case 'exit': player.removeTemporaryState('shop'); return { success: true, message: 'You stop browsing the shop.' }; default: return { success: false, message: 'Valid actions: browse, next, prev, buy, cart, clear, exit' }; } } }; ``` ## Error Handling ### Graceful Error Recovery ```typescript export const robustCommand: Command = { name: 'example', handler: async (player, args, context) => { try { // Main command logic return await executeCommandLogic(player, args, context); } catch (error) { // Log error for debugging context.engine.logger.error('Command execution failed:', { command: 'example', player: player.name, args, error: error.message, stack: error.stack }); // Determine appropriate user-facing error message if (error instanceof ValidationError) { return { success: false, message: error.message }; } if (error instanceof PermissionError) { return { success: false, message: 'You do not have permission to use this command.' }; } if (error instanceof DatabaseError) { return { success: false, message: 'A database error occurred. Please try again later.' }; } // Generic error for unexpected issues return { success: false, message: 'An unexpected error occurred. Please report this to an administrator.' }; } } }; ``` ### Error Context and Recovery ```typescript class CommandExecutionError extends Error { constructor( message: string, public player: Player, public command: string, public args: string[], public originalError?: Error ) { super(message); this.name = 'CommandExecutionError'; } } export const errorAwareHandler: CommandHandler = async (player, args, context) => { try { // Validate preconditions if (!validatePreconditions(player, args, context)) { throw new ValidationError('Command preconditions not met'); } // Execute with retry logic for transient errors let attempts = 0; const maxAttempts = 3; while (attempts < maxAttempts) { try { return await performAction(player, args, context); } catch (error) { attempts++; if (isTransientError(error) && attempts < maxAttempts) { // Wait and retry await new Promise(resolve => setTimeout(resolve, 1000 * attempts)); continue; } throw error; } } } catch (error) { // Enhanced error information throw new CommandExecutionError( `Failed to execute command: ${error.message}`, player, context.command.name, args, error ); } }; ``` ## Advanced Features ### Command Chaining ```typescript export const chainCommand: Command = { name: 'chain', description: 'Execute multiple commands in sequence', usage: 'chain "command1" "command2" ... [--stop-on-fail]', handler: async (player, args, context) => { const stopOnFail = context.flags?.has('stop-on-fail') || false; const results: CommandResult[] = []; for (const commandString of args) { try { const result = await context.engine.commandProcessor.execute(player, commandString); results.push(result); if (!result.success && stopOnFail) { break; } } catch (error) { const errorResult: CommandResult = { success: false, message: `Error executing "${commandString}": ${error.message}` }; results.push(errorResult); if (stopOnFail) break; } } // Compile summary const successful = results.filter(r => r.success).length; const total = results.length; let message = `Executed ${total} commands, ${successful} successful.`; if (results.some(r => !r.success)) { const failures = results.filter(r => !r.success); message += `\nFailures:\n${failures.map(f => f.message).join('\n')}`; } return { success: successful === total, message }; } }; ``` ### Autocomplete Support ```typescript export const giveCommand: Command = { name: 'give', description: 'Give an item to another player', usage: 'give <player> <item> [quantity]', autocomplete: async (player, args, context) => { const argIndex = args.length - 1; const partialArg = args[argIndex] || ''; if (argIndex === 0) { // Autocomplete player names const onlinePlayers = context.engine.playerManager.getOnlinePlayers() .filter(p => p !== player) .filter(p => p.name.toLowerCase().startsWith(partialArg.toLowerCase())) .map(p => p.name); return { suggestions: onlinePlayers, exact: false }; } if (argIndex === 1) { // Autocomplete item names from inventory const inventory = player.getInventory(); const items = inventory.getItems() .filter(item => item.name.toLowerCase().includes(partialArg.toLowerCase())) .map(item => item.name); return { suggestions: items, exact: false }; } if (argIndex === 2) { // Suggest quantities based on available amount const itemName = args[1]; const item = player.getInventory().findItem(itemName); if (item) { const suggestions = []; for (let i = 1; i <= Math.min(item.quantity, 10); i++) { suggestions.push(i.toString()); } return { suggestions, exact: true }; } } return { suggestions: [], exact: false }; }, handler: giveHandler }; ``` ### Conditional Command Execution ```typescript export const conditionalCommand: Command = { name: 'if', description: 'Execute command conditionally', usage: 'if <condition> then <command> [else <command>]', handler: async (player, args, context) => { const thenIndex = args.indexOf('then'); const elseIndex = args.indexOf('else'); if (thenIndex === -1) { return { success: false, message: 'Usage: if <condition> then <command> [else <command>]' }; } const conditionArgs = args.slice(0, thenIndex); const thenCommand = args.slice(thenIndex + 1, elseIndex === -1 ? undefined : elseIndex).join(' '); const elseCommand = elseIndex !== -1 ? args.slice(elseIndex + 1).join(' ') : null; // Evaluate condition const conditionResult = await evaluateCondition(player, conditionArgs, context); const commandToExecute = conditionResult ? thenCommand : elseCommand; if (!commandToExecute) { return { success: true, message: 'Condition evaluated but no command to execute.' }; } return await context.engine.commandProcessor.execute(player, commandToExecute); } }; async function evaluateCondition(player: Player, conditionArgs: string[], context: CommandContext): Promise<boolean> { const [operator, ...operands] = conditionArgs; switch (operator) { case 'has': const itemName = operands.join(' '); return player.getInventory().hasItem(itemName); case 'level': const requiredLevel = parseInt(operands[0]); return player.getLevel() >= requiredLevel; case 'gold': const requiredGold = parseInt(operands[0]); return player.getGold() >= requiredGold; case 'in': const roomId = operands[0]; return context.room.id === roomId; case 'time': const timeRange = operands[0]; // e.g., "day", "night" const currentTime = context.world.getTimeOfDay(); return currentTime === timeRange; default: return false; } } ``` ## Testing Commands ### Unit Tests ```typescript // commands/__tests__/look.test.ts import { lookCommand } from '../look.js'; import { createMockPlayer, createMockRoom, createMockContext } from '../../test/mocks.js'; describe('lookCommand', () => { let player: MockPlayer; let room: MockRoom; let context: MockCommandContext; beforeEach(() => { player = createMockPlayer('TestPlayer'); room = createMockRoom('test-room'); context = createMockContext(player, room); }); test('should describe room when no arguments', async () => { const result = await lookCommand.handler(player, [], context); expect(result.success).toBe(true); expect(result.message).toContain(room.title); expect(result.message).toContain(room.description); }); test('should examine object when target specified', async () => { const sword = createMockItem('iron sword'); room.addItem(sword); const result = await lookCommand.handler(player, ['sword'], context); expect(result.success).toBe(true); expect(result.message).toContain(sword.description); }); test('should handle target not found', async () => { const result = await lookCommand.handler(player, ['nonexistent'], context); expect(result.success).toBe(false); expect(result.message).toContain('don\'t see'); }); }); ``` ### Integration Tests ```typescript // commands/__tests__/integration.test.ts import { MUDEngine } from '../../index.js'; import { lookCommand, moveCommand } from '../index.js'; describe('Command Integration', () => { let engine: MUDEngine; let player: Player; beforeEach(async () => { engine = new MUDEngine({ database: ':memory:', protocols: [] }); // Register commands engine.commandRegistry.register(lookCommand); engine.commandRegistry.register(moveCommand); await engine.start(); player = await engine.playerManager.createPlayer('TestPlayer'); }); afterEach(async () => { await engine.stop(); }); test('should execute look command through engine', async () => { const result = await engine.commandProcessor.execute(player, 'look'); expect(result.success).toBe(true); expect(result.message).toBeTruthy(); }); test('should handle movement between rooms', async () => { // Create connected rooms const startRoom = await engine.worldManager.createRoom({ id: 'start', title: 'Start Room', description: 'Starting location' }); const endRoom = await engine.worldManager.createRoom({ id: 'end', title: 'End Room', description: 'Ending location' }); startRoom.addExit('north', endRoom.id); await player.moveTo(startRoom); // Test movement const moveResult = await engine.commandProcessor.execute(player, 'go north'); expect(moveResult.success).toBe(true); // Verify new location const lookResult = await engine.commandProcessor.execute(player, 'look'); expect(lookResult.message).toContain('End Room'); }); }); ``` ### Performance Tests ```typescript // commands/__tests__/performance.test.ts import { performanceCommand } from '../test-commands.js'; describe('Command Performance', () => { test('should execute within acceptable time limits', async () => { const player = createMockPlayer('TestPlayer'); const context = createMockContext(player); const iterations = 1000; const startTime = Date.now(); for (let i = 0; i < iterations; i++) { await performanceCommand.handler(player, ['test'], context); } const endTime = Date.now(); const totalTime = endTime - startTime; const avgTime = totalTime / iterations; // Command should execute in under 1ms on average expect(avgTime).toBeLessThan(1); }); test('should handle concurrent executions', async () => { const concurrency = 100; const players = Array.from({ length: concurrency }, (_, i) => createMockPlayer(`Player${i}`) ); const startTime = Date.now(); const promises = players.map(player => { const context = createMockContext(player); return performanceCommand.handler(player, ['test'], context); }); const results = await Promise.all(promises); const endTime = Date.now(); const totalTime = endTime - startTime; // All commands should succeed expect(results.every(r => r.success)).toBe(true); // Should complete within reasonable time expect(totalTime).toBeLessThan(1000); // 1 second }); }); ``` ## Best Practices ### 1. Command Design Principles ```typescript // Good: Clear, focused command export const dropCommand: Command = { name: 'drop', description: 'Drop an item from your inventory', usage: 'drop <item> [quantity]', handler: async (player, args, context) => { // Single responsibility: dropping items // Clear error messages // Consistent return format } }; // Bad: Overly complex multi-purpose command export const itemCommand: Command = { name: 'item', description: 'Manage items', // Too vague usage: 'item <action> <target> [options...]', // Too complex handler: async (player, args, context) => { // Handles get, drop, use, examine, etc. // Single command doing too many things } }; ``` ### 2. Error Message Guidelines ```typescript // Good: Clear, helpful error messages if (!target) { return { success: false, message: `You don't see '${targetName}' here. Use 'look' to see available items.` }; } if (player.getGold() < cost) { return { success: false, message: `You need ${cost} gold but only have ${player.getGold()}.` }; } // Bad: Vague or technical errors if (!target) { return { success: false, message: 'Target not found.' // Too vague }; } if (player.getGold() < cost) { return { success: false, message: 'EntityNotFound: Insufficient currency balance' // Too technical }; } ``` ### 3. Resource Management ```typescript export class ResourceAwareCommand implements Command { name = 'resource-command'; async handler(player: Player, args: string[], context: CommandContext): Promise<CommandResult> { // Set up resources const resources = new Set<Resource>(); try { // Acquire resources as needed const connection = await context.engine.database.getConnection(); resources.add(connection); const lockId = await context.engine.lockManager.acquireLock(`player:${player.id}`); resources.add({ type: 'lock', id: lockId }); // Perform command logic return await this.executeLogic(player, args, context); } finally { // Always clean up resources for (const resource of resources) { try { await this.releaseResource(resource); } catch (error) { context.engine.logger.warn('Failed to release resource:', error); } } } } private async releaseResource(resource: any): Promise<void> { if (resource.close) { await resource.close(); } else if (resource.type === 'lock') { await context.engine.lockManager.releaseLock(resource.id); } } } ``` ### 4. State Management ```typescript export class StatefulCommand implements Command { private static readonly STATE_KEY = 'command_state'; private static readonly STATE_TIMEOUT = 5 * 60 * 1000; // 5 minutes async handler(player: Player, args: string[], context: CommandContext): Promise<CommandResult> { // Get existing state or create new let state = player.getTemporaryState<CommandState>(StatefulCommand.STATE_KEY); if (!state) { state = this.createInitialState(); player.setTemporaryState(StatefulCommand.STATE_KEY, state, StatefulCommand.STATE_TIMEOUT); } // Process command based on state const result = await this.processWithState(player, args, state, context); // Update state if needed if (state.shouldPersist) { player.setTemporaryState(StatefulCommand.STATE_KEY, state, StatefulCommand.STATE_TIMEOUT); } else { player.removeTemporaryState(StatefulCommand.STATE_KEY); } return result; } } ``` ## Performance Optimization ### Command Caching ```typescript class CommandCache { private cache = new Map<string, { result: CommandResult; timestamp: number }>(); private readonly TTL = 30 * 1000; // 30 seconds getCachedResult(key: string): CommandResult | null { const entry = this.cache.get(key); if (!entry) return null; if (Date.now() - entry.timestamp > this.TTL) { this.cache.delete(key); return null; } return entry.result; } setCachedResult(key: string, result: CommandResult): void { this.cache.set(key, { result, timestamp: Date.now() }); } generateKey(player: Player, command: string, args: string[]): string { return `${player.id}:${command}:${args.join(':')}`; } } export const cachingInfoCommand: Command = { name: 'info', handler: async (player, args, context) => { const cache = context.engine.commandCache; const cacheKey = cache.generateKey(player, 'info', args); // Try cache first const cached = cache.getCachedResult(cacheKey); if (cached) { return cached; } // Generate fresh result const result = await generateInfoResult(player, args, context); // Cache if successful if (result.success) { cache.setCachedResult(cacheKey, result); } return result; } }; ``` ### Batch Operations ```typescript export const massCommand: Command = { name: 'mass', description: 'Perform action on multiple targets', usage: 'mass <action> <pattern>', handler: async (player, args, context) => { const action = args[0]; const pattern = args[1]; // Find all matching targets const targets = findTargets(pattern, context); if (targets.length === 0) { return { success: false, message: `No targets matching '${pattern}' found.` }; } // Batch process for better performance const batchSize = 10; const results: CommandResult[] = []; for (let i = 0; i < targets.length; i += batchSize) { const batch = targets.slice(i, i + batchSize); const batchResults = await Promise.all( batch.map(target => executeAction(action, target, player, context)) ); results.push(...batchResults); // Small delay to prevent overwhelming the system if (i + batchSize < targets.length) { await new Promise(resolve => setTimeout(resolve, 10)); } } // Summarize results const successful = results.filter(r => r.success).length; return { success: successful > 0, message: `Processed ${targets.length} targets, ${successful} successful.` }; } }; ``` This comprehensive command development guide provides everything needed to create professional, maintainable commands for Ursamu, from basic concepts to advanced optimization techniques.