Proyectos · JavaScript · Intermedio

Juego de preguntas

Un cuestionario con pantallas de inicio, partida y resultado: opciones que se colorean al responder, barra de progreso, preguntas mezcladas y 15 segundos para cada una.

  • 5 pasos
  • 1 hora y cuarto aproximadamente
  • 222 líneas de código
  • Editor en vivo

Lo que vas a construir

Un cuestionario es el ejemplo perfecto para separar los datos (las preguntas) de la forma de mostrarlos. Vas a guardar las preguntas en un array, generar los botones con JavaScript, llevar la puntuación y pasar de una pantalla a otra; al final, mezclarás preguntas y respuestas y pondrás un límite de tiempo.

Lo que vas a practicar

  • Separar datos y presentación
  • Generar botones desde un array
  • Llevar la puntuación en variables
  • Mostrar y ocultar pantallas con hidden
  • Mezclar con Fisher-Yates y un temporizador
Resultado final

Antes de empezar, conviene haber hecho estas lecciones

Paso 1 de 5

Tres pantallas en HTML

El juego tiene tres pantallas (inicio, partida y resultado), cada una en una <section>. Las que no se ven llevan el atributo hidden; JavaScript lo quitará o lo pondrá para pasar de una a otra.

Los estilos ya incluyen los colores de las respuestas acertadas y falladas que usarás más adelante.

index.html
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>¿Cuánto sabes de informática?</title>
  <link rel="stylesheet" href="estilos.css">
</head>
<body>
  <main class="juego">
    <section id="inicio">
      <h1>¿Cuánto sabes de informática?</h1>
      <p>Seis preguntas. Elige la respuesta correcta antes de que se acabe el tiempo.</p>
      <button type="button" id="empezar">Empezar</button>
    </section>

    <section id="partida" hidden>
      <div class="cabecera">
        <span id="numero">Pregunta 1 de 6</span>
        <span id="reloj" class="reloj"></span>
      </div>
      <div class="progreso"><span id="progreso"></span></div>
      <h2 id="pregunta"></h2>
      <div id="opciones" class="opciones"></div>
      <p id="aviso" class="aviso" aria-live="polite"></p>
      <button type="button" id="siguiente" hidden>Siguiente</button>
    </section>

    <section id="final" hidden>
      <h2>Resultado</h2>
      <p id="puntuacion" class="puntuacion"></p>
      <button type="button" id="otra">Jugar otra vez</button>
    </section>
  </main>
  <script src="app.js"></script>
</body>
</html>
estilos.css
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 16px;
  background: #faf5ff;
  color: #3b0764;
  font-family: system-ui, sans-serif;
}

.juego {
  width: min(100%, 520px);
  padding: 28px;
  border-radius: 20px;
  background: #ffffff;
  box-shadow: 0 12px 32px rgb(59 7 100 / 0.12);
}

h1, h2 { margin-top: 0; }

button {
  padding: 12px 18px;
  border: 0;
  border-radius: 12px;
  background: #7e22ce;
  color: #ffffff;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
button:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }

.cabecera { display: flex; justify-content: space-between; font-weight: 600; }
.reloj { font-variant-numeric: tabular-nums; }

