__  __ _   _ ____  _                 _
|  \/  | | | |  _ \| | __ _ _ __   __| |___
| |\/| | | | | | | | |/ _` | '_ \ / _` / __|
| |  | | |_| | |_| | | (_| | | | | (_| \__ \
|_|  |_|\___/|____/|_|\__,_|_| |_|\__,_|___/
            

Project Portfolio

AI-Powered Multiplayer Text Adventure

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.

~15k Lines of Code
13 Backend Services
50+ API Endpoints
7 Data Models
Node.js| Express| Socket.IO| PostgreSQL| Redis| LLaMA 3.1 / Ollama| JWT / bcrypt| Phaser.js| Nginx| PM2

> cat README.md

What It Is

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.

Why I Built It

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.

Architecture

                    +------------------+
                    |   Nginx Proxy    |
                    | (Rate Limiting)  |
                    +--------+---------+
                             |
                    +--------+---------+
                    |   Express.js     |
                    |   + Socket.IO    |
                    +---+---------+----+
                        |         |
              +---------+    +----+--------+
              |              |             |
     +--------+---+  +------+------+ +----+------+
     | PostgreSQL  |  |    Redis    | |  Ollama   |
     | (Players,   |  | (Sessions, | | (LLaMA    |
     |  World,     |  |  Cache,    | |  3.1 AI)  |
     |  Quests)    |  |  Tokens)   | |           |
     +-------------+  +------------+ +-----------+

Project Structure

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

> highlight --best-of

Real-Time Multiplayer via Socket.IO

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.

Socket.IO JWT Auth Event-Driven Rate Limited
src/services/SocketHandler.js
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() });
    }
}

Turn-Based Combat with RPG Mechanics

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.

Damage Formula Crit System Diminishing Returns Loot Drops
src/services/CombatSystem.js
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 };
}

80+ Command Parser with Natural Aliases

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.

80+ Commands Alias System Natural Language
src/services/CommandParser.js
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
    };
}

> describe --ai-system

AI Content Generation with Circuit Breaker

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.

Circuit Breaker Rate Limiting Redis Cache Graceful Fallback
src/services/AIContentService.js
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);
    }
}

AI Character Controller - Living NPCs

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.

Behavior Stack Memory System Event-Driven 5 Unique NPCs
src/services/AICharacterController.js
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...
    }
}
ai_session.log - NPCs in action
[SYSTEM] AI Character Controller initialized - 5 characters loaded
 
Elder Thaddeus (Town Leader) walks to the Town Square.
"Good morning, adventurers. The mists are thick today..."
 
Sister Morwyn (Healer) opens the healing shrine.
"Come, weary traveler. Let me tend to your wounds."
 
Razorclaw (Beast-kin Protector) patrols the northern gate.
The Veiled Scholar (Cultist) whispers from the shadows.
Grizelda Ironfoot (Dwarf Explorer) studies her maps.
 
[STORY] A mysterious tremor shakes the earth...
[STORY] New quest generated: "Investigate the Tremors"

> audit --security

Authentication

  • JWT tokens with unique JTI identifiers
  • Token blacklisting on logout
  • bcrypt password hashing (cost 10)
  • Session management via Redis

API Protection

  • CSRF tokens with timing-safe comparison
  • Per-IP rate limiting (5 login/15min)
  • Helmet security headers + CSP
  • Role-based access control (4 levels)

Data Safety

  • Parameterized SQL queries (no injection)
  • Input validation & sanitization layer
  • XSS prevention via HTML escaping
  • httpOnly, sameSite, secure cookies

Infrastructure

  • Nginx reverse proxy with rate limiting
  • HTTPS-only CORS in production
  • Structured security event logging
  • Graceful shutdown handlers

> screenshot --gameplay