Game level design? Time-consuming but perfect for AI. Here's how to do it:
- Procedural Algorithms: Use wave function collapse or BSP dungeon gen—no cost, always available.
- Local LLMs via Ollama: For natural language → level config. Fallback chain if no model installed.
No OpenAI account needed. No API keys. No internet required.
The Generation Stack
Text Prompt → Local LLM (Ollama/llama3) → Level Config JSON
↓ (fallback)
Procedural Generator → Level Config JSON
↓
Tilemap Exporter → Godot .tscn / Unity tilemap / TMX
Setup
# Install Ollama for local LLM
curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3.2:3b # Small, fast, works well for structured output
# Python dependencies
pip install noise requests numpy pillow
Step 1: Procedural Level Generation (No LLM Needed)
Binary space partitioning (BSP) dungeon generator:
import random
import numpy as np
from dataclasses import dataclass
from typing import List, Optional, Tuple
@dataclass
class Room:
x: int
y: int
width: int
height: int
@property
def center(self) -> Tuple[int, int]:
return (self.x + self.width // 2, self.y + self.height // 2)
def intersects(self, other: 'Room', padding: int = 1) -> bool:
return not (
self.x + self.width + padding <= other.x or
other.x + other.width + padding <= self.x or
self.y + self.height + padding <= other.y or
other.y + other.height + padding <= self.y
)
class BSPDungeonGenerator:
EMPTY = 0
FLOOR = 1
WALL = 2
DOOR = 3
SPAWN = 4
EXIT = 5
CHEST = 6
def __init__(self, width: int = 64, height: int = 48, seed: int = None):
self.width = width
self.height = height
self.rng = random.Random(seed)
self.grid = np.zeros((height, width), dtype=int)
self.rooms: List[Room] = []
def generate(
self,
min_room_size: int = 6,
max_room_size: int = 14,
max_rooms: int = 12,
add_chests: bool = True,
corridor_style: str = "L" # "L", "straight", "both"
) -> np.ndarray:
self.grid.fill(self.WALL)
attempts = 0
while len(self.rooms) < max_rooms and attempts < max_rooms * 5:
w = self.rng.randint(min_room_size, max_room_size)
h = self.rng.randint(min_room_size, max_room_size)
x = self.rng.randint(1, self.width - w - 1)
y = self.rng.randint(1, self.height - h - 1)
new_room = Room(x, y, w, h)
if not any(new_room.intersects(r) for r in self.rooms):
self._carve_room(new_room)
if self.rooms:
self._connect_rooms(self.rooms[-1], new_room, corridor_style)
self.rooms.append(new_room)
attempts += 1
if self.rooms:
cx, cy = self.rooms[0].center
self.grid[cy][cx] = self.SPAWN
cx, cy = self.rooms[-1].center
self.grid[cy][cx] = self.EXIT
if add_chests:
for room in self.rooms[1:-1]:
if random.random() < 0.4:
chest_x = self.rng.randint(room.x + 1, room.x + room.width - 2)
chest_y = self.rng.randint(room.y + 1, room.y + room.height - 2)
self.grid[chest_y][chest_x] = self.CHEST
return self.grid
def _carve_room(self, room: Room):
for y in range(room.y, room.y + room.height):
for x in range(room.x, room.x + room.width):
self.grid[y][x] = self.FLOOR
def _connect_rooms(self, a: Room, b: Room, style: str):
ax, ay = a.center
bx, by = b.center
if style == "L" or (style == "both" and random.random() < 0.5):
for x in range(min(ax, bx), max(ax, bx) + 1):
self.grid[ay][x] = self.FLOOR
for y in range(min(ay, by), max(ay, by) + 1):
self.grid[y][bx] = self.FLOOR
# Generate a dungeon
gen = BSPDungeonGenerator(width=64, height=48, seed=42)
level = gen.generate(max_rooms=10, add_chests=True)
print(f"Generated level with {len(gen.rooms)} rooms")
Step 2: Local LLM for Level Configuration
import requests
import json
def prompt_to_level_config(prompt: str, model: str = "llama3.2:3b") -> dict:
system_prompt = """You are a game level design assistant.
Convert the user's description into a JSON level configuration.
Respond ONLY with valid JSON, no explanation.
Schema:
{
"theme": "dungeon|cave|forest|castle|sci-fi",
"width": 64,
"height": 48,
"max_rooms": 8,
"min_room_size": 5,
"max_room_size": 14,
"enemy_density": 0.3,
"chest_probability": 0.4,
"boss_room": false,
"difficulty": "easy|medium|hard",
"special_features": ["dark", "wet", "trapped"]
}"""
try:
response = requests.post(
"http://localhost:11434/api/generate",
json={
"model": model,
"prompt": f"Level description: {prompt}",
"system": system_prompt,
"stream": False,
"options": {"temperature": 0.3}
},
timeout=30
)
if response.status_code == 200:
raw = response.json()["response"].strip()
start = raw.find("{")
end = raw.rfind("}") + 1
config = json.loads(raw[start:end])
print(f"LLM generated config: theme={config.get('theme')}, rooms={config.get('max_rooms')}")
return config
except Exception as e:
print(f"Ollama unavailable ({e}), using procedural fallback")
# Fallback: parse prompt with keyword matching
config = {"theme": "dungeon", "width": 64, "height": 48, "max_rooms": 8,
"min_room_size": 5, "max_room_size": 14, "enemy_density": 0.3,
"chest_probability": 0.4, "boss_room": False, "difficulty": "medium"}
prompt_lower = prompt.lower()
if "cave" in prompt_lower: config["theme"] = "cave"
elif "forest" in prompt_lower: config["theme"] = "forest"
elif "castle" in prompt_lower: config["theme"] = "castle"
if "large" in prompt_lower or "big" in prompt_lower:
config["max_rooms"] = 14
config["width"] = 96
config["height"] = 64
if "hard" in prompt_lower or "difficult" in prompt_lower:
config["difficulty"] = "hard"
config["enemy_density"] = 0.6
return config
# Test it
config = prompt_to_level_config("A large underground cave with lots of treasure but very dangerous")
print(json.dumps(config, indent=2))
Step 3: Export to Godot (.tscn)
def export_to_godot_tilemap(
grid: np.ndarray,
config: dict,
output_path: str = "level.tscn"
):
tile_atlas = {
0: (-1, -1), # Empty (no tile)
1: (0, 0), # Floor
2: (0, 1), # Wall
3: (1, 0), # Door
4: (2, 0), # Spawn
5: (2, 1), # Exit
6: (3, 0) # Chest
}
lines = [
'[gd_scene load_steps=2 format=3]',
'',
'[ext_resource type="TileSet" path="res://tilesets/dungeon.tres" id="1_abc"]',
'',
'[node name="Level" type="Node2D"]',
'',
'[node name="TileMap" type="TileMap" parent="."]',
'tile_set = ExtResource("1_abc")',
'format = 2',
'tile_data = PackedInt32Array(',
]
tile_entries = []
height, width = grid.shape
for y in range(height):
for x in range(width):
tile_id = int(grid[y][x])
atlas_x, atlas_y = tile_atlas.get(tile_id, (-1, -1))
if atlas_x == -1:
continue
cell_key = x | (y << 16)
tile_entries.append(f"{cell_key}, 0, {atlas_x | (atlas_y << 16)}")
lines.append(", ".join(tile_entries))
lines.append(")")
spawn_y, spawn_x = np.argwhere(grid == 4)[0] if np.any(grid == 4) else (1, 1)
lines.extend([
'',
f'[node name="SpawnPoint" type="Marker2D" parent="."]',
f'position = Vector2({spawn_x * 32}, {spawn_y * 32})'
])
with open(output_path, "w") as f:
f.write("\n".join(lines))
print(f"Exported Godot scene: {output_path}")
print(f" Tiles written: {len(tile_entries)}")
# Full pipeline example
user_prompt = "A dark underground dungeon with lots of traps and a boss at the end"
config = prompt_to_level_config(user_prompt)
gen = BSPDungeonGenerator(
width=config.get("width", 64),
height=config.get("height", 48)
)
grid = gen.generate(
max_rooms=config.get("max_rooms", 8),
min_room_size=config.get("min_room_size", 5),
max_room_size=config.get("max_room_size", 14)
)
export_to_godot_tilemap(grid, config, "my_dungeon.tscn")
Exporting to TMX (Tiled / Unity)
def export_to_tmx(grid: np.ndarray, output_path: str = "level.tmx", tile_size: int = 32):
height, width = grid.shape
gid_map = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6}
lines = [
f'<?xml version="1.0" encoding="UTF-8"?>',
f'<map version="1.10" orientation="orthogonal" width="{width}" height="{height}" tilewidth="{tile_size}" tileheight="{tile_size}">',
f' <tileset firstgid="1" source="dungeon.tsx"/>',
f' <layer id="1" name="Ground" width="{width}" height="{height}">',
f' <data encoding="csv">',
]
csv_rows = []
for y in range(height):
row = ",".join(str(gid_map.get(int(grid[y][x]), 0)) for x in range(width))
csv_rows.append(row)
lines.append(",\n".join(csv_rows))
lines.extend([
f' </data>',
f' </layer>',
f'</map>'
])
with open(output_path, "w") as f:
f.write("\n".join(lines))
print(f"Exported TMX: {output_path}")
export_to_tmx(grid, "level.tmx")
Ollama not running? No worries—procedural fallback still gives you a well-structured level. Have Ollama available? Get theme-aware configurations from natural language descriptions.
Looking for more AI game dev tools? Check out my real AI tools at axon.nepa-ai.com.



