Apuntes DAM
Volver al inicio

Ejercicios de JavaScript resueltos

Los 14 ejercicios de JavaScript de la web en una sola hoja: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Desarrollo Web en Entorno Cliente

1. Encuentra el fallo: los turnos que se repiten

Medio · Fundamentos de JavaScript · apuntesdam.com/subject/dwec/topic/fundamentos-javascript

El programa programa tres avisos con setTimeout y cada uno debería decir su número: «Turno 1», «Turno 2» y «Turno 3». En lugar de eso, los tres dicen «Turno 4». Corrígelo cambiando una sola palabra.

Código de partida (javascript)
for (var i = 1; i <= 3; i++) {  setTimeout(() => console.log(`Turno ${i}`), 10 * i);}

Ejemplo: Tres turnos

Salida esperada

Turno 1
Turno 2
Turno 3

2. Encuentra el fallo: el pago que nunca cuadra

Medio · Fundamentos de JavaScript · apuntesdam.com/subject/dwec/topic/fundamentos-javascript

El carrito tiene dos artículos de 0,10 € y 0,20 €, y el cliente paga 0,30 €. El programa dice que falta dinero. Corrige la comparación para que funcione con importes en euros.

Código de partida (javascript)
const carrito = [0.1, 0.2];const total = carrito.reduce((a, b) => a + b, 0);const pagado = 0.3;console.log(total === pagado ? "Pago correcto" : "Falta dinero");

Ejemplo: 0,10 + 0,20 pagando 0,30

Salida esperada

Pago correcto

3. Encuentra el fallo: las notas mal ordenadas

Fácil · Objetos Predefinidos y BOM · apuntesdam.com/subject/dwec/topic/objetos-predefinidos

El programa ordena unas puntuaciones de menor a mayor, pero el 10 y el 25 salen en mal sitio: «10, 2, 25, 7, 9». Corrígelo.

Código de partida (javascript)
const notas = [10, 9, 2, 7, 25];notas.sort();console.log(notas.join(", "));

Ejemplo: Cinco puntuaciones

Salida esperada

2, 7, 9, 10, 25

4. Encuentra el fallo: la copia que no es una copia

Fácil · Funciones, Objetos y Clases · apuntesdam.com/subject/dwec/topic/funciones-objetos

El programa hace una copia de la lista para añadir un elemento sin tocar la original. Al final las dos listas tienen 4 elementos, cuando la original debería seguir con 3. Corrígelo.

Código de partida (javascript)
const original = ["lunes", "martes", "miércoles"];const copia = original;copia.push("jueves");console.log(`Original: ${original.length}, copia: ${copia.length}`);

Ejemplo: Longitudes

Salida esperada

Original: 3, copia: 4

5. Encuentra el fallo: el contador que no cuenta

Difícil · Funciones, Objetos y Clases · apuntesdam.com/subject/dwec/topic/funciones-objetos

El método sumarTodos debería acumular en this.cuenta los números recibidos y devolver 6 para [1, 2, 3], pero devuelve 0. Corrígelo.

Código de partida (javascript)
const contador = {  cuenta: 0,  sumarTodos(numeros) {    numeros.forEach(function (n) {      this.cuenta += n;    });    return this.cuenta;  },};console.log(contador.sumarTodos([1, 2, 3]));

Ejemplo: Sumar 1, 2 y 3

Salida esperada

6

6. Encuentra el fallo: el total que es una promesa

Medio · Asincronía, AJAX y Frameworks · apuntesdam.com/subject/dwec/topic/asincronia-ajax

precioConIva es una función asíncrona. El programa debería mostrar «Total: 121 €», pero muestra «Total: [object Promise] €». Corrígelo.

Código de partida (javascript)
async function precioConIva(precio) {  return precio * 1.21;} async function main() {  const total = precioConIva(100);  console.log(`Total: ${total} €`);}main();

Ejemplo: 100 € con IVA

Salida esperada

Total: 121 €

Ejercicios largos

7. Calendario de un mes con festivos y puentes

Medio · 50 minutos · apuntesdam.com/ejercicios/javascript/calendario-con-festivos-y-puentes

La intranet de una empresa quiere un calendario sencillo en el que cada departamento marque sus festivos y vea de un vistazo cuántos días se trabajan en el mes y qué puentes se pueden pedir. Antes de dibujarlo en la página, la lógica se escribe y se prueba en la consola con Node.js.

En España las semanas empiezan en lunes, pero getDay() numera los días con el domingo como 0. Además, new Date(2026, 1, 1) crea la fecha en la hora local del ordenador, y según la zona horaria el día puede cambiar al pasarla a UTC. Usar Date.UTC y los métodos getUTC… evita las dos trampas.

Un puente es un día laborable que queda encerrado entre un festivo y el fin de semana: si un festivo cae en martes, el lunes anterior; si cae en jueves, el viernes siguiente.

