Dungeon Generation Algorithm - Ubivis/UbiDungeon GitHub Wiki
Dungeon Generation Algorithms
Overview
The AI Dungeon Generator uses advanced AI algorithms to create unique, procedurally generated dungeons. This page explains the three core algorithms used in the generation process and how they work together to create compelling dungeons.
Algorithm Components
The generation process combines three primary algorithms:
- Cellular Automata - Creates organic, natural-looking room layouts
- Markov Chain Model - Ensures theme coherence and logical room transitions
- Genetic Algorithm - Optimizes layouts for playability, aesthetics, and challenge
Let's explore each of these in detail.
Cellular Automata
Purpose
Cellular automata create natural-looking cave systems and room layouts that avoid the artificial feel of traditional grid-based dungeon generators.
How It Works
- Initialization: The generator starts with a grid where each cell has a random chance to be a "room" or "wall"
- Rule Application: For multiple iterations, each cell's state is updated based on neighboring cells
- Smoothing: The results are smoothed to create coherent room structures
Implementation Details
The plugin uses a modified Conway's Game of Life ruleset:
private boolean[][] doSimulationStep(boolean[][] oldMap, int size) {
boolean[][] newMap = new boolean[size][size];
// Evaluate each cell in the grid
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
// Count neighbors (including diagonals)
int neighbors = countNeighbors(oldMap, x, y, size);
// Apply cellular automata rules
if (oldMap[x][y]) {
// Currently a room
newMap[x][y] = neighbors >= deathLimit;
} else {
// Currently empty
newMap[x][y] = neighbors > birthLimit;
}
}
}
return newMap;
}
Parameters
The cellular automata system can be tuned with several parameters:
initialFillPercent
- Initial percentage of cells that are rooms (default: 45%)iterations
- Number of rule application cycles (default: 4)birthLimit
- Min neighbors needed to create a new room (default: 4)deathLimit
- Min neighbors needed to keep a room (default: 3)
Results
This algorithm produces cave-like structures with natural-feeling room shapes and connections, avoiding the grid-like appearance of traditional room-and-corridor designs.
Markov Chain Model
Purpose
The Markov Chain model ensures that room types follow logical patterns and theme consistency, creating coherent dungeon layouts with proper progression.
How It Works
- Transition Matrix: The model uses a probability matrix that defines likely transitions between room types
- Room Type Assignment: After the cellular automata creates the basic layout, the Markov Chain assigns specific room types
- Theme Coherence: Ensures that room types make sense for the dungeon's theme and create a logical progression
Implementation Details
The transition probability matrix determines which room types are likely to be adjacent:
private void initializeTransitionProbabilities() {
// For each source room type
for (RoomType sourceType : RoomType.values()) {
Map<RoomType, Double> transitions = new HashMap<>();
transitionProbabilities.put(sourceType, transitions);
// For each destination room type
for (RoomType destType : RoomType.values()) {
// Set default probabilities
double probability = 0.0;
if (sourceType == RoomType.EMPTY) {
// Empty cells should stay empty
probability = (destType == RoomType.EMPTY) ? 1.0 : 0.0;
} else if (sourceType == RoomType.ENTRANCE) {
// Entrance should connect to normal rooms
probability = (destType == RoomType.NORMAL) ? 0.9 : 0.1;
} else if (sourceType == RoomType.NORMAL) {
// Normal rooms can connect to various types
switch (destType) {
case NORMAL:
probability = 0.7; // Most likely another normal room
break;
case TREASURE:
probability = 0.1; // Sometimes treasure
break;
case TRAP:
probability = 0.15; // Sometimes traps
break;
case BOSS:
probability = 0.05; // Rarely boss rooms
break;
default:
probability = 0.0; // Never others
}
} else if (sourceType.isSpecial()) {
// Special rooms mostly connect back to normal rooms
probability = (destType == RoomType.NORMAL) ? 0.9 : 0.1;
}
transitions.put(destType, probability);
}
}
}
Parameters
The Markov Chain can be customized through:
transitionProbabilities
- Matrix of room type transition probabilities- Room type distributions specific to dungeon themes
Results
The Markov Chain creates logical room progressions that feel properly designed:
- Treasure rooms are appropriately rare
- Boss rooms are placed at logical endpoints
- Trap rooms are interspersed at appropriate intervals
- Special rooms are connected to normal rooms for proper flow
Genetic Algorithm
Purpose
The genetic algorithm optimizes dungeon layouts for playability, challenge balance, and aesthetic appeal, taking the raw output from the previous algorithms and refining it.
How It Works
- Population: Creates multiple variations of the dungeon layout
- Fitness Evaluation: Evaluates each layout based on various criteria
- Selection: Chooses the best layouts for "breeding"
- Crossover & Mutation: Combines and modifies layouts to create new variations
- Evolution: Repeats the process for multiple generations to optimize
Implementation Details
The genetic algorithm evaluates dungeons based on multiple fitness criteria:
void calculateFitness() {
double score = 0.0;
// 1. Connectivity - reward well-connected layouts
score += evaluateConnectivity() * 0.4;
// 2. Room distribution - reward good room type distribution
score += evaluateRoomDistribution() * 0.3;
// 3. Aesthetics - reward interesting shapes
score += evaluateAesthetics() * 0.2;
// 4. Challenge balance - reward good difficulty progression
score += evaluateChallenge() * 0.1;
this.fitness = score;
}
Fitness Criteria
The algorithm evaluates layouts on four main criteria:
-
Connectivity: How well rooms are connected and accessible from the entrance
private double evaluateConnectivity() { // Count accessible rooms using flood fill boolean[][] visited = new boolean[size][size]; int accessibleRooms = floodFill(entranceX, entranceY, visited); // Calculate percentage of accessible rooms int totalRooms = countTotalRooms(); return (double) accessibleRooms / totalRooms; }
-
Room Distribution: Proper mix of room types (normal, treasure, trap, boss)
private double evaluateRoomDistribution() { // Count room types int normalRooms = countRoomsByType(RoomType.NORMAL); int treasureRooms = countRoomsByType(RoomType.TREASURE); int trapRooms = countRoomsByType(RoomType.TRAP); int bossRooms = countRoomsByType(RoomType.BOSS); // Calculate ideal distributions and score // [calculation logic...] return distributionScore; }
-
Aesthetics: Interesting and varied room shapes and layouts
private double evaluateAesthetics() { // Count different room shapes/clusters int shapes = countRoomShapes(); // Score based on variety return calculateShapeScore(shapes); }
-
Challenge Balance: Proper difficulty progression with boss placement
private double evaluateChallenge() { // Boss distance from entrance double bossDistanceScore = evaluateBossDistance(); // Trap distribution double trapDistributionScore = evaluateTrapDistribution(); return bossDistanceScore * 0.6 + trapDistributionScore * 0.4; }
Parameters
The genetic optimization can be tuned with:
populationSize
- Number of layout variations per generation (default: 10)generations
- Number of evolution cycles (default: varies by dungeon size)mutationRate
- Chance of random changes (default: 0.2)crossoverRate
- Rate of breeding between layouts (default: 0.7)
Results
The genetic algorithm refines dungeons to ensure they are:
- Fully connected with no isolated rooms
- Well-balanced with proper room type distribution
- Aesthetically interesting with varied layouts
- Properly challenging with good progression
Combined Process
The complete generation process flows as follows:
- Initial Layout: Cellular automata creates the basic room structure
- Room Type Assignment: Markov Chain assigns room types and ensures theme coherence
- Layout Optimization: Genetic algorithm refines the dungeon for playability and balance
- World Generation: The optimized layout is translated into actual Minecraft blocks
- Feature Placement: Traps, monsters, treasures, and quest markers are placed in appropriate locations
Advanced Configuration
Advanced users can fine-tune the algorithms through the config.yml file:
generation:
algorithm:
room-size:
min: 5
max: 15
corridor-length:
min: 3
max: 10
dungeon-size:
small: 25
medium: 40
large: 60
optimization-generations: 10
cellular-automata:
initial-fill: 45
iterations: 4
birth-limit: 4
death-limit: 3
markov:
enforce-boss-room: true
min-treasure-rooms: 2
max-trap-density: 0.15
genetic:
population-size: 10
mutation-rate: 0.2
crossover-rate: 0.7
Technical Details
Asynchronous Generation
To prevent server lag, the generation process runs asynchronously:
private void processGenerationTaskAsync(GenerationTask task) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
// Generate dungeon layout asynchronously
DungeonLayout layout = dungeonGenerator.generateDungeonAsync(task.getArea());
// Schedule synchronous placement in world
Bukkit.getScheduler().runTask(plugin, () -> {
placeDungeonInWorld(layout, task.getArea());
// [additional processing...]
});
} catch (Exception e) {
plugin.getLogger().severe("Error generating dungeon: " + e.getMessage());
// [error handling...]
}
});
}
Runtime Complexity
- Cellular Automata: O(n² * i) where n is dungeon size and i is iterations
- Markov Chain: O(n²) for room type assignment
- Genetic Algorithm: O(p * g * n²) where p is population size and g is generations
Memory Usage
The algorithms are designed to be memory-efficient, with a typical dungeon requiring:
- ~250KB for grid representation
- ~1-2MB during genetic optimization
- ~500KB for the final layout