.progreso { height: 6px; margin: 10px 0 20px; border-radius: 999px; background: #f3e8ff; overflow: hidden; }
.progreso span { display: block; height: 100%; width: 0; background: #a855f7; transition: width 300ms ease; }

.opciones { display: grid; gap: 10px; }
.opciones button {
  padding: 14px;
  border: 2px solid #e9d5ff;
  background: #ffffff;
  color: inherit;
  text-align: left;
  font-weight: 500;
}
.opciones button:hover:not(:disabled) { border-color: #a855f7; }
.opciones button.correcta { border-color: #16a34a; background: #dcfce7; }
.opciones button.incorrecta { border-color: #dc2626; background: #fee2e2; }

.aviso { min-height: 1.5em; font-weight: 600; }
.puntuacion { font-size: 1.4rem; }
app.js
// El JavaScript llega en el paso 2

Paso 2 de 5

Las preguntas como datos

Cada pregunta es un objeto con su texto, sus opciones y la posición de la correcta. Así añadir preguntas es añadir datos, sin tocar el resto del código.

mostrarPregunta crea un botón por opción con map y los pone todos de golpe con replaceChildren. Cada botón guarda su posición en data-opcion.

app.js
const PREGUNTAS = [
  { texto: "¿Qué significan las siglas CPU?", opciones: ["Unidad central de procesamiento", "Copia de protección universal", "Centro de programas útiles"], correcta: 0 },
  { texto: "¿Cuántos bits tiene un byte?", opciones: ["4", "8", "16"], correcta: 1 },
  { texto: "¿Qué protocolo usan las webs para cifrar la conexión?", opciones: ["HTTP", "FTP", "HTTPS"], correcta: 2 },
  { texto: "¿Qué lenguaje se usa para dar estilo a las páginas web?", opciones: ["CSS", "SQL", "Python"], correcta: 0 },
  { texto: "¿Qué memoria se vacía al apagar el ordenador?", opciones: ["El disco SSD", "La RAM", "La memoria USB"], correcta: 1 },
  { texto: "¿Qué hace un servidor DNS?", opciones: ["Guarda copias de seguridad", "Bloquea virus", "Traduce nombres de dominio en direcciones IP"], correcta: 2 },
];

const inicio = document.querySelector("#inicio");
const partida = document.querySelector("#partida");
const textoPregunta = document.querySelector("#pregunta");
const opciones = document.querySelector("#opciones");
const numero = document.querySelector("#numero");

let indice = 0;

function mostrarPregunta() {
  const pregunta = PREGUNTAS[indice];
  numero.textContent = `Pregunta ${indice + 1} de ${PREGUNTAS.length}`;
  textoPregunta.textContent = pregunta.texto;
  opciones.replaceChildren(
    ...pregunta.opciones.map((texto, i) => {
      const boton = document.createElement("button");
      boton.type = "button";
      boton.textContent = texto;
      boton.dataset.opcion = i;
      return boton;
    }),
  );
}

document.querySelector("#empezar").addEventListener("click", () => {
  inicio.hidden = true;
  partida.hidden = false;
  mostrarPregunta();
});

Paso 3 de 5

Responder y puntuar

Al pulsar una opción se desactivan todas (no se puede cambiar de idea), se marca en verde la correcta y, si te equivocaste, en rojo la tuya. Si aciertas, aciertos sube.

El botón «Siguiente» aparece y recibe el foco, para que con el teclado baste con pulsar Intro para seguir.

app.jsLo nuevo, resaltado
const PREGUNTAS = [
  { texto: "¿Qué significan las siglas CPU?", opciones: ["Unidad central de procesamiento", "Copia de protección universal", "Centro de programas útiles"], correcta: 0 },
  { texto: "¿Cuántos bits tiene un byte?", opciones: ["4", "8", "16"], correcta: 1 },
  { texto: "¿Qué protocolo usan las webs para cifrar la conexión?", opciones: ["HTTP", "FTP", "HTTPS"], correcta: 2 },
  { texto: "¿Qué lenguaje se usa para dar estilo a las páginas web?", opciones: ["CSS", "SQL", "Python"], correcta: 0 },
  { texto: "¿Qué memoria se vacía al apagar el ordenador?", opciones: ["El disco SSD", "La RAM", "La memoria USB"], correcta: 1 },
  { texto: "¿Qué hace un servidor DNS?", opciones: ["Guarda copias de seguridad", "Bloquea virus", "Traduce nombres de dominio en direcciones IP"], correcta: 2 },
];

const inicio = document.querySelector("#inicio");
const partida = document.querySelector("#partida");
const textoPregunta = document.querySelector("#pregunta");
const opciones = document.querySelector("#opciones");
const numero = document.querySelector("#numero");
const aviso = document.querySelector("#aviso");
const siguiente = document.querySelector("#siguiente");

let indice = 0;
let aciertos = 0;

function mostrarPregunta() {
  const pregunta = PREGUNTAS[indice];
  numero.textContent = `Pregunta ${indice + 1} de ${PREGUNTAS.length}`;
  textoPregunta.textContent = pregunta.texto;
  aviso.textContent = "";
  siguiente.hidden = true;
  opciones.replaceChildren(
    ...pregunta.opciones.map((texto, i) => {
      const boton = document.createElement("button");
      boton.type = "button";
      boton.textContent = texto;
      boton.dataset.opcion = i;
      return boton;
    }),
  );
}

function responder(elegida) {
  const correcta = PREGUNTAS[indice].correcta;
  for (const boton of opciones.children) {
    boton.disabled = true; // ya no se puede cambiar la respuesta
    if (Number(boton.dataset.opcion) === correcta) boton.classList.add("correcta");
  }
  if (elegida === correcta) {
    aciertos += 1;
    aviso.textContent = "¡Correcto!";
  } else {
    opciones.children[elegida].classList.add("incorrecta");
    aviso.textContent = "No era esa.";
  }
  siguiente.hidden = false;
  siguiente.focus();
}

opciones.addEventListener("click", (event) => {
  const boton = event.target.closest("button");
  if (boton && !boton.disabled) responder(Number(boton.dataset.opcion));
});

document.querySelector("#empezar").addEventListener("click", () => {
  inicio.hidden = true;
  partida.hidden = false;
  mostrarPregunta();
});

Paso 4 de 5

Progreso y resultado

«Siguiente» avanza el índice; al pasar de la última pregunta se muestra la pantalla final con la puntuación y un comentario según el resultado.

La barra de progreso cambia su ancho con style.width, y «Jugar otra vez» reutiliza la misma función empezar poniendo los contadores a cero.

app.jsLo nuevo, resaltado
const PREGUNTAS = [
  { texto: "¿Qué significan las siglas CPU?", opciones: ["Unidad central de procesamiento", "Copia de protección universal", "Centro de programas útiles"], correcta: 0 },
  { texto: "¿Cuántos bits tiene un byte?", opciones: ["4", "8", "16"], correcta: 1 },
  { texto: "¿Qué protocolo usan las webs para cifrar la conexión?", opciones: ["HTTP", "FTP", "HTTPS"], correcta: 2 },
  { texto: "¿Qué lenguaje se usa para dar estilo a las páginas web?", opciones: ["CSS", "SQL", "Python"], correcta: 0 },
  { texto: "¿Qué memoria se vacía al apagar el ordenador?", opciones: ["El disco SSD", "La RAM", "La memoria USB"], correcta: 1 },
  { texto: "¿Qué hace un servidor DNS?", opciones: ["Guarda copias de seguridad", "Bloquea virus", "Traduce nombres de dominio en direcciones IP"], correcta: 2 },
];

const inicio = document.querySelector("#inicio");
const partida = document.querySelector("#partida");
const textoPregunta = document.querySelector("#pregunta");
const opciones = document.querySelector("#opciones");
const numero = document.querySelector("#numero");
const aviso = document.querySelector("#aviso");
const siguiente = document.querySelector("#siguiente");
const final = document.querySelector("#final");
const puntuacion = document.querySelector("#puntuacion");
const progreso = document.querySelector("#progreso");

let indice = 0;
let aciertos = 0;

function mostrarPregunta() {
  const pregunta = PREGUNTAS[indice];
  numero.textContent = `Pregunta ${indice + 1} de ${PREGUNTAS.length}`;
  textoPregunta.textContent = pregunta.texto;
  progreso.style.width = `${(indice / PREGUNTAS.length) * 100}%`;
  aviso.textContent = "";
  siguiente.hidden = true;
  opciones.replaceChildren(
    ...pregunta.opciones.map((texto, i) => {
      const boton = document.createElement("button");
      boton.type = "button";
      boton.textContent = texto;
      boton.dataset.opcion = i;
      return boton;
    }),
  );
}

function responder(elegida) {
  const correcta = PREGUNTAS[indice].correcta;
  for (const boton of opciones.children) {
    boton.disabled = true; // ya no se puede cambiar la respuesta
    if (Number(boton.dataset.opcion) === correcta) boton.classList.add("correcta");
  }
  if (elegida === correcta) {
    aciertos += 1;
    aviso.textContent = "¡Correcto!";
  } else {
    opciones.children[elegida].classList.add("incorrecta");
    aviso.textContent = "No era esa.";
  }
  siguiente.hidden = false;
  siguiente.focus();
}

opciones.addEventListener("click", (event) => {
  const boton = event.target.closest("button");
  if (boton && !boton.disabled) responder(Number(boton.dataset.opcion));
});

function terminar() {
  partida.hidden = true;
  final.hidden = false;
  const nota = aciertos === PREGUNTAS.length ? "¡Perfecto!" : aciertos >= PREGUNTAS.length / 2 ? "¡Bien hecho!" : "Sigue practicando.";
  puntuacion.textContent = `Has acertado ${aciertos} de ${PREGUNTAS.length}. ${nota}`;
}

function empezar() {
  indice = 0;
  aciertos = 0;
  inicio.hidden = true;
  final.hidden = true;
  partida.hidden = false;
  mostrarPregunta();
}

siguiente.addEventListener("click", () => {
  indice += 1;
  if (indice < PREGUNTAS.length) mostrarPregunta();
  else terminar();
});

document.querySelector("#empezar").addEventListener("click", empezar);
document.querySelector("#otra").addEventListener("click", empezar);

Paso 5 de 5

Mezcla y cuenta atrás

Para que cada partida sea distinta, prepararPartida mezcla las preguntas y también las opciones de cada una; como la correcta cambia de posición, se recalcula con findIndex. Aquí basta con Math.random(), porque no hay nada que proteger.

Cada pregunta tiene 15 segundos: un setInterval descuenta y, al llegar a cero, cuenta como fallo. Responder (o agotar el tiempo) detiene el reloj con clearInterval.

app.jsLo nuevo, resaltado
const PREGUNTAS = [
  { texto: "¿Qué significan las siglas CPU?", opciones: ["Unidad central de procesamiento", "Copia de protección universal", "Centro de programas útiles"], correcta: 0 },
  { texto: "¿Cuántos bits tiene un byte?", opciones: ["4", "8", "16"], correcta: 1 },
  { texto: "¿Qué protocolo usan las webs para cifrar la conexión?", opciones: ["HTTP", "FTP", "HTTPS"], correcta: 2 },
  { texto: "¿Qué lenguaje se usa para dar estilo a las páginas web?", opciones: ["CSS", "SQL", "Python"], correcta: 0 },
  { texto: "¿Qué memoria se vacía al apagar el ordenador?", opciones: ["El disco SSD", "La RAM", "La memoria USB"], correcta: 1 },
  { texto: "¿Qué hace un servidor DNS?", opciones: ["Guarda copias de seguridad", "Bloquea virus", "Traduce nombres de dominio en direcciones IP"], correcta: 2 },
];

const inicio = document.querySelector("#inicio");
const partida = document.querySelector("#partida");
const textoPregunta = document.querySelector("#pregunta");
const opciones = document.querySelector("#opciones");
const numero = document.querySelector("#numero");
const aviso = document.querySelector("#aviso");
const siguiente = document.querySelector("#siguiente");
const final = document.querySelector("#final");
const puntuacion = document.querySelector("#puntuacion");
const progreso = document.querySelector("#progreso");

const SEGUNDOS = 15;
const reloj = document.querySelector("#reloj");

let orden = []; // las preguntas de esta partida, ya mezcladas
let indice = 0;
let aciertos = 0;
let cuentaAtras = null;

// Mezcla una copia de la lista (algoritmo de Fisher-Yates)
function mezclar(lista) {
  const copia = [...lista];
  for (let i = copia.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [copia[i], copia[j]] = [copia[j], copia[i]];
  }
  return copia;
}

// Mezcla también las opciones y recalcula cuál es la correcta
function prepararPartida() {
  return mezclar(PREGUNTAS).map((p) => {
    const opcionesMezcladas = mezclar(p.opciones.map((texto, i) => ({ texto, correcta: i === p.correcta })));
    return {
      texto: p.texto,
      opciones: opcionesMezcladas.map((o) => o.texto),
      correcta: opcionesMezcladas.findIndex((o) => o.correcta),
    };
  });
}

function arrancarReloj() {
  let quedan = SEGUNDOS;
  reloj.textContent = `${quedan} s`;
  cuentaAtras = setInterval(() => {
    quedan -= 1;
    reloj.textContent = `${quedan} s`;
    if (quedan === 0) responder(-1); // se acabó el tiempo
  }, 1000);
}

function mostrarPregunta() {
  const pregunta = orden[indice];
  numero.textContent = `Pregunta ${indice + 1} de ${PREGUNTAS.length}`;
  textoPregunta.textContent = pregunta.texto;
  progreso.style.width = `${(indice / PREGUNTAS.length) * 100}%`;
  aviso.textContent = "";
  siguiente.hidden = true;
  arrancarReloj();
  opciones.replaceChildren(
    ...pregunta.opciones.map((texto, i) => {
      const boton = document.createElement("button");
      boton.type = "button";
      boton.textContent = texto;
      boton.dataset.opcion = i;
      return boton;
    }),
  );
}

function responder(elegida) {
  clearInterval(cuentaAtras);
  const correcta = orden[indice].correcta;
  for (const boton of opciones.children) {
    boton.disabled = true; // ya no se puede cambiar la respuesta
    if (Number(boton.dataset.opcion) === correcta) boton.classList.add("correcta");
  }
  if (elegida === correcta) {
    aciertos += 1;
    aviso.textContent = "¡Correcto!";
  } else if (elegida === -1) {
    aviso.textContent = "¡Se acabó el tiempo!";
  } else {
    opciones.children[elegida].classList.add("incorrecta");
    aviso.textContent = "No era esa.";
  }
  siguiente.hidden = false;
  siguiente.focus();
}

opciones.addEventListener("click", (event) => {
  const boton = event.target.closest("button");
  if (boton && !boton.disabled) responder(Number(boton.dataset.opcion));
});

function terminar() {
  partida.hidden = true;
  final.hidden = false;
  const nota = aciertos === PREGUNTAS.length ? "¡Perfecto!" : aciertos >= PREGUNTAS.length / 2 ? "¡Bien hecho!" : "Sigue practicando.";
  puntuacion.textContent = `Has acertado ${aciertos} de ${PREGUNTAS.length}. ${nota}`;
}

function empezar() {
  orden = prepararPartida();
  indice = 0;
  aciertos = 0;
  inicio.hidden = true;
  final.hidden = true;
  partida.hidden = false;
  mostrarPregunta();
}

siguiente.addEventListener("click", () => {
  indice += 1;
  if (indice < PREGUNTAS.length) mostrarPregunta();
  else terminar();
});

document.querySelector("#empezar").addEventListener("click", empezar);
document.querySelector("#otra").addEventListener("click", empezar);

Retos para seguir

Has terminado el proyecto. Si quieres ir más allá, prueba a añadirle esto por tu cuenta:

  1. Da más puntos cuanto antes se responda.
  2. Guarda la mejor puntuación en localStorage y muéstrala al empezar.
  3. Carga las preguntas desde un archivo JSON con fetch.

Conceptos clave

Repasa la ruta de JavaScript o elige otro proyecto.