Requisitos

  • La primera línea es año mes (año de 1900 a 2100, mes de 1 a 12); si no es válida, escribe Fecha no válida y nada más. Cada línea siguiente es un festivo: día nombre. Una línea con un día que no existe en ese mes, o sin nombre, escribe Aviso: línea N ignorada (las líneas vacías no avisan). Si un día se repite, vale el último nombre.

  • Escribe el título (Diciembre de 2026) y la cabecera L M X J V S D. Después, una línea por semana: cada día ocupa 3 caracteres, el número alineado a la derecha en 2 y un * si es festivo o un espacio si no. La primera semana empieza con 3 espacios por cada día del mes anterior. Ninguna línea termina en espacios.

  • Festivos: y, por orden de día, una línea por festivo con dos espacios delante: 8 Inmaculada Concepción (martes). Si no hay ninguno, Festivos: ninguno.

  • Días laborables: N: los días de lunes a viernes que no son festivos.

  • Puentes posibles: lunes 7, viernes 24: los lunes anteriores a un festivo en martes y los viernes posteriores a un festivo en jueves, siempre que sean del mismo mes y no sean festivos, por orden de día y sin repetir. Si no hay ninguno, Puentes posibles: ninguno.

Formato de la entrada

  • Línea 1: año mes. Resto: un festivo por línea, día nombre.

Ejemplo: Diciembre con tres festivos

Entrada

2026 12
6 Día de la Constitución
8 Inmaculada Concepción
25 Navidad

Salida esperada

Diciembre de 2026
 L  M  X  J  V  S  D
    1  2  3  4  5  6*
 7  8* 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25*26 27
28 29 30 31
Festivos:
  6 Día de la Constitución (domingo)
  8 Inmaculada Concepción (martes)
  25 Navidad (viernes)
Días laborables: 21
Puentes posibles: lunes 7

Ejemplo: Mayo con líneas que no valen

Entrada

2026 5
1 Fiesta del Trabajo
2 Comunidad de Madrid
40 Día imposible
abc
15 San Isidro

Salida esperada

Aviso: línea 4 ignorada
Aviso: línea 5 ignorada
Mayo de 2026
 L  M  X  J  V  S  D
             1* 2* 3
 4  5  6  7  8  9 10
11 12 13 14 15*16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Festivos:
  1 Fiesta del Trabajo (viernes)
  2 Comunidad de Madrid (sábado)
  15 San Isidro (viernes)
Días laborables: 19
Puentes posibles: ninguno
Código de partida (javascript)
const lineas = require("fs").readFileSync(0, "utf8").split("\n"); const MESES = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];const DIAS = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"]; function main() {  const [anio, mes] = (lineas[0] ?? "").trim().split(/\s+/).map(Number);  // TODO: valida la fecha, lee los festivos y dibuja el calendario  // TODO: festivos, días laborables y puentes posibles} main();

8. Reservas de pistas de pádel con clases

Difícil · 70 minutos · apuntesdam.com/ejercicios/javascript/reservas-de-pistas-de-padel

Un club de pádel quiere una web para reservar pistas desde el móvil. Antes de hacer la interfaz, el equipo escribe el «motor» de reservas como una clase que no sabe nada del HTML: recibe órdenes y devuelve textos. Así se puede probar en la consola con Node.js y después usarla tal cual en la página.

El club tiene 3 pistas y nueve turnos de 90 minutos cada día, de las 09:00 a las 22:30. Por la mañana y a primera hora de la tarde (turnos de las 09:00 a las 15:00) es hora valle y la pista cuesta 12 €; desde las 16:30, hora punta, 18 €. Los socios tienen un 30 % de descuento, y para que todo el mundo pueda jugar nadie puede tener más de dos reservas a la vez.

Requisitos

  • Cada línea es una orden; la palabra de la orden puede ir en mayúsculas o minúsculas. Las líneas vacías se ignoran y una orden desconocida escribe Orden desconocida: palabra.

  • SOCIOS Ana,Luis Mora añade socios (nombres separados por comas, sin espacios alrededor) y escribe Socios: N con el total de socios.

  • RESERVA pista hora nombre (el nombre puede tener espacios) comprueba, en este orden: que la pista sea 1, 2 o 3 (Pista no válida: X), que la hora sea uno de los turnos (Hora no válida: X), que la pista esté libre a esa hora (La pista P ya está reservada a las HH:MM) y que esa persona no tenga ya 2 reservas (Nombre ya tiene 2 reservas). Si todo va bien, la reserva recibe un número (1, 2, 3… sin reutilizar los cancelados) y se escribe Reserva N: pista P a las HH:MM para Nombre (X €).

  • CANCELA n escribe Reserva n cancelada y libera la pista, o No existe la reserva n.

  • LIBRES pista escribe Pista P libre a las 09:00, 10:30, … con sus turnos libres en orden, o Pista P sin horas libres, o Pista no válida: X.

  • RESUMEN escribe una línea por pista, Pista P: R de 9 horas (Q %) con el porcentaje redondeado al entero; Recaudación prevista: X € con la suma de las reservas vigentes; y Mejor cliente: Nombre (N reservas) (la persona con más reservas vigentes; si empatan, la primera alfabéticamente; «reserva» si es 1), o Mejor cliente: ninguno.

  • Los importes se escriben con dos decimales y coma decimal: 12,60 €.

Formato de la entrada

  • Una orden por línea: SOCIOS, RESERVA, CANCELA, LIBRES o RESUMEN.

Turnos y precios
TurnosTipoPrecioSocios (−30 %)
09:00, 10:30, 12:00, 13:30, 15:00valle12,00 €8,40 €
16:30, 18:00, 19:30, 21:00punta18,00 €12,60 €

Ejemplo: Una mañana de reservas

