__ __ _ _ ____ _ _
| \/ | | | | _ \| | __ _ _ __ __| |___
| |\/| | | | | | | | |/ _` | '_ \ / _` / __|
| | | | |_| | |_| | | (_| | | | | (_| \__ \
|_| |_|\___/|____/|_|\__,_|_| |_|\__,_|___/
Project Portfolio
A full-stack real-time MUD game where AI-driven NPCs live, breathe, and shape a dynamic world. 13+ services, 50+ API endpoints, real-time WebSocket gameplay, and LLaMA-powered content generation.
MUDlands is a production-grade multiplayer text adventure game (MUD) built entirely from scratch. Players connect in real-time via WebSocket, explore an interconnected world, fight monsters, complete quests, craft items, and interact with AI-driven NPCs that have their own personalities and daily schedules.
To explore real-time multiplayer architecture, AI integration in game systems, and full-stack development with production concerns: security hardening, rate limiting, session management, circuit breaker patterns, and graceful degradation.
+------------------+
| Nginx Proxy |
| (Rate Limiting) |
+--------+---------+
|
+--------+---------+
| Express.js |
| + Socket.IO |
+---+---------+----+
| |
+---------+ +----+--------+
| | |
+--------+---+ +------+------+ +----+------+
| PostgreSQL | | Redis | | Ollama |
| (Players, | | (Sessions, | | (LLaMA |
| World, | | Cache, | | 3.1 AI) |
| Quests) | | Tokens) | | |
+-------------+ +------------+ +-----------+
codebase/app/ ├── server.js # Express + Socket.IO entry point ├── src/ │ ├── services/ # 13 core game services │ │ ├── GameEngine.js # Main game loop & state │ │ ├── SocketHandler.js # Real-time WebSocket layer │ │ ├── CommandParser.js # 80+ command parser with aliases │ │ ├── CombatSystem.js # Turn-based combat mechanics │ │ ├── QuestManager.js # Quest progression system │ │ ├── AIContentService.js # Ollama AI with circuit breaker │ │ ├── AICharacterController.js # NPC behavior & scheduling │ │ ├── NPCFactory.js # Dynamic NPC generation │ │ ├── DailyStoryEvolution.js # World event system │ │ ├── ClassManager.js # Character class progression │ │ └── CraftingSystem.js # Item crafting recipes │ ├── routes/ # 50+ API endpoints │ │ ├── auth.js # JWT auth with token blacklisting │ │ ├── admin.js # 35+ admin management routes │ │ ├── ai.js # AI generation endpoints │ │ └── character.js # Character creation & management │ ├── models/ # 7 data models │ ├── middleware/ # CSRF, role auth, error handling │ └── utils/ # Validation, logging, token mgmt ├── public/ # Frontend clients │ ├── index.html # Main game client (32KB) │ ├── graphical.html # Phaser.js graphical mode │ └── admin.html # Admin dashboard (57KB) └── docs/ # Technical documentation
The SocketHandler manages all real-time communication. Players authenticate via JWT over WebSocket, and every movement, chat message, and combat action is broadcast instantly to relevant players in the same room.
class SocketHandler {
constructor(io, gameEngine, commandParser) {
this.io = io;
this.gameEngine = gameEngine;
this.commandParser = commandParser;
this.socketToPlayer = new Map();
this.commandRateLimits = new Map();
this.maxCommandsPerMinute = 60;
this.setupGameEngineListeners();
}
setupGameEngineListeners() {
// Broadcast player movements to all players in affected rooms
this.gameEngine.on('playerMoved', ({ player, from, to, direction }) => {
this.gameEngine.broadcastToRoom(from.id,
`${player.name} leaves ${direction}.`, player.id);
this.gameEngine.broadcastToRoom(to.id,
`${player.name} arrives.`, player.id);
});
// Push real-time stat updates to individual players
this.gameEngine.on('playerUpdate', (player) => {
const socket = this.getSocketByPlayerId(player.id);
if (socket) socket.emit('playerUpdate', {
hp: player.currentHp, maxHp: player.maxHp,
level: player.level, experience: player.experience
});
});
}
async handleAuthenticate(socket, { token, playerId, guest }) {
// Validate JWT, check blacklist, load player from DB
const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (decoded.jti && tokenBlacklist.isBlacklisted(decoded.jti)) {
socket.emit('authError', { message: 'Token revoked' });
return;
}
const player = new Player(await db.loadPlayer(decoded.id));
this.socketToPlayer.set(socket.id, player);
this.gameEngine.addPlayer(player);
socket.emit('authenticated', { player: player.toJSON() });
}
}
The combat system uses D&D-inspired mechanics: STR-scaled base damage, VIT/AGI defense with diminishing returns (soft cap formula), DEX+LUK critical chance with a 50% ceiling, agility-based dodge, and 85-115% damage variance for unpredictability.
calculateDamage(attacker, defender) {
// Base damage: STR scaling + level bonus
const attackPower = attacker.stats.str || 5;
const baseDamage = attackPower * 2 + (attacker.level * 3);
// Defense with diminishing returns (soft cap formula)
const defense = defender.stats.vit + Math.floor(defender.stats.agi * 0.3);
const damageReduction = defense / (defense + 50);
// Critical hit: DEX + LUK based, capped at 50%
const critChance = Math.min(0.5,
(attacker.stats.dex + attacker.stats.luk) / 100);
const isCritical = Math.random() < critChance;
const critMultiplier = isCritical ? 1.5 : 1.0;
// Dodge chance based on agility difference
const dodgeChance = Math.min(0.3,
Math.max(0, (defender.stats.agi - attacker.stats.dex) * 0.02));
if (Math.random() < dodgeChance)
return { damage: 0, dodged: true };
// Final: apply reduction, crit, and 85-115% variance
const variance = Math.random() * 0.3 + 0.85;
const final = Math.max(1,
Math.floor(baseDamage * (1 - damageReduction) * critMultiplier * variance));
return { damage: final, critical: isCritical, dodged: false };
}
Players type natural language commands. The parser maps 80+ commands and aliases to handler functions - from movement shortcuts (n/s/e/w) to social verbs (say, yell, whisper) to combat (attack, kill, slay) to crafting (forge, brew, enchant). Even punctuation works: 'hello maps to say hello.
initializeCommands() {
return {
// Movement - full names and shortcuts
north: this.handleMove.bind(this, 'north'),
n: this.handleMove.bind(this, 'north'),
ne: this.handleMove.bind(this, 'northeast'),
// Combat - multiple natural verbs
attack: this.handleAttack.bind(this),
kill: this.handleAttack.bind(this),
fight: this.handleAttack.bind(this),
slay: this.handleAttack.bind(this),
// Social - talk to NPCs with any verb
talk: this.handleTalk.bind(this),
speak: this.handleTalk.bind(this),
converse: this.handleTalk.bind(this),
ask: this.handleAsk.bind(this),
// Crafting - thematic aliases
craft: this.handleCraft.bind(this),
forge: this.handleCraft.bind(this),
brew: this.handleCraft.bind(this),
enchant: this.handleCraft.bind(this),
// Punctuation shortcuts
"'": this.handleSay.bind(this), // 'hello => say hello
'"': this.handleSay.bind(this),
'!': this.handleYell.bind(this),
// D&D dice rolling
roll: this.handleRoll.bind(this), // roll 2d6+3
advantage: this.handleAdvantage.bind(this),
// ...80+ total commands
};
}
The AIContentService wraps Ollama/LLaMA 3.1 with production-grade resilience: a circuit breaker pattern (CLOSED → OPEN → HALF_OPEN), per-minute rate limiting, Redis caching with configurable TTL, request queuing, and automatic fallback to static content when AI is unavailable.
async generateContent(type, parameters) {
// Circuit breaker: if OPEN, check if cooldown elapsed
if (this.circuitState === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.circuitResetTimeout) {
this.circuitState = 'HALF_OPEN'; // Try one request
this.failureCount = 0;
} else {
return this.getFallbackContent(type, parameters);
}
}
// Rate limiting: queue if over budget
if (!this.checkRateLimit()) {
return this.queueRequest(type, parameters);
}
// Check Redis cache first
const cacheKey = this.generateCacheKey(type, parameters);
const cached = await this.getFromCache(cacheKey);
if (cached) return cached;
try {
const content = await this.callOllama(type, parameters);
// Success: reset circuit breaker
if (this.circuitState === 'HALF_OPEN') {
this.circuitState = 'CLOSED';
}
await this.saveToCache(cacheKey, content);
return content;
} catch (error) {
this.handleGenerationError(error); // Trips breaker after 5 fails
return this.getFallbackContent(type, parameters);
}
}
5 AI-driven NPCs with unique personalities, memory systems, emotional states, and behavior stacks. Each character tracks their interactions, remembers players, maintains goals, and reacts to story events. Characters are loaded from JSON profiles and managed through an event-driven architecture.
class AICharacterController extends EventEmitter {
registerAICharacter(config) {
const aiChar = {
id: config.metadata.character_id,
config: config,
currentBehavior: null,
behaviorStack: [],
memory: {
recentInteractions: [],
knownPlayers: new Map(), // Remember who they've met
currentGoals: [],
emotionalState: 'neutral' // Affects dialogue tone
},
stats: {
activations: 0,
interactions: 0,
questsGiven: 0,
storiesShared: 0
}
};
this.activeAICharacters.set(aiChar.id, aiChar);
}
async activateCharacter(characterId, behavior, duration, event) {
const aiChar = this.activeAICharacters.get(characterId);
aiChar.currentBehavior = behavior;
aiChar.activationTime = Date.now();
aiChar.duration = duration * 60 * 1000;
aiChar.stats.activations++;
// Character now acts autonomously in the world...
}
}
Exploration - room descriptions, NPCs, and exits
Combat - crits, dodges, XP, and loot drops
Admin Panel - 35+ management routes for game masters