CodeQuest RPG

live

RPG en el navegador donde el combate es resolver código real: escribes JavaScript en un editor de verdad y tu solución se ejecuta en un Web Worker aislado.

Educación

cómo llegó hasta aquí

Línea de evolución

Seis fases reconstruyendo el proyecto sin dejarlo roto entre pasos. Cada una se despliega con su detalle y su rango de commits.

1AuditoríaFase 1 de 6Diagnóstico honesto de una PoC de trivia abandonada.

El proyecto era una capa de trivia de opción múltiple con temática de RPG: sin persistencia, sin tipado, sin tests. La auditoría decidió qué salvar (la ambientación y el mapa de zonas) y qué tirar por completo (la mecánica de trivia).

commitse9b911f–9a103c6

2RefactorFase 2 de 6Estado centralizado con Zustand, persistencia y TypeScript progresivo.

Se sustituyó el estado disperso por un único store organizado en dominios, con persist(partialize) y migración de JS a TS archivo por archivo, verificando el juego jugable en cada paso.

commits2d810d8

3Core loopFase 3 de 6La trivia se reemplaza por retos de código reales en un Web Worker aislado.

Editor CodeMirror real + ejecución en un Worker con timeout gestionado desde el hilo principal; encima, un sistema de maestría y hechizos con efectos mecánicos reales, no decorativos.

commits01b8f2d–76d6603

4ContenidoFase 4 de 6De 4 a 11 retos, cubriendo las 6 zonas y conceptos.

Ampliación en dos tandas (7 retos/3 zonas, luego 4 retos más) para que los 6 conceptos tuvieran cobertura real y los hechizos fueran alcanzables sin repetir zona.

commits17fb41b, 99c2ecb

5CI/CDFase 5 de 6Pipeline de typecheck, lint, Vitest y Playwright, con despliegue documentado.

GitHub Actions corre la suite completa en cada push; el deploy a Vercel usa la CLI (no la integración nativa) para que el mismo pipeline decida qué se despliega, y se salta en silencio si faltan los secrets.

commits9ef5487–f2d5c9e

6RediseñoFase 6 de 6Identidad pixel-art retro en 5 pasos, sin romper el juego entre pasos.

Paleta y tipografía retro, luego HP por segmentos, animaciones con steps(), sonido vía Web Audio API y, para cerrar, el overlay CRT. Cada paso se integró jugable de principio a fin.

commits7998db1–95fb652

por qué existe

Contexto

El proyecto empezó como una prueba de concepto abandonada a medias: una capa de trivia de opción múltiple con temática de RPG, sin persistencia, sin tipado y sin tests. Se retomó con una auditoría técnica honesta y se reconstruyó en fases incrementales, sin dejar el juego roto entre pasos.

lo más difícil

Reto técnico

eval() o new Function() en el hilo principal comparte el mismo scope de ejecución que el resto de la app: un bucle infinito bloquea la UI de forma irrecuperable. Un Web Worker es un hilo real y aislado, sin memoria compartida ni acceso al DOM — y, el motivo decisivo, si el jugador escribe un bucle infinito, el hilo principal puede matarlo desde fuera. El propio worker no puede resolver su timeout si está colgado en un bucle síncrono, así que el reloj y el worker.terminate() viven deliberadamente en lib/codeRunner.ts (hilo principal), no dentro del worker.

el porqué, no solo el qué

Decisiones técnicas

01

key={challenge.id} en vez de useEffect para resetear el editor

La primera versión usaba un useEffect que llamaba a setCode/setStatus al detectar un challenge.id distinto; eslint-plugin-react-hooks lo marcó como antipatrón (un setState síncrono dentro de un efecto provoca renders en cascada). Remontar el subárbol con <ChallengeRunner key={challenge.id} /> hace que React destruya y recree la instancia en cada reto, con estado limpio sin sincronización manual.

02

TypeScript progresivo, no una reescritura completa

La migración de .js/.jsx a .ts/.tsx se hizo archivo por archivo, verificando build y juego jugable en cada paso, en vez de una reescritura de una sola vez que arriesgaría romper algo a mitad de camino sin darse cuenta.

03

Code-splitting de CodeMirror

CodeMirror es, con diferencia, la dependencia más pesada del proyecto. Cargar ChallengeScreen con React.lazy() + Suspense evita que TitleScreen y WorldMap paguen ese coste: el bundle inicial pasó de 734 kB a 216 kB (241,96 kB a 68,77 kB gzip), casi un 70% menos de JS en la carga inicial.

dónde está hoy

Resultado

con qué está construido

Stack

React 19

frontend

Rendimiento y estabilidad para una SPA con pantallas que se montan y desmontan mucho (mapa ↔ reto).

TypeScript (strict)

tooling

Migración progresiva desde JS sin tipos, archivo por archivo; strict detecta los mismos bugs de estado que antes solo aparecían jugando.

Vite

tooling

Arranque y HMR instantáneos, clave para iterar rápido en un proyecto que se reconstruyó por fases incrementales.

Zustand + persist

frontend

Un único store organizado por dominios (player, challenge, session, skills) sin la ceremonia de Redux; persist(partialize) guarda solo player/skills, no la sesión de un reto en curso.

CodeMirror 6

frontend

Editor con resaltado de sintaxis real, no un <textarea> — necesario para que "el combate es código real" se sienta de verdad.

Vitest

tooling

Testea la lógica de negocio (store, sandbox, codeRunner) sin levantar un Worker real ni mockear timers.

Playwright

tooling

Un único e2e del flujo completo (título, mapa, reto real y resultados), deliberadamente el único test que toca el navegador.

GitHub Actions

tooling

CI gratuito para un proyecto personal: typecheck, lint, Vitest y Playwright antes de cada merge.

pruébalo tú

Demo en vivo

comprobando…
codequest-rpg.vercel.appAbrir en pestaña

La aplicación real, embebida aquí. No se carga hasta que la pides, para no penalizar la carga de esta página.

Ver el resto de proyectos