Entrada

SOCIOS Ana,Luis Mora
RESERVA 2 18:00 Ana
RESERVA 1 09:00 Pedro Gil
RESERVA 2 18:00 Pedro Gil
RESERVA 3 10:30 Ana
RESERVA 1 12:00 Ana
RESERVA 4 12:00 Eva
RESERVA 1 11:00 Eva
CANCELA 2
CANCELA 9
LIBRES 1
RESERVA 1 21:00 Luis Mora
RESUMEN

Salida esperada

Socios: 2
Reserva 1: pista 2 a las 18:00 para Ana (12,60 €)
Reserva 2: pista 1 a las 09:00 para Pedro Gil (12,00 €)
La pista 2 ya está reservada a las 18:00
Reserva 3: pista 3 a las 10:30 para Ana (8,40 €)
Ana ya tiene 2 reservas
Pista no válida: 4
Hora no válida: 11:00
Reserva 2 cancelada
No existe la reserva 9
Pista 1 libre a las 09:00, 10:30, 12:00, 13:30, 15:00, 16:30, 18:00, 19:30, 21:00
Reserva 4: pista 1 a las 21:00 para Luis Mora (12,60 €)
Pista 1: 1 de 9 horas (11 %)
Pista 2: 1 de 9 horas (11 %)
Pista 3: 1 de 9 horas (11 %)
Recaudación prevista: 33,60 €
Mejor cliente: Ana (2 reservas)

Ejemplo: Órdenes en minúsculas

Entrada

reserva 3 15:00 Marta
libres 3
resumen
borrar 1

Salida esperada

Reserva 1: pista 3 a las 15:00 para Marta (12,00 €)
Pista 3 libre a las 09:00, 10:30, 12:00, 13:30, 16:30, 18:00, 19:30, 21:00
Pista 1: 0 de 9 horas (0 %)
Pista 2: 0 de 9 horas (0 %)
Pista 3: 1 de 9 horas (11 %)
Recaudación prevista: 12,00 €
Mejor cliente: Marta (1 reserva)
Orden desconocida: borrar
Código de partida (javascript)
const lineas = require("fs").readFileSync(0, "utf8").split("\n"); const HORAS = ["09:00", "10:30", "12:00", "13:30", "15:00", "16:30", "18:00", "19:30", "21:00"];const PISTAS = 3; const euros = (centimos) => `${(centimos / 100).toFixed(2).replace(".", ",")} €`; class Reserva {  constructor(id, pista, hora, nombre, precio) {    this.id = id;    this.pista = pista;    this.hora = hora;    this.nombre = nombre;    this.precio = precio;  }} class Club {  #reservas = new Map();   // id → Reserva  #siguiente = 1;  #socios = new Set();   // TODO: anadirSocios, reservar, cancelar, libres y resumen (cada uno devuelve el texto que se escribe)} const club = new Club();for (const linea of lineas) {  // TODO: SOCIOS, RESERVA, CANCELA, LIBRES y RESUMEN}

9. Validador de formularios de registro

Difícil · 65 minutos · apuntesdam.com/ejercicios/javascript/validador-de-formularios-de-registro

Una web de cursos recibe cientos de altas al día. El formulario ya valida en el navegador, pero eso se puede saltar (basta con desactivar JavaScript o enviar la petición a mano), así que el equipo quiere la misma validación en una función que se pueda ejecutar en el cliente y en el servidor con Node.js. La prueba se hace con un fichero de formularios en JSON, uno por línea.

Cada campo tiene sus reglas y el usuario debe recibir un único mensaje por campo, el del primer problema que se encuentre: no tiene sentido decirle a la vez que la contraseña es corta y que le faltan números. Los nombres pueden tener tildes, eñes, apóstrofos (O'Brien) y guiones (Ruiz-Pérez), así que [a-zA-Z] no sirve: hace falta la clase Unicode \p{L}.

Para que los resultados no dependan del día en que se ejecuten, la edad se calcula a una fecha de referencia fija: el 1 de septiembre de 2026.

Requisitos

  • Cada línea no vacía es un formulario en JSON. Si no es JSON válido, o no es un objeto, escribe Formulario N: JSON no válido (N cuenta todas las líneas no vacías). Los valores se convierten a texto y se les quitan los espacios de los extremos; un campo que falta o es null cuenta como vacío.

  • nombre: obligatorio; entre 2 y 40 caracteres; empieza por una letra y solo tiene letras (de cualquier idioma), espacios, apóstrofos y guiones (solo letras, espacios, apóstrofos y guiones).

  • email: obligatorio; algo sin espacios ni arrobas, una @, algo sin espacios ni arrobas, un punto y al menos dos letras al final (formato no válido).

  • password: obligatoria; al menos 8 caracteres; con mayúsculas, minúsculas y números (necesita mayúsculas, minúsculas y números); y si la primera palabra del nombre tiene 3 letras o más, no puede contenerla sin distinguir mayúsculas (no puede contener tu nombre). confirmacion: si hay contraseña, tiene que ser igual (no coincide con la contraseña).

  • nacimiento: obligatoria, con formato AAAA-MM-DD y una fecha que exista (fecha no válida); la persona tiene que haber cumplido 16 años el 1 de septiembre de 2026 (hay que tener al menos 16 años).

  • cp: obligatorio; 5 cifras con las dos primeras entre 01 y 52 (código postal no válido). telefono: opcional; si se escribe, quitando los espacios deben quedar 9 cifras que empiecen por 6, 7, 8 o 9 (teléfono no válido). Un campo vacío obligatorio da obligatorio.

  • Para cada formulario: Formulario N: correcto, o Formulario N: E errores («error» si es uno) seguido de una línea por campo con error, en el orden nombre, email, password, confirmacion, nacimiento, cp, telefono: - campo: mensaje. Las reglas de cada campo se comprueban en el orden en que aparecen aquí y solo se muestra el primer fallo.

  • Al final, Correctos: C de N y Campo con más errores: campo (veces) (si empatan, el primero en el orden de los campos) o Campo con más errores: ninguno.

Formato de la entrada

  • Un formulario por línea, como objeto JSON con los campos nombre, email, password, confirmacion, nacimiento, cp y telefono.

Ejemplo: Tres altas

Entrada

{"nombre": "Lucía Martín-Ruiz", "email": "lucia@correo.es", "password": "Clave2026", "confirmacion": "Clave2026", "nacimiento": "2004-05-17", "cp": "28013", "telefono": "612 345 678"}
{"nombre": "Ana", "email": "ana@correo", "password": "anaPerez99", "confirmacion": "anaperez99", "nacimiento": "2011-02-30", "cp": "53001"}
{"nombre": "O'Brien", "email": "obrien@mail.ie", "password": "Dublin1916", "confirmacion": "Dublin1916", "nacimiento": "2010-09-01", "cp": 8001}

Salida esperada

Formulario 1: correcto
Formulario 2: 5 errores
  - email: formato no válido
  - password: no puede contener tu nombre
  - confirmacion: no coincide con la contraseña
  - nacimiento: fecha no válida
  - cp: código postal no válido
Formulario 3: 1 error
  - cp: código postal no válido
Correctos: 1 de 3
Campo con más errores: cp (2)

Ejemplo: Formularios incompletos o mal escritos

Entrada

{"email": "  ", "password": "corta"}
{nombre: "sin comillas"}
[1, 2, 3]
{"nombre": "Ñuño Álvarez", "email": "nuno@x.com", "password": "abcdefgh", "nacimiento": "2010-08-31", "cp": "01001", "telefono": "91234567"}

Salida esperada

Formulario 1: 6 errores
  - nombre: obligatorio
  - email: obligatorio
  - password: al menos 8 caracteres
  - confirmacion: no coincide con la contraseña
  - nacimiento: obligatorio
  - cp: obligatorio
Formulario 2: JSON no válido
Formulario 3: JSON no válido
Formulario 4: 3 errores
  - password: necesita mayúsculas, minúsculas y números
  - confirmacion: no coincide con la contraseña
  - telefono: teléfono no válido
Correctos: 0 de 4
Campo con más errores: password (2)
Código de partida (javascript)
const lineas = require("fs").readFileSync(0, "utf8").split("\n"); const HOY = new Date(Date.UTC(2026, 8, 1));   // fecha de referencia: 1 de septiembre de 2026const CAMPOS = ["nombre", "email", "password", "confirmacion", "nacimiento", "cp", "telefono"]; /** Errores de un formulario: un objeto campo → mensaje, como mucho uno por campo. */function validar(datos) {  const errores = {};  // TODO: las reglas de cada campo  return errores;} // TODO: lee cada línea como JSON, valida y escribe el resultado y el resumen

10. Cargador de datos asíncrono con reintentos y dependencias

Muy difícil · 90 minutos · apuntesdam.com/ejercicios/javascript/cargador-asincrono-con-reintentos

El panel de control de una tienda online pide al arrancar los usuarios, los productos, los pedidos, las estadísticas… cada dato a una API distinta. Pedirlos uno detrás de otro haría esperar al usuario la suma de todos los tiempos; pedirlos a la vez, solo lo que tarde el más lento.

Pero las redes fallan. Una petición puede devolver un error (y quizá funcione si se repite) o quedarse colgada para siempre: por eso cada petición tiene un tiempo límite de 100 ms y se reintenta hasta 3 veces. Además, algunos datos dependen de otros (los pedidos necesitan los usuarios): esos tienen que esperar a sus dependencias, y si alguna falla, no se piden.

Como no hay APIs reales, se simulan con setTimeout: cada recurso tarda unos milisegundos y falla las primeras veces que se indique. El informe final va en el orden de la entrada, no en el de llegada, para que sea siempre el mismo.

Requisitos

  • Cada línea es nombre ms fallos [dependencias]: lo que tarda cada petición en milisegundos, cuántas de las primeras peticiones fallan con error del servidor y, opcionalmente, los nombres de los recursos de los que depende separados por comas (sin espacios). Las líneas vacías se ignoran. Una línea con otro formato o con un nombre repetido escribe Aviso: línea N ignorada. No hay dependencias circulares.

  • Todos los recursos empiezan a cargarse a la vez, salvo los que tienen dependencias, que esperan a que terminen todas ellas. Si alguna dependencia no se ha cargado (o no existe), el recurso no se pide.

  • Cada intento dura lo que diga ms, pero si pasan 100 ms sin respuesta se da por fallido con tiempo agotado. Se hacen hasta 3 intentos, uno detrás de otro, y el motivo de error que se informa es el del último.

  • Al terminar todo, una línea por recurso en el orden de la entrada: nombre: cargado (N intentos) («intento» si es 1), nombre: error tras 3 intentos (motivo) u nombre: omitido (depende de dep, que no se ha podido cargar) / (depende de dep, que no existe), indicando la primera dependencia de su lista que ha fallado.

  • Para terminar, Cargados: C de T · Peticiones: P, donde P es el número total de peticiones simuladas que se han hecho.

Formato de la entrada

  • Un recurso por línea: nombre ms fallos y, opcionalmente, dep1,dep2.

Ejemplo: Un panel con seis recursos

Entrada

usuarios 30 0
productos 20 2
fotos 150 0
pedidos 10 0 usuarios,productos
galeria 10 0 fotos
informe 10 0 pedidos,stock

Salida esperada

usuarios: cargado (1 intento)
productos: cargado (3 intentos)
fotos: error tras 3 intentos (tiempo agotado)
pedidos: cargado (1 intento)
galeria: omitido (depende de fotos, que no se ha podido cargar)
informe: omitido (depende de stock, que no existe)
Cargados: 3 de 6 · Peticiones: 8

Ejemplo: Errores y líneas no válidas

Entrada

a 10 3
b 10 1
b 20 0
c diez 0
d 5 0 a

Salida esperada

Aviso: línea 3 ignorada
Aviso: línea 4 ignorada
a: error tras 3 intentos (error del servidor)
b: cargado (2 intentos)
d: omitido (depende de a, que no se ha podido cargar)
Cargados: 1 de 3 · Peticiones: 5
Código de partida (javascript)
const lineas = require("fs").readFileSync(0, "utf8").split("\n"); const LIMITE_MS = 100;const INTENTOS = 3;let peticiones = 0; /** Petición simulada: tarda «ms» y falla mientras el intento sea menor o igual que «fallos». */function peticion(recurso, intento) {  peticiones++;  return new Promise((resolve, reject) => {    setTimeout(() => (intento <= recurso.fallos ? reject(new Error("error del servidor")) : resolve(recurso.nombre)), recurso.ms);  });} /** La primera promesa que termine gana: la petición o el temporizador. */function conTiempoLimite(promesa, ms) {  // TODO} async function conReintentos(recurso) {  // TODO} async function main() {  // TODO: lee los recursos, cárgalos respetando las dependencias y escribe el informe} main();

11. Componente web accesible: valoración con estrellas

Muy difícil · 100 minutos · apuntesdam.com/ejercicios/javascript/componente-web-de-valoracion

La plataforma de cursos de un centro quiere que los alumnos valoren cada curso con estrellas, y el mismo control aparece en varias páginas y en dos aplicaciones distintas. En lugar de copiar el HTML y el JavaScript en cada sitio, se crea un componente: una etiqueta propia, <valoracion-estrellas>, que cualquier página puede usar como si fuera un elemento más de HTML.

Los Web Components son el estándar del navegador para esto: customElements.define registra la etiqueta, una clase que extiende HTMLElement define su comportamiento y el Shadow DOM encapsula su HTML y su CSS para que la página no los rompa ni ellos rompan la página.

Un control de estrellas es, en realidad, un grupo de opciones excluyentes: para un lector de pantalla tiene que comportarse como un grupo de botones de radio, con su nombre, el estado de cada opción y el manejo con las flechas del teclado.

Requisitos

  • Cada apartado reescribe el script completo del componente: empieza con la solución del anterior. La página tiene tres componentes: #v1 (máximo 5, valor 3, etiqueta «Valora el curso»), #v2 (sin atributos) y #v3 (máximo 10, valor 7).

  • El componente usa un Shadow DOM abierto (mode: "open") con un contenedor role="radiogroup" y un button por estrella, cuyo texto es ★.

  • Cada apartado se corrige por separado con pruebas que se ejecutan dentro de la página (algunas a varios anchos de pantalla). Debajo del editor tienes el resultado en directo.

Página de partida (html)
<p>Valora el curso:</p><valoracion-estrellas id="v1" max="5" valor="3" etiqueta="Valora el curso"></valoracion-estrellas><p>Sin valorar todavía:</p><valoracion-estrellas id="v2"></valoracion-estrellas><p>Sobre 10:</p><valoracion-estrellas id="v3" max="10" valor="7"></valoracion-estrellas>

Apartados

  • Define la clase ValoracionEstrellas y regístrala como valoracion-estrellas. Al conectarse a la página dibuja en su Shadow DOM un div con role="radiogroup" y el aria-label del atributo etiqueta (o «Valoración» si no lo tiene), y dentro tantos button como diga el atributo max (5 si no lo tiene), cada uno con el texto ★ y el aria-label «1 estrella», «2 estrellas»…

  • Añade el valor: el atributo valor (0 si no lo tiene, nunca más que max) marca como llenas (clase llena) las estrellas hasta él. Cada botón tiene role="radio" y aria-checked ("true" solo en la estrella del valor). Al pulsar una estrella, el atributo valor cambia, el componente se redibuja y lanza el evento cambio con detail: { valor }, que debe llegar a la página (bubbles y composed).

  • Hazlo usable con el teclado como un grupo de radios: solo la estrella elegida (o la primera, si el valor es 0) tiene tabindex="0" y las demás -1, para que el tabulador entre y salga del grupo de una vez; con las flechas derecha y arriba sube una estrella y con izquierda y abajo baja una (entre 1 y max), igual que al pulsar (atributo, redibujo y evento), y el foco pasa a la estrella elegida.

  • Sincroniza el componente con el exterior: si otro código cambia los atributos valor, max o etiqueta, el componente se redibuja (observedAttributes y attributeChangedCallback). Y añade la propiedad valor: leerla devuelve el número y asignarla (el.valor = 4) cambia el atributo.

12. Informe de ventas imprimible: agrupación, formatos y CSS de impresión

Difícil · 75 minutos · apuntesdam.com/ejercicios/javascript/informe-de-ventas-imprimible

El departamento comercial necesita cada trimestre un informe de ventas que se pueda ver en pantalla y también imprimir o guardar en PDF desde el navegador. Herramientas como JasperReports generan informes desde Java; en una aplicación web, lo habitual es generarlos con HTML y CSS, y el diálogo de impresión del navegador se encarga del PDF.

Los datos llegan como JSON (aquí, dentro de la propia página en un <script type="application/json">). El informe agrupa las ventas por comercial, con un subtotal por grupo y un total general: es la estructura clásica de un informe con grupos y sumas.

Lo que se ve bien en pantalla no tiene por qué imprimirse bien: los botones sobran en el papel, las tablas largas se parten entre páginas y los fondos gastan tinta. Para eso existe la hoja de estilos de impresión.

Requisitos

  • Los datos se leen del elemento #datos con JSON.parse. La tabla se añade dentro de #informe.

  • Cada apartado se corrige por separado con pruebas que se ejecutan dentro de la página (algunas a varios anchos de pantalla). Debajo del editor tienes el resultado en directo.

Página de partida (html)
<h1>Ventas del tercer trimestre</h1><button class="no-imprimir" type="button" onclick="print()">Imprimir</button><div id="informe"></div><script id="datos" type="application/json">[{"comercial": "Luis Mora", "cliente": "Ferretería López", "fecha": "2026-07-03", "importe": 1250.5}, {"comercial": "Ana Gil", "cliente": "Papelería Sol", "fecha": "2026-07-10", "importe": 320.0}, {"comercial": "Ana Gil", "cliente": "Talleres Ruiz", "fecha": "2026-07-02", "importe": 2875.9}, {"comercial": "Eva Ruiz", "cliente": "Hotel Mar", "fecha": "2026-08-14", "importe": 4120.0}, {"comercial": "Luis Mora", "cliente": "Bar Central", "fecha": "2026-08-01", "importe": 89.95}, {"comercial": "Ana Gil", "cliente": "Clínica Norte", "fecha": "2026-09-22", "importe": 1500.0}, {"comercial": "Eva Ruiz", "cliente": "Colegio Sur", "fecha": "2026-09-05", "importe": 760.4}, {"comercial": "Luis Mora", "cliente": "Ferretería López", "fecha": "2026-09-18", "importe": 640.0}]</script>
Hoja de estilos de partida (css)
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }th, td { border-bottom: 1px solid #ccc; padding: 4px 8px; text-align: left; }.importe { text-align: right; }.subtotal td { font-weight: bold; background: #f2f2f2; }.total td { font-weight: bold; border-top: 2px solid #000; }

Apartados

  • Genera la tabla: un thead con las columnas Comercial, Cliente, Fecha e Importe; en el tbody, las ventas agrupadas por comercial (los comerciales por orden alfabético y sus ventas por fecha), y detrás de cada grupo una fila con la clase subtotal: «Total Nombre», dos celdas vacías y la suma. Al final, un tfoot con una fila total: «Total general», dos celdas vacías y la suma de todo. En este apartado, las fechas tal como vienen (2026-07-03) y los importes con dos decimales y punto (1250.50).

  • Formato para personas: importes con Intl.NumberFormat en euros a la española (1.250,50 €) y fechas con Intl.DateTimeFormat como 03/07/2026. Añade un caption con el periodo y el número de ventas: Del 02/07/2026 al 22/09/2026 · 8 ventas, pon scope="col" en los encabezados y la clase importe en las celdas de importe.

  • La hoja de impresión: dentro de @media print, oculta .no-imprimir, pon el texto a 10pt y negro, haz que el thead se repita en cada página (display: table-header-group), evita que una fila se parta entre dos páginas (break-inside: avoid en tr) y quita el fondo de los subtotales. Fuera, una regla @page con tamaño A4 y márgenes de 2cm. Conserva los estilos de pantalla.

13. Física de un juego de plataformas: gravedad, saltos y colisiones

Muy difícil · 100 minutos · apuntesdam.com/ejercicios/javascript/fisica-de-un-juego-de-plataformas

Los juegos de plataformas 2D (de Super Mario a Celeste) se mueven con unas pocas reglas que se aplican en cada fotograma: leer los controles, mover al personaje, aplicar la gravedad y corregir su posición si ha atravesado una pared o el suelo. Motores como Unity o Godot traen esto hecho, pero entenderlo es lo que permite ajustar el «tacto» del juego y depurar los fallos de colisiones.

El nivel es un mapa de casillas de 16×16 píxeles: # es suelo o pared, C una moneda, E la meta, P la posición inicial del jugador y . aire. El jugador es una caja de 12×14 píxeles (una AABB, caja alineada con los ejes) y todo se calcula con números enteros, como en las consolas antiguas: así el resultado es exacto y repetible.

El truco clásico de las colisiones es mover y corregir cada eje por separado: primero en horizontal (si choca con una pared, se pega a ella) y después en vertical (si cae sobre el suelo, se queda encima y puede volver a saltar). Hacerlo con los dos ejes a la vez provoca que el personaje se enganche en las esquinas.

Requisitos

  • La entrada es el mapa (filas del mismo ancho con ., #, C, E y un solo P), una línea --- y las acciones separadas por espacios en una o varias líneas: R (derecha), L (izquierda), J (salto), combinaciones como RJ, . (nada) y repeticiones como R*10. Errores: Mapa no válido: falta el separador ---, …: las filas no miden lo mismo, …: carácter desconocido y …: tiene que haber un jugador (P) (se comprueban en ese orden).

  • El jugador empieza en x = columna × 16 + 2 e y = fila × 16 + 2 (la casilla de P, apoyado en su borde inferior), con velocidad vertical 0 y fuera del suelo.

  • En cada fotograma, en este orden: (1) la velocidad horizontal es +2 con R, −2 con L (0 con las dos o ninguna); se suma a x, se limita al mapa (0 a ancho×16−12) y, si la caja toca alguna casilla #, se pega a ella (a su izquierda si iba hacia la derecha, a su derecha si no); (2) si hay J y está en el suelo, la velocidad vertical pasa a −9; después se le suma la gravedad (1) con un máximo de 8, se suma a y, y si toca un #: si bajaba, se coloca encima (y queda en el suelo); si subía, se coloca debajo; en los dos casos la velocidad vertical pasa a 0; (3) las monedas que toque la caja se recogen (una vez cada una), la meta termina el nivel y, si y llega a filas×16, el jugador ha caído.

  • Una casilla «toca» la caja si se solapan: la caja ocupa de x a x+11 y de y a y+13. Fuera del mapa no hay suelo (se puede saltar por encima de la fila superior y caer por debajo de la inferior).

  • Mensajes: Fotograma N: moneda (k/total) al recoger cada moneda; Fotograma N: ¡meta! Nivel superado con k/total monedas (y termina); Fotograma N: el jugador ha caído al vacío con k/total monedas (y termina). Si se acaban las acciones: Fin de las acciones (N fotogramas): posición (x, y), en el suelo|en el aire, k/total monedas.

Formato de la entrada

  • El mapa, una línea --- y las acciones (R, L, J, combinaciones, . y repeticiones con *).

Constantes del juego
ConstanteValor
Tamaño de casilla16 px
Caja del jugador12 × 14 px
Velocidad horizontal2 px por fotograma
Impulso del salto−9 px por fotograma
Gravedad+1 px por fotograma²
Velocidad máxima de caída8 px por fotograma

Ejemplo: Recorrer el nivel saltando

Entrada

.............C......
..................E.
...............####.
.....C...####.......
P..####.............
####################
---
R*20 RJ R*12 . . R*24 RJ R*14 R*34 RJ R*16 R*30

Salida esperada

Fotograma 42: moneda (1/2)
Fotograma 111: moneda (2/2)
Fotograma 146: ¡meta! Nivel superado con 2/2 monedas

Ejemplo: Mapas que no valen

Entrada

.............C......
..................E.
...............####.
.....C...####.......
P..####.............
####################
---
R*20 RJ R*12 . . R*24 RJ R*14 R*28 RJ R*16 R*30

Salida esperada

Fotograma 42: moneda (1/2)
Fotograma 106: moneda (2/2)
Fin de las acciones (149 fotogramas): posición (284, 66), en el suelo, 2/2 monedas
Código de partida (javascript)
const lineas = require("fs").readFileSync(0, "utf8").split("\n"); const T = 16;                  // tamaño de casilla en píxelesconst ANCHO = 12, ALTO = 14;   // caja del jugadorconst VEL = 2, SALTO = -9, GRAVEDAD = 1, CAIDA_MAX = 8; function main() {  // TODO: lee y valida el mapa, lee las acciones y simula fotograma a fotograma} main();

14. Inteligencia artificial de un enemigo con una máquina de estados

Difícil · 80 minutos · apuntesdam.com/ejercicios/javascript/ia-enemiga-con-maquina-de-estados

Los enemigos de la mayoría de los videojuegos no piensan: siguen una máquina de estados. En cada momento están en un estado (patrullar, perseguir, atacar…) que decide qué hacen, y unas reglas de transición deciden cuándo cambian a otro. Es fácil de programar, de depurar y de ajustar, y por eso motores como Unity (con su Animator) y Godot la usan a todas horas.

Este guardia vigila un pasillo en un mapa de casillas. Patrulla entre dos puntos; si ve al jugador (a 5 casillas o menos y sin que el jugador vaya en sigilo) lo persigue; si lo tiene al lado, ataca; si lo pierde de vista, va al último sitio donde lo vio y espera un poco antes de volver a patrullar. Y si el jugador le baja la vida por debajo de 30, huye hasta poder recuperarse.

La simulación es por turnos: cada línea de la entrada dice dónde está el jugador en ese turno y si ha golpeado al guardia. El programa escribe cada cambio de estado con su motivo, que es exactamente el registro que se activa en un juego para depurar la IA.

Requisitos

  • La primera línea es PATRULLA x1,y1 x2,y2 (si no, La primera línea debe ser PATRULLA x1,y1 x2,y2 y termina). El guardia empieza en el primer punto, en estado PATRULLA, con 100 de vida. Cada línea siguiente es un turno: x,y del jugador y, opcionalmente, SIGILO y GOLPE n. Las líneas vacías no son turnos; una línea con otra forma escribe t=N: línea no válida (y cuenta como turno).

  • En cada turno: primero se resta el golpe (mínimo 0; si la vida llega a 0, t=N: el enemigo cae derrotado y la simulación termina). Después se calcula la distancia Manhattan al jugador: el guardia lo ve si es 5 o menos y no va en sigilo, y entonces recuerda su posición.

  • Transiciones, se aplica solo la primera que se cumpla: cualquier estado salvo HUIDA → HUIDA si la vida es 30 o menos (vida V); PATRULLA → PERSECUCION si lo ve (te ve a distancia D); PERSECUCION → ATAQUE si la distancia es 1 o menos (a tu lado); PERSECUCION → BUSCAR si la distancia es mayor que 8 (te pierde a distancia D); ATAQUE → PERSECUCION si la distancia es mayor que 1 (te alejas a distancia D); BUSCAR → PERSECUCION si lo ve (te vuelve a ver a distancia D); HUIDA → PATRULLA si la vida es 60 o más (recuperado con vida V). Cada cambio escribe t=N: ANTES → DESPUÉS (motivo).

  • Acción del estado (ya cambiado): PATRULLA, un paso hacia el punto de destino (al llegar a uno, el destino pasa a ser el otro); PERSECUCION, un paso hacia el jugador; ATAQUE, 10 de daño al jugador; BUSCAR, un paso hacia la última posición vista y, una vez allí, espera: al cuarto turno de espera escribe t=N: BUSCAR → PATRULLA (no te encuentra) y vuelve a patrullar; HUIDA, un paso alejándose del jugador si está a 8 o menos, o recupera 10 de vida (hasta 100) si está más lejos.

  • Un paso mueve una casilla en el eje en el que queda más distancia (en horizontal si empatan); alejarse es lo mismo en sentido contrario. Al final: Posición final: (x, y) · vida V · daño causado D y Turnos por estado: PATRULLA n, … con los estados que han tenido algún turno, en el orden PATRULLA, PERSECUCION, ATAQUE, BUSCAR, HUIDA. Cada turno cuenta para el estado en el que termina (el turno en que cae derrotado, para el estado que tenía).

Formato de la entrada

  • Línea 1: PATRULLA x1,y1 x2,y2. Resto: un turno por línea, x,y [SIGILO] [GOLPE n].

Estados y transiciones
EstadoQué haceSale hacia
PATRULLAva y vuelve entre los dos puntosPERSECUCION (te ve) · HUIDA (vida ≤ 30)
PERSECUCIONun paso hacia tiATAQUE (distancia ≤ 1) · BUSCAR (distancia > 8) · HUIDA
ATAQUE10 de daño por turnoPERSECUCION (distancia > 1) · HUIDA
BUSCARva a donde te vio y espera 3 turnosPERSECUCION (te ve) · PATRULLA (no te encuentra) · HUIDA
HUIDAse aleja o, lejos, recupera 10 de vidaPATRULLA (vida ≥ 60)

Ejemplo: Te ve, te persigue, ataca y te pierde

Entrada

PATRULLA 0,0 4,0
9,5
8,4
6,2
4,1
3,2
3,2
3,2
3,2
5,3
9,6
12,8
14,9
15,9
15,9
15,9
15,9
15,9
15,9
15,9

Salida esperada

t=4: PATRULLA → PERSECUCION (te ve a distancia 2)
t=7: PERSECUCION → ATAQUE (a tu lado)
t=9: ATAQUE → PERSECUCION (te alejas a distancia 4)
t=10: PERSECUCION → BUSCAR (te pierde a distancia 10)
t=16: BUSCAR → PATRULLA (no te encuentra)
Posición final: (4, 1) · vida 100 · daño causado 20
Turnos por estado: PATRULLA 7, PERSECUCION 4, ATAQUE 2, BUSCAR 6

Ejemplo: Sigilo, golpes y huida

Entrada

PATRULLA 0,0 0,3
2,0 SIGILO
1,0 SIGILO GOLPE 40
1,0 GOLPE 35
2,0
3,0
4,0
5,0
12,0
12,0
12,0
12,0
12,0

Salida esperada

t=3: PATRULLA → HUIDA (vida 25)
t=10: HUIDA → PATRULLA (recuperado con vida 65)
Posición final: (0, 2) · vida 65 · daño causado 0
Turnos por estado: PATRULLA 5, HUIDA 7
Código de partida (javascript)
const lineas = require("fs").readFileSync(0, "utf8").split("\n"); const VISION = 5, PERDER = 8, ESPERA = 3, VIDA_MAX = 100, HUIR = 30, RECUPERADO = 60; const distancia = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); function main() {  // TODO: lee la ruta de patrulla y, turno a turno, aplica las transiciones y la acción de cada estado} main();