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() ]; } } }