@ursamu/core
Version:
Ursamu - Modular MUD Engine with sandboxed scripting and plugin system
1,679 lines (1,358 loc) • 43.4 kB
Markdown
# 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.