From 449cb1d9763eae2dcc6583b1efb76bca40f241e1 Mon Sep 17 00:00:00 2001 From: alexis Date: Tue, 29 Sep 2026 10:50:58 +0200 Subject: [PATCH] Add funcionamiento del juego --- clases/ClaseJuego.php | 348 +++++++++++++++++++++++++++++ controladores/ControladorJuego.php | 96 ++++++++ css/juego.css | 64 +++++- js/juego.js | 219 ++++++++++++++++++ juego.php | 63 +++--- 5 files changed, 760 insertions(+), 30 deletions(-) create mode 100644 clases/ClaseJuego.php create mode 100644 controladores/ControladorJuego.php create mode 100644 js/juego.js diff --git a/clases/ClaseJuego.php b/clases/ClaseJuego.php new file mode 100644 index 0000000..2a3834f --- /dev/null +++ b/clases/ClaseJuego.php @@ -0,0 +1,348 @@ +prepare(" + SELECT COUNT(*) AS total + FROM equipoxpartida exp + INNER JOIN equipoxusuario exu + ON exu.idEquipo = exp.idEquipo + WHERE exp.idPartida = :idPartida + AND exu.idUsuario = :idUsuario + "); + $consulta->execute([ + ':idPartida' => $idPartida, + ':idUsuario' => $idUsuario + ]); + return (int)$consulta->fetchColumn() > 0; + } catch (PDOException $error) { + return [ + 'error' => 'Ocurrió un error al comprobar si perteneces a la partida.', + 'error_tecnico' => $error->getMessage() + ]; + } + } + + /** + * Si la partida todavía no tiene preguntas, le asigna 10 al azar. + * Se hace dentro de una transacción con la fila de la partida bloqueada + * para que, si varios jugadores entran a la vez, las preguntas se + * asignen una sola vez. + * Devuelve null si todo va bien, o un array de error. + */ + private static function asegurarPreguntas($conexion, $idPartida) + { + try { + $conexion->beginTransaction(); + + $bloqueo = $conexion->prepare(" + SELECT idPartida FROM partida + WHERE idPartida = :idPartida + FOR UPDATE + "); + $bloqueo->execute([':idPartida' => $idPartida]); + + if (!$bloqueo->fetch()) { + $conexion->rollBack(); + return ['error' => 'La partida no existe.']; + } + + $cuenta = $conexion->prepare(" + SELECT COUNT(*) FROM partidaxpregunta + WHERE idPartida = :idPartida + "); + $cuenta->execute([':idPartida' => $idPartida]); + + if ((int)$cuenta->fetchColumn() === 0) { + + // Solo preguntas que tengan alguna respuesta marcada como correcta + $elegidas = $conexion->query(" + SELECT p.idPregunta + FROM pregunta p + WHERE EXISTS ( + SELECT 1 FROM respuesta r + WHERE r.idPregunta = p.idPregunta + AND r.correcta = 1 + ) + ORDER BY RAND() + LIMIT " . self::NUM_PREGUNTAS + )->fetchAll(PDO::FETCH_COLUMN); + + $insertar = $conexion->prepare(" + INSERT INTO partidaxpregunta (idPartida, idPregunta) + VALUES (:idPartida, :idPregunta) + "); + foreach ($elegidas as $idPregunta) { + $insertar->execute([ + ':idPartida' => $idPartida, + ':idPregunta' => $idPregunta + ]); + } + } + + $conexion->commit(); + return null; + } catch (PDOException $error) { + if ($conexion->inTransaction()) { + $conexion->rollBack(); + } + return [ + 'error' => 'Ocurrió un error al preparar las preguntas de la partida.', + 'error_tecnico' => $error->getMessage() + ]; + } + } + + /** + * Devuelve las preguntas de la partida (máximo 10) con sus respuestas + * mezcladas. NO incluye el campo 'correcta' para que no se pueda hacer + * trampa mirando la respuesta del servidor. + * Cada pregunta indica si este usuario ya la ha respondido, para poder + * retomar la partida si recarga la página. + */ + public static function cargarPreguntas($idPartida, $idUsuario) + { + $conexion = conexionBD(); + if (is_array($conexion)) { + return $conexion; + } + + $error = self::asegurarPreguntas($conexion, $idPartida); + if ($error !== null) { + return $error; + } + + try { + // 1. Las preguntas, siempre en el mismo orden para todos los jugadores + $consulta = $conexion->prepare(" + SELECT + pxp.idPartidaxPregunta, + p.idPregunta, + p.nombrePregunta + FROM partidaxpregunta pxp + INNER JOIN pregunta p + ON p.idPregunta = pxp.idPregunta + WHERE pxp.idPartida = :idPartida + ORDER BY pxp.idPartidaxPregunta + LIMIT " . self::NUM_PREGUNTAS + ); + $consulta->execute([':idPartida' => $idPartida]); + $preguntas = $consulta->fetchAll(PDO::FETCH_ASSOC); + + // 2. Las respuestas de todas esas preguntas, agrupadas por pregunta + $consulta = $conexion->prepare(" + SELECT DISTINCT + r.idRespuesta, + r.idPregunta, + r.nombreRespuesta + FROM respuesta r + INNER JOIN partidaxpregunta pxp + ON pxp.idPregunta = r.idPregunta + WHERE pxp.idPartida = :idPartida + "); + $consulta->execute([':idPartida' => $idPartida]); + + $respuestasPorPregunta = []; + foreach ($consulta->fetchAll(PDO::FETCH_ASSOC) as $fila) { + $respuestasPorPregunta[(int)$fila['idPregunta']][] = [ + 'idRespuesta' => (int)$fila['idRespuesta'], + 'nombreRespuesta' => $fila['nombreRespuesta'] + ]; + } + + // 3. Qué preguntas ha respondido ya este usuario + $consulta = $conexion->prepare(" + SELECT ra.idPartidaxPregunta + FROM respuestaalmacenada ra + INNER JOIN partidaxpregunta pxp + ON pxp.idPartidaxPregunta = ra.idPartidaxPregunta + WHERE pxp.idPartida = :idPartida + AND ra.idUsuario = :idUsuario + "); + $consulta->execute([ + ':idPartida' => $idPartida, + ':idUsuario' => $idUsuario + ]); + $respondidas = array_flip($consulta->fetchAll(PDO::FETCH_COLUMN)); + + // 4. Montamos la respuesta final + $resultado = []; + foreach ($preguntas as $pregunta) { + $idPregunta = (int)$pregunta['idPregunta']; + $idPxp = (int)$pregunta['idPartidaxPregunta']; + + $respuestas = $respuestasPorPregunta[$idPregunta] ?? []; + shuffle($respuestas); + + $resultado[] = [ + 'idPartidaxPregunta' => $idPxp, + 'idPregunta' => $idPregunta, + 'nombrePregunta' => $pregunta['nombrePregunta'], + 'respondida' => isset($respondidas[$idPxp]), + 'respuestas' => $respuestas + ]; + } + + return [ + 'total' => count($resultado), + 'respondidas' => count($respondidas), + 'preguntas' => $resultado + ]; + } catch (PDOException $error) { + return [ + 'error' => 'Ocurrió un error al cargar las preguntas.', + 'error_tecnico' => $error->getMessage() + ]; + } + } + + /** + * Guarda la respuesta que ha elegido el usuario en una pregunta y + * devuelve si ha acertado y cuál era la correcta (para el feedback). + * Comprueba en el servidor que: + * - la pregunta pertenece a la partida + * - la respuesta pertenece a esa pregunta + * - el usuario no ha respondido ya esa pregunta + */ + public static function registrarRespuesta($idPartida, $idUsuario, $idPartidaxPregunta, $idRespuesta) + { + $conexion = conexionBD(); + if (is_array($conexion)) { + return $conexion; + } + + try { + // 1. La pregunta tiene que ser de esta partida + $consulta = $conexion->prepare(" + SELECT idPregunta + FROM partidaxpregunta + WHERE idPartidaxPregunta = :idPxp + AND idPartida = :idPartida + "); + $consulta->execute([ + ':idPxp' => $idPartidaxPregunta, + ':idPartida' => $idPartida + ]); + $idPregunta = $consulta->fetchColumn(); + + if ($idPregunta === false) { + return ['error' => 'Esa pregunta no pertenece a esta partida.']; + } + + // 2. La respuesta tiene que ser una de las de esa pregunta + $consulta = $conexion->prepare(" + SELECT idRespuesta, correcta + FROM respuesta + WHERE idPregunta = :idPregunta + "); + $consulta->execute([':idPregunta' => $idPregunta]); + + $elegida = null; + $primeraCorrecta = null; + foreach ($consulta->fetchAll(PDO::FETCH_ASSOC) as $fila) { + if ((int)$fila['correcta'] === 1 && $primeraCorrecta === null) { + $primeraCorrecta = (int)$fila['idRespuesta']; + } + if ((int)$fila['idRespuesta'] === $idRespuesta) { + $elegida = $fila; + } + } + + if ($elegida === null) { + return ['error' => 'Esa respuesta no pertenece a la pregunta.']; + } + + // 3. Solo se puede responder una vez a cada pregunta + $consulta = $conexion->prepare(" + SELECT COUNT(*) + FROM respuestaalmacenada + WHERE idPartidaxPregunta = :idPxp + AND idUsuario = :idUsuario + "); + $consulta->execute([ + ':idPxp' => $idPartidaxPregunta, + ':idUsuario' => $idUsuario + ]); + + if ((int)$consulta->fetchColumn() > 0) { + return ['error' => 'Ya has respondido a esta pregunta.']; + } + + // 4. Guardamos la respuesta con la clase que ya existía + $almacenada = new RespuestaAlmacenada( + null, + $idPartidaxPregunta, + $idUsuario, + $idRespuesta + ); + $guardado = $almacenada->guardarRespuesta(); + + if (isset($guardado['error'])) { + return $guardado; + } + + // 5. Progreso del usuario en la partida + $consulta = $conexion->prepare(" + SELECT COUNT(*) + FROM respuestaalmacenada ra + INNER JOIN partidaxpregunta pxp + ON pxp.idPartidaxPregunta = ra.idPartidaxPregunta + WHERE pxp.idPartida = :idPartida + AND ra.idUsuario = :idUsuario + "); + $consulta->execute([ + ':idPartida' => $idPartida, + ':idUsuario' => $idUsuario + ]); + $respondidas = (int)$consulta->fetchColumn(); + + $consulta = $conexion->prepare(" + SELECT COUNT(*) FROM partidaxpregunta + WHERE idPartida = :idPartida + "); + $consulta->execute([':idPartida' => $idPartida]); + $total = min((int)$consulta->fetchColumn(), self::NUM_PREGUNTAS); + + $acierto = (int)$elegida['correcta'] === 1; + + return [ + 'exito' => 'Respuesta guardada.', + 'acierto' => $acierto, + 'idRespuestaCorrecta' => $acierto ? $idRespuesta : $primeraCorrecta, + 'respondidas' => $respondidas, + 'total' => $total, + 'terminada' => $respondidas >= $total + ]; + } catch (PDOException $error) { + return [ + 'error' => 'Ocurrió un error al guardar la respuesta.', + 'error_tecnico' => $error->getMessage() + ]; + } + } +} diff --git a/controladores/ControladorJuego.php b/controladores/ControladorJuego.php new file mode 100644 index 0000000..5b4b41c --- /dev/null +++ b/controladores/ControladorJuego.php @@ -0,0 +1,96 @@ + 'Debes iniciar sesión para jugar.']); + exit; +} + +$idUsuario = (int)$_SESSION['idUsuario']; +$idPartida = intval($_POST['idPartida'] ?? 0); + +if ($idPartida <= 0) { + echo json_encode(['error' => 'No se ha indicado la partida.']); + exit; +} + +// Solo pueden jugar los usuarios que están en uno de los equipos de la partida +$perteneceAPartida = Juego::usuarioEnPartida($idPartida, $idUsuario); + +if (is_array($perteneceAPartida)) { + echo json_encode($perteneceAPartida); + exit; +} + +if (!$perteneceAPartida) { + echo json_encode(['error' => 'No formas parte de esta partida.']); + exit; +} + + +switch ($_POST['orden'] ?? '') { + + // Devuelve las 10 preguntas con sus respuestas (sin marcar la correcta) + case 'cargarPreguntas': + + echo json_encode(Juego::cargarPreguntas($idPartida, $idUsuario)); + + break; + + + // Guarda la respuesta del usuario y dice si ha acertado + case 'responder': + + $idPartidaxPregunta = intval($_POST['idPartidaxPregunta'] ?? 0); + $idRespuesta = intval($_POST['idRespuesta'] ?? 0); + + if ($idPartidaxPregunta <= 0 || $idRespuesta <= 0) { + echo json_encode(['error' => 'Faltan datos para registrar la respuesta.']); + break; + } + + echo json_encode( + Juego::registrarRespuesta( + $idPartida, + $idUsuario, + $idPartidaxPregunta, + $idRespuesta + ) + ); + + break; + + + // Recibe los aciertos del jugador al terminar sus 10 preguntas, + // para que resultados.php los pueda usar al montar la hoja de resultados + case 'finalizarPartida': + + $aciertos = intval($_POST['aciertos'] ?? 0); + + if (!isset($_SESSION['aciertosPorPartida'])) { + $_SESSION['aciertosPorPartida'] = []; + } + + $_SESSION['aciertosPorPartida'][$idPartida] = $aciertos; + + echo json_encode([ + 'exito' => 'Aciertos registrados correctamente.', + 'idPartida' => $idPartida, + 'aciertos' => $aciertos + ]); + + break; + + + default: + + echo json_encode(['error' => 'La orden indicada no existe.']); + + break; +} \ No newline at end of file diff --git a/css/juego.css b/css/juego.css index 252da22..c56c443 100644 --- a/css/juego.css +++ b/css/juego.css @@ -116,4 +116,66 @@ .btn-answer { font-size: 18px; } - } \ No newline at end of file + } + +/* ===== Lógica de juego (js/juego.js) ===== */ + +.game-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.progress-track { + height: 8px; + background-color: #ddd; + border-radius: 4px; + overflow: hidden; + margin-bottom: 16px; +} + +.progress-fill { + height: 100%; + width: 0; + background-color: #1368ce; + transition: width 0.3s ease; +} + +.feedback { + min-height: 1.5em; + margin-top: 12px; + font-size: 18px; + font-weight: 600; +} + +.feedback.ok { color: #26890c; } +.feedback.ko { color: #e21b3c; } + +/* Mientras se envía la respuesta o se ve el feedback no se puede volver a pulsar */ +.answers-grid.bloqueado .btn-answer { + pointer-events: none; +} + +.btn-answer.correcta { + box-shadow: 0 0 0 6px #fff, 0 0 0 10px #26890c; +} + +.btn-answer.incorrecta, +.btn-answer.atenuada { + opacity: 0.45; +} + +.btn-answer:focus-visible { + outline: 4px solid #333; + outline-offset: 3px; +} + +@media (prefers-reduced-motion: reduce) { + .progress-fill, + .btn-answer { + transition: none; + } +} diff --git a/js/juego.js b/js/juego.js new file mode 100644 index 0000000..8b6df8d --- /dev/null +++ b/js/juego.js @@ -0,0 +1,219 @@ +// Lógica de juego.php +// Carga las preguntas de la partida, las muestra de una en una, +// guarda cada respuesta en el servidor y al terminar lleva a resultados.php + +const URL_CONTROLADOR = "controladores/ControladorJuego.php"; +const PAUSA_FEEDBACK = 1800; // ms que se ve si has acertado o fallado + +// Colores y formas de los botones (estilo Kahoot, definidos en juego.css) +const ESTILOS_BOTON = [ + { color: "red", forma: "triangle" }, + { color: "blue", forma: "diamond" }, + { color: "yellow", forma: "circle" }, + { color: "green", forma: "square" }, +]; + +let idPartida = 0; +let preguntas = []; +let indice = 0; // pregunta que se está mostrando +let bloqueado = false; // true mientras se envía una respuesta o se ve el feedback +let numeroAciertos = 0; // número de aciertos del jugador + +$(document).ready(function () { + idPartida = parseInt($("body").data("id-partida"), 10); + + // Un solo listener para todos los botones de respuesta + $("#respuestas").on("click", ".btn-answer", function () { + responder($(this)); + }); + + cargarPreguntas(); +}); + +// 1. Pedir las preguntas al servidor +function cargarPreguntas() { + $.ajax({ + url: URL_CONTROLADOR, + type: "POST", + dataType: "json", + data: { orden: "cargarPreguntas", idPartida: idPartida }, + success: function (datos) { + if (datos.error) { + mostrarError(datos.error); + return; + } + + preguntas = datos.preguntas; + + if (preguntas.length === 0) { + mostrarError("Esta partida no tiene preguntas."); + return; + } + + // Retomamos por la primera pregunta sin responder (por si recarga la página) + indice = preguntas.findIndex(function (p) { + return !p.respondida; + }); + + if (indice === -1) { + irAResultados(); + return; + } + + mostrarPregunta(); + }, + error: function () { + mostrarError("No se pudo conectar con el servidor."); + }, + }); +} + +// 2. Pintar la pregunta actual y sus respuestas +function mostrarPregunta() { + const pregunta = preguntas[indice]; + + bloqueado = false; + + $("#textoPregunta").text(pregunta.nombrePregunta); + $("#feedback").text("").removeClass("ok ko"); + $("#contadorPregunta").text( + "Pregunta " + (indice + 1) + " de " + preguntas.length, + ); + $("#barraProgreso").css("width", (indice / preguntas.length) * 100 + "%"); + + const $respuestas = $("#respuestas").empty(); + + pregunta.respuestas.forEach(function (respuesta, i) { + const estilo = ESTILOS_BOTON[i % ESTILOS_BOTON.length]; + + // .text() y createTextNode evitan que el texto de la BD se interprete como HTML + const $boton = $(" - - - - - - - + +
- + + - \ No newline at end of file +