Generador web de mapas de fantasía procedurales, deterministas y "vivos": rellenas un cuestionario breve y obtienes un mundo envejecido —civilizaciones que nacen y caen, guerras, una plaga, figuras legendarias— renderizado como un mapa SVG interactivo con estética de libro de fantasía. Inspirado en el modo leyendas de Dwarf Fortress, pero moderno.
Construido con Angular 21 + TypeScript, terreno Voronoi (d3-delaunay) + ruido
simplex-noise, y un motor de simulación propio determinista por semilla. La IA es
opcional y solo adorna (nombres y narrativa); nunca cambia los hechos del mundo.
Arranca la app (ver más abajo) y abre http://localhost:4200. El cuestionario pide:
| Campo | Qué controla |
|---|---|
| World name | La semilla. Mismo nombre → exactamente el mismo mundo (determinista). |
| Cells | Densidad del terreno Voronoi (~300–600). |
| Sea level | Proporción de océano frente a tierra. |
| World lifespan | Años que simula el motor antes de "congelar" el presente. |
| Factions | Número de civilizaciones iniciales (2–6). |
| Add AI flavour (opcional) | Activa el enriquecimiento por IA con tu propia clave (ver abajo). Apagado por defecto. |
Pulsa Generate World. Tras la pantalla de forja aparece el mapa interactivo:
- Click en cualquier celda → panel lateral con su lore, asentamientos, figuras y eventos.
- Slider temporal (cabecera) → reproduce la historia año a año. Las bandas de era (founding · expansion · conflict · decay) muestran en qué etapa de la vida del mundo estás; la crónica se reescribe por capítulos según el año visible.
- Copy link → comparte el mundo exacto vía URL (la semilla y los parámetros van en el enlace; la clave de IA nunca).
Por defecto la app funciona 100 % sin clave ni red: genera nombres y lore deterministas a partir de la semilla mediante un fallback compartido entre frontend y backend.
Si activas Add AI flavour, introduces en la propia UI:
- Tone — un estilo libre (
runepunk,dark elves,grimdark,high magic…). - API base URL — cualquier endpoint compatible con OpenAI (
https://api.openai.com/v1,https://api.deepseek.com, etc.). - Model — p. ej.
gpt-4o-mini,deepseek-chat. - API key — tu clave.
El servidor no guarda ninguna clave. Cada petición a
POST /api/enrichlleva las credenciales del usuario, se usan para esa única llamada y no se almacenan ni se registran. No hay secretos que configurar en el despliegue. Si la llamada falla o no hay credenciales, se devuelve el fallback determinista automáticamente.
SSRF guard: como la
base URLla controla el cliente, el backend exigehttpsy rechaza cualquier host que sea (o resuelva a) una IP privada/reservada. Para un LLM local en desarrollo, arranca el servidor conLLM_ALLOW_PRIVATE=true.
npm install
npm run dev # backend (puerto 3000) + frontend (puerto 4200) en paraleloO por separado:
npm run start:server # solo backend → http://localhost:3000
npm start # solo frontend → http://localhost:4200 (proxy /api → :3000)Build de producción:
npm run build # bundle Angular en dist/loreforge/browserUna sola imagen construye la SPA y arranca el servidor Node, que sirve a la vez el estático
y el endpoint /api/enrich en el puerto 3000. Al ser BYO-key, no hay secretos que configurar.
docker compose up -d --build # levanta http://localhost:3000Equivale a:
docker build -t loreforge .
docker run -p 3000:3000 loreforgeVariables opcionales: PORT (puerto HTTP), STATIC_DIR (ruta de la SPA construida, ya fijada
en el Dockerfile) y LLM_ALLOW_PRIVATE=true (permitir endpoints LLM en red privada).
node scripts/test-determinism.mjs # terreno + simulación
node scripts/test-sim-determinism.mjs # solo simulaciónGeneran el mismo mundo dos veces con la misma semilla y verifican que son idénticos.
server/
index.ts # Express: POST /api/enrich (BYO-key) + sirve la SPA en prod + SSRF guard
src/
engine/ # Motor puro TS (sin Angular, sin Node) — fuente de verdad del mundo
types.ts # Tipos: Cell, Faction, Settlement, Figure, WorldEvent, World
terrain.ts # Generación Voronoi + simplex-noise
simulation.ts # Expansión, guerra, plaga y eras deterministas
lore-fallback.ts# Fallback determinista + tipos del DTO/LlmResult (frontend + backend)
index.ts
app/
services/
llm.service.ts # POST /api/enrich con FlavourConfig opcional + fallback local
world-generator.service.ts # Orquesta terreno → simulación → enriquecimiento
components/
map-view/ # Render SVG interactivo (celdas, asentamientos, slider)
info-panel/ # Paneles bento con lore, crónica por eras y leyendas
app.ts / app.html / app.css # Raíz: cuestionario + visor
scripts/
test-determinism.mjs # Verifica determinismo en Node
Dockerfile / docker-compose.yml # Despliegue single-container
proxy.conf.json # Redirige /api → localhost:3000 en dev
.env.example # Opciones del servidor (sin secretos: es BYO-key)
Browser (DTO compacto: seed, factions[], settlements[], figures[], events[] +
└─ POST /api/enrich tone/baseUrl/model/apiKey del usuario, si los hay)
│
▼
Express backend (server/index.ts)
│ ¿llegan credenciales del usuario en el body?
├── sí → assertSafeProviderUrl() (SSRF) → llamada OpenAI-compatible (json_object)
└── no → buildFallbackResult() determinista
│ (las credenciales se usan solo para esta llamada; nunca se guardan ni se loguean)
▼
LlmResult { factions[{name,capitalName,lore}], lore[], legends[], chronicle, eraChronicles }
│
▼
Browser renderiza el mapa con nombres + lore + crónica por eras
Si el fetch falla (backend caído), LlmService ejecuta buildFallbackResult() localmente,
así que la app funciona completamente offline y sin backend.
MIT © develatter

