From c3223d211e79d959a06e39f5a462f321ad4c80b7 Mon Sep 17 00:00:00 2001 From: qjorge Date: Wed, 16 Jul 2025 17:57:35 +0200 Subject: [PATCH] first commit --- .dockerignore | 16 ++++ Dockerfile | 34 ++++++++ README.md | 3 + app.py | 149 +++++++++++++++++++++++++++++++++ requirements.txt | 3 + run.sh | 3 + static/css/styles.css | 10 +++ static/js/game.js | 186 +++++++++++++++++++++++++++++++++++++++++ templates/base.html | 52 ++++++++++++ templates/game.html | 60 +++++++++++++ templates/index.html | 16 ++++ templates/ranking.html | 38 +++++++++ 12 files changed, 570 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app.py create mode 100644 requirements.txt create mode 100755 run.sh create mode 100644 static/css/styles.css create mode 100644 static/js/game.js create mode 100644 templates/base.html create mode 100644 templates/game.html create mode 100644 templates/index.html create mode 100644 templates/ranking.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b78c85f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +run.sh + +venv/ +env/ +__pycache__/ +*.pyc + +.git +.gitignore + +ranking.db + +.vscode/ +.idea/ +*.swp + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c3930ba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 5000 + +ENV FLASK_APP=app.py +ENV FLASK_ENV=production +ENV FLASK_RUN_HOST=0.0.0.0 + +# --------------------------------------------- +# 9) Antes de arrancar, creamos la tabla si no existe +# (llamamos a la función crear_tabla() definida en app.py). +# Esto hará que, cuando se construya la imagen, se garantice +# que la BD ya tiene la estructura mínima. +# --------------------------------------------- +RUN python - << 'EOF' +from app import crear_tabla +crear_tabla() +EOF + +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] + diff --git a/README.md b/README.md new file mode 100644 index 0000000..452ca22 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# MULTI-GAME + +Tienes 1 minuto para resolver todas las multiplicaciones de 2 cifras que seas capaz! diff --git a/app.py b/app.py new file mode 100644 index 0000000..13a1937 --- /dev/null +++ b/app.py @@ -0,0 +1,149 @@ +import sqlite3 +from flask import Flask, render_template, request, jsonify, g + +DATABASE = "ranking.db" +app = Flask(__name__) + + +def get_db(): + """ + Abre una conexión a la base de datos SQLite y la asocia a 'g' + para reutilizarla en cada petición. + """ + db = getattr(g, "_database", None) + if db is None: + db = g._database = sqlite3.connect(DATABASE) + return db + + +@app.teardown_appcontext +def close_connection(exception): + """ + Al terminar el contexto de la petición, cierra la conexión si existe. + """ + db = getattr(g, "_database", None) + if db is not None: + db.close() + + +def crear_tabla(): + """ + Crea la tabla ranking si no existe. + """ + conn = sqlite3.connect(DATABASE) + c = conn.cursor() + c.execute(""" + CREATE TABLE IF NOT EXISTS ranking ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nombre TEXT NOT NULL, + puntuacion INTEGER NOT NULL + ) + """) + conn.commit() + conn.close() + + +def obtener_top10(): + """ + Devuelve una lista de tuplas [(nombre, puntuacion), ...] con el top 10 ordenado descendente. + """ + conn = get_db() + c = conn.cursor() + c.execute("SELECT nombre, puntuacion FROM ranking ORDER BY puntuacion DESC, id ASC LIMIT 10") + resultados = c.fetchall() + return resultados + + +def puntuacion_entra_en_top(puntos): + """ + Comprueba si 'puntos' supera la última posición del top 10. + Si hay menos de 10 filas, siempre devuelve True para guardar. + """ + top = obtener_top10() + if len(top) < 10: + return True + # La décima posición es top[-1][1] (puntuacion más baja en top10) + return puntos > top[-1][1] + + +def guardar_puntuacion(nombre, puntos): + """ + Inserta la nueva puntuación en la tabla ranking. + """ + conn = get_db() + c = conn.cursor() + c.execute("INSERT INTO ranking (nombre, puntuacion) VALUES (?, ?)", (nombre, puntos)) + conn.commit() + + +@app.route("/") +def index(): + """ + Página principal con instrucciones y botón para iniciar el juego. + """ + return render_template("index.html") + + +@app.route("/juego") +def juego(): + """ + Página donde se juega: se carga el HTML y el JS se encarga de todo lo demás. + """ + return render_template("game.html") + + +@app.route("/ranking") +def ranking(): + """ + Página que muestra el top 10. + """ + top10 = obtener_top10() + return render_template("ranking.html", top10=top10) + + +@app.route("/api/score", methods=["POST"]) +def api_score(): + """ + Recibe JSON: { "puntuacion": } + Comprueba si entra en top10 y devuelve JSON: { "entra_en_top": true/false, "top10": [...] } + """ + data = request.get_json() + if not data or "puntuacion" not in data: + return jsonify({"error": "No se recibió puntuación"}), 400 + + puntos = int(data["puntuacion"]) + entra = puntuacion_entra_en_top(puntos) + top10 = obtener_top10() + # Devolvemos el top10 actual (ANTES de guardar la nueva puntuación) + return jsonify({ + "entra_en_top": entra, + "top10": top10 + }) + + +@app.route("/api/score/save", methods=["POST"]) +def api_score_save(): + """ + Recibe JSON: { "nombre": , "puntuacion": } + Guarda en la BD y devuelve el ranking actualizado. + """ + data = request.get_json() + if not data or "nombre" not in data or "puntuacion" not in data: + return jsonify({"error": "Faltan parámetros"}), 400 + + nombre = data["nombre"].strip() + puntos = int(data["puntuacion"]) + # Sólo guardamos si efectivamente entra en top10 (por seguridad) + if puntuacion_entra_en_top(puntos): + guardar_puntuacion(nombre, puntos) + top10 = obtener_top10() + return jsonify({ + "guardado": True, + "top10": top10 + }) + + +if __name__ == "__main__": + crear_tabla() + app.run(debug=True) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..446e792 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.3.2 +gunicorn==20.1.0 + diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..dc0c626 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +docker rm -f multiplicat-contenedor +docker build -t multiplicat-app . +docker run -d --name multiplicat-contenedor -p 5000:5000 multiplicat-app diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..3a2e1f0 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,10 @@ +body { + background-color: #f8f9fa; /* gris claro */ +} +.card { + border-radius: 1rem; +} +#enunciado { + font-weight: 500; +} + diff --git a/static/js/game.js b/static/js/game.js new file mode 100644 index 0000000..d0b6fd7 --- /dev/null +++ b/static/js/game.js @@ -0,0 +1,186 @@ +// static/js/game.js + +document.addEventListener("DOMContentLoaded", () => { + const TIEMPO_TOTAL = 60; // segundos + let segundosRestantes = TIEMPO_TOTAL; + let puntuacion = 0; + + const barraTiempo = document.getElementById("barra-tiempo"); + const tiempoTexto = document.getElementById("tiempo-texto"); + const contadorAciertos = document.getElementById("contador-aciertos"); + const enunciado = document.getElementById("enunciado"); + const inputRespuesta = document.getElementById("input-respuesta"); + const feedback = document.getElementById("feedback"); + const botonFinal = document.getElementById("boton-final"); + const verRankingBtn = document.getElementById("ver-ranking"); + const jugarOtraBtn = document.getElementById("jugar-otra"); + + const formNombre = document.getElementById("form-nombre"); + const inputNombre = document.getElementById("input-nombre"); + const guardarNombreBtn = document.getElementById("guardar-nombre"); + const mensajeGuardar = document.getElementById("mensaje-guardar"); + + let numeroA = 0; + let numeroB = 0; + let temporizadorInterval = null; + + // Genera un par aleatorio (dos números entre 10 y 99) + function generarPregunta() { + numeroA = Math.floor(Math.random() * 90) + 10; // 10…99 + numeroB = Math.floor(Math.random() * 90) + 10; + enunciado.textContent = `¿Cuánto es ${numeroA} × ${numeroB}?`; + inputRespuesta.value = ""; + inputRespuesta.focus(); + } + + // Inicia el cronómetro y actualiza cada segundo + function iniciarTemporizador() { + barraTiempo.style.width = "100%"; + tiempoTexto.textContent = `${segundosRestantes} segundos restantes`; + temporizadorInterval = setInterval(() => { + segundosRestantes--; + if (segundosRestantes < 0) { + clearInterval(temporizadorInterval); + terminarJuego(); + return; + } + // Actualiza texto y barra de progreso + tiempoTexto.textContent = `${segundosRestantes} segundos restantes`; + const porcentaje = (segundosRestantes / TIEMPO_TOTAL) * 100; + barraTiempo.style.width = `${porcentaje}%`; + + // Cambiar color de barra si queda poco tiempo + if (segundosRestantes <= 10) { + barraTiempo.classList.remove("bg-success"); + barraTiempo.classList.add("bg-danger"); + } else if (segundosRestantes <= 30) { + barraTiempo.classList.remove("bg-success"); + barraTiempo.classList.add("bg-warning"); + } + }, 1000); + } + + // Función que maneja el envío de la respuesta al presionar Enter + inputRespuesta.addEventListener("keydown", (e) => { + if (e.key === "Enter" && segundosRestantes > 0) { + e.preventDefault(); + validarRespuesta(); + } + }); + + function validarRespuesta() { + const valor = parseInt(inputRespuesta.value); + const correcta = numeroA * numeroB; + if (!isNaN(valor)) { + if (valor === correcta) { + puntuacion++; + contadorAciertos.textContent = puntuacion; + feedback.innerHTML = `Correcto!`; + } else { + feedback.innerHTML = `Incorrecto. Era ${correcta}.`; + } + // Mostrar feedback por 800ms y luego limpiarlo + setTimeout(() => { + feedback.textContent = ""; + }, 800); + } else { + feedback.innerHTML = `Ingresa un número válido.`; + setTimeout(() => { + feedback.textContent = ""; + }, 800); + } + generarPregunta(); + } + + function terminarJuego() { + // Deshabilitar input + inputRespuesta.disabled = true; + feedback.innerHTML = ` +
+ ¡Tiempo terminado! Tu puntuación es: ${puntuacion} +
+ `; + + // Enviar puntuación al servidor + fetch('/api/score', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ puntuacion }) + }) + .then(res => res.json()) + .then(data => { + if (data.error) { + feedback.innerHTML += `
Error: ${data.error}
`; + botonFinal.classList.remove("d-none"); + return; + } + if (data.entra_en_top) { + // Muestra formulario para nombre + formNombre.classList.remove("d-none"); + } else { + // No entra en top10: mostrar botón para ver ranking + botonFinal.classList.remove("d-none"); + } + // Guarda el top10 actual en data.top10 para mostrar más tarde si hace falta + window.top10_actual = data.top10; + }) + .catch(err => { + feedback.innerHTML += `
Error conectando al servidor.
`; + botonFinal.classList.remove("d-none"); + }); + } + + // Botón “Ver Ranking” (si no entró en top 10) + verRankingBtn.addEventListener("click", () => { + // Redirigir a la página /ranking + window.location.href = "/ranking"; + }); + + // Botón “Jugar otra vez” + jugarOtraBtn.addEventListener("click", () => { + window.location.href = "/juego"; + }); + + // Al hacer clic en “Guardar” nombre (si entró en top 10) + guardarNombreBtn.addEventListener("click", () => { + const nombre = inputNombre.value.trim(); + if (nombre.length === 0) { + mensajeGuardar.innerHTML = `El nombre no puede estar vacío.`; + return; + } + // Enviar nombre + puntuación al servidor + fetch('/api/score/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nombre, puntuacion }) + }) + .then(res => res.json()) + .then(data => { + if (data.error) { + mensajeGuardar.innerHTML = `Error: ${data.error}`; + return; + } + mensajeGuardar.innerHTML = `
+ ¡Puntuación guardada! Puedes ver el ranking actualizado.
`; + // Opcional: mostrar tabla de top10 recibida en data.top10 + setTimeout(() => { + window.location.href = "/ranking"; + }, 1500); + }) + .catch(err => { + mensajeGuardar.innerHTML = `Error guardando el nombre.`; + }); + }); + + // Iniciar todo + function iniciarJuego() { + generarPregunta(); + iniciarTemporizador(); + inputRespuesta.disabled = false; + inputRespuesta.focus(); + } + + // Esperar 300ms para que el usuario vea la primera pregunta + setTimeout(iniciarJuego, 300); +}); + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..55276ad --- /dev/null +++ b/templates/base.html @@ -0,0 +1,52 @@ + + + + + + {% block head %} + {% block title %}Juego de Multiplicaciones{% endblock %} + + + + + {% endblock %} + + + + + +
+ {% block body %}{% endblock %} +
+ + + + {% block scripts %}{% endblock %} + + + diff --git a/templates/game.html b/templates/game.html new file mode 100644 index 0000000..665d35f --- /dev/null +++ b/templates/game.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} +{% block title %}Jugar - Multiplic-a-t{% endblock %} + +{% block body %} +
+
+
+
+

¡Pon a prueba tus multiplicaciones!

+ + +
+ 60 segundos restantes +
+
+
+
+
+ + +
+ Aciertos: 0 +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+

¡Felicidades! Entraste en el Top 10. Escribe tu nombre:

+
+ + +
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + + +{% endblock %} + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..e97e0ce --- /dev/null +++ b/templates/index.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}Inicio - Multiplic-a-t{% endblock %} + +{% block body %} +
+

Bienvenido a Multiplic-a-t

+

+ Tienes 1 minuto para resolver tantas multiplicaciones de dos cifras como puedas. + Escribe la respuesta en el campo de texto y presiona Enter. + Al terminar el minuto, podrás ver tu puntuación y, si entras en el top 10, registrar tu nombre. +

+ Comenzar a jugar +
+{% endblock %} + diff --git a/templates/ranking.html b/templates/ranking.html new file mode 100644 index 0000000..8272584 --- /dev/null +++ b/templates/ranking.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block title %}Top 10 - Multiplic-a-t{% endblock %} + +{% block body %} +
+

Top 10 de Puntuaciones

+ Jugar de nuevo +
+ +
+
+ {% if top10 %} + + + + + + + + + + {# Recorremos top10 sin enumerate; loop.index comienza en 1 #} + {% for par in top10 %} + + + {# par[0] = nombre #} + {# par[1] = puntuación #} + + {% endfor %} + +
#NombrePuntuación
{{ loop.index }}{{ par[0] }}{{ par[1] }}
+ {% else %} +

Aún no hay puntuaciones.

+ {% endif %} +
+
+{% endblock %} +