Gestor profesional de torneos y ligas de tenis — del sorteo al saque final.
Cambios recientes — detalle completo en CHANGELOG.md.
- 🎾 Partido Amistoso: marcador rápido sin liga ni torneo, accesible desde el Home. La configuración viaja en la URL para sobrevivir refrescos.
- 🐳 Docker listo para usar:
Dockerfile+docker-compose.ymlcon hot-reload para arrancar el stack en un comando. - ⚙️ CI en GitHub Actions: unit + integration en cada push/PR y un workflow E2E nightly con Playwright.
CourtManager es una aplicación web full-stack para organizar competiciones de tenis: desde una liga vecinal hasta un torneo eliminatorio con sembrado y BYEs. Diseñada con un único objetivo: que arbitrar un partido sea tan rápido como pulsar dos botones.
Pensada para clubs, organizadores aficionados y profesores que necesitan una herramienta sencilla pero rigurosa, sin hojas de cálculo, sin papel y sin errores de cuenta.
| Módulo | Qué hace |
|---|---|
| 🏆 Ligas (Round Robin) | Genera el calendario completo ida y vuelta con el algoritmo del círculo. Tabla de clasificación con desempates por sets y orden alfabético. |
| 🎯 Torneos eliminatorios | Cuadros con sembrado aleatorio, distribución de BYEs (nunca se enfrentan entre sí), propagación automática del ganador a la siguiente ronda. |
| 🎾 Partido Amistoso | Marcador rápido sin crear competición: dos nombres, formato (sets / juegos) y a jugar. La configuración viaja en la URL para sobrevivir refrescos. |
| ⏱️ Marcador en vivo | Máquina de estados completa: 0 → 15 → 30 → 40, deuce, ventaja, tie-break (configurable), partidos al mejor de N sets / M juegos. |
| 💾 Persistencia robusta | Postgres (Supabase) en producción, SQLite en local. Migraciones gestionadas con Alembic. |
| 🌗 Modo administrador | Acciones destructivas (borrado de competiciones) protegidas por clave; sesión persistida vía LocalStorage. |
Dashboard de liga (Round Robin) y cuadro eliminatorio en acción.
Partido Amistoso: del formulario al marcador en dos clics.
- Frontend + Backend: Reflex 0.9 (Python end-to-end, sin tocar JS)
- ORM y BD: SQLModel + SQLAlchemy 2.0 + PostgreSQL (Supabase) / SQLite
- Migraciones: Alembic
- Estilos: Tailwind CSS v4 con sistema de diseño "Advantage" (Material 3 + tipografía Inter)
- Testing: Pytest + pytest-cov + Playwright + pytest-playwright
- CI: GitHub Actions (unit + integration en cada push y PR sobre
master) - Deployment: Reflex Hosting / Docker (Dockerfile + docker-compose para desarrollo local)
CourtManager se construye sobre una suite de tests con 3 niveles de cobertura que protegen el dominio del juego, la persistencia y el flujo de usuario.
╱╲
╱E2E╲ 5 tests Playwright (flujos UI completos)
╱──────╲
╱ Integ ╲ 53 tests con BD SQLite en memoria + states de Reflex
╱──────────╲
╱ Unit ╲ 107 tests puros (sin DB, sin red, sin UI)
╱──────────────╲
100 % de cobertura sobre TennisTournament/logic/ y el motor de puntuación. Tests deterministas que corren en milisegundos:
- Motor de tenis (
test_match_logic.py): puntuación 0/15/30/40, deuce/advantage, tie-break a 7 con diferencia de 2, validación de resultados imposibles (no se llega a 7-6 sin pasar por 5-5),config_gamesconfigurable. - Cuadros eliminatorios (
test_tournament_engine.py): cálculo de bracket size, distribución de BYEs sin BYE-vs-BYE, planificación completa con propagación de ganadores e índices locales. - Clasificación de liga (
test_standings.py): puntos por victoria, desempates encadenados (puntos → diferencia de sets → orden alfabético). - Fixtures (
test_fixtures.py): emparejamiento ida/vuelta por índice, flags de ganador, asimetría defensiva.
Cada test arranca con una BD SQLite en memoria limpia (fixture test_db_engine con scope="function") y mockea rx.session() para garantizar que ningún test toca Supabase.
- Ligas (
test_league_integration.py): creación deLeague, generación del calendario Round Robin, persistencia deconfig_gamesen cadaLeagueMatch, lectura de standings desde DB →MatchView→compute_standings. - Torneos (
test_tournament_integration.py): bracket completo en BD, cableado denext_match_id, finalización automática de BYEs, propagación E2E del ganador: réplica 1:1 del métodoLeagueState.record_resultque verifica que el winner aparece en el slot correcto del partido siguiente tras cerrar el partido en BD. - Partido Amistoso (
test_casual_integration.py): instancia elCasualMatchStatey valida steppers (sets impares 1–9, juegos 1–12),start_matchemitiendo elrx.redirectcon la URL/scoreboard?casual=1&p1=…&p2=…&sets=…&games=…, URL-encoding de espacios/acentos y rechazo (rx.toast.error) ante nombres vacíos o duplicados case-insensitive.
Cinco flujos críticos validados sobre la aplicación real corriendo en localhost:3000. Patrón Page Object Model estricto, locators 100 % user-centric (get_by_role, get_by_text, get_by_placeholder):
- Flow A — Liga: crear liga con 3 jugadores y 4 juegos por set, verificar que aparecen todos en standings.
- Flow B — Torneo + avance del ganador: crear cuadro de 4, ganar el primer partido, verificar visualmente que el ganador avanza a la final.
- Flow C — Persistencia de Estado: Validación de que las competiciones creadas aparecen y persisten correctamente en el Dashboard de "Competiciones Recientes" tras la navegación.
- Flow D — Partido Amistoso (
test_casual_match_spec.py): entrar al bento card, rellenar nombres y formato, comenzar partido y verificar que la URL contiene los query params (casual=1&sets=…&games=…) y que el marcador renderiza ambos jugadores; un segundo test valida que sumar puntos no rompe el contexto casual.
Cada test E2E captura screenshot automático en caso de fallo (hook pytest_runtest_makereport).
- Python 3.12+
- (Opcional) PostgreSQL 14+ o cuenta de Supabase para producción
# 1. Clonar el repo
git clone https://github.com/davidsored/TennisTournament.git
cd TennisTournament
# 2. Crear y activar el venv
python -m venv .venv
source .venv/bin/activate # Linux / macOS
# .venv\Scripts\activate # Windows
# 3. Instalar dependencias
pip install -r requirements.txt
# 4. Variables de entorno (.env en la raíz)
cat > .env <<'EOF'
DATABASE_URL=sqlite:///reflex.db
ADMIN_KEY=cambia-esta-clave
EOF
# 5. Aplicar migraciones
reflex db migrate
# 6. Levantar la app
reflex runLa app queda disponible en http://localhost:3000 (frontend) y http://localhost:8000 (backend WS).
Si prefieres no instalar Python ni Node localmente, hay un Dockerfile y un docker-compose.yml listos para desarrollo con hot-reload:
# Construye la imagen (Python 3.12.3-slim + Node.js 20 + dependencias) y arranca la app
docker compose up --build
# Arranques posteriores (sin reconstruir)
docker compose up
# Detener
docker compose downDetalles:
- Puertos
3000(frontend Next.js) y8000(backend WS) mapeados al host. - Hot-reload activo: el código del repo se monta como volumen, así los cambios locales se reflejan al instante dentro del contenedor (se usa
WATCHFILES_FORCE_POLLINGpara que funcione bajo Docker Desktop en Windows/macOS). DATABASE_URLpor defecto apunta a SQLite dentro del contenedor (sqlite:////app/reflex.db) para no interferir con Supabase. Puedes sobreescribirla con un.enven la raíz o exportándola antes dedocker compose up.- Volumen nombrado
reflex_webpreserva el build de Next entre reinicios y evita recompilar el frontend cada vez.
# Suite completa (unit + integration; ~5 segundos)
pytest tests/unit tests/integration -v
# Solo unitarios (super rápido)
pytest tests/unit -v
# Con cobertura
pytest tests/unit --cov=TennisTournament/logic --cov-report=term-missing
# Tests E2E (requiere reflex run en otra terminal + playwright install)
playwright install chromium
pytest tests/e2e -v --headed # con navegador visible
pytest tests/e2e -v # headless (CI)📖 Más detalles E2E en
README_E2E.mdy testing general enTESTING.md.
Cada push y cada pull request contra master dispara el workflow .github/workflows/ci.yml, que sobre Ubuntu y Python 3.12.3 instala las dependencias y ejecuta la suite pytest tests/unit tests/integration. El badge CI en la cabecera refleja el estado del último build.
TennisTournament/
├── logic/ ← Lógica de negocio pura (testeable sin Reflex)
│ ├── tournament_engine.py
│ ├── standings.py
│ └── fixtures.py
├── models/ ← rx.Model + SQLModel (League, Tournament, Match…)
├── states/ ← rx.State (LeagueState, ScoreboardState, CasualMatchState…)
├── pages/ ← Páginas Reflex (home, scoreboard, casual_match, dashboards…)
└── components/ ← UI reutilizable (top bar, bento cards, brackets…)
tests/
├── unit/ ← 107 tests sin dependencias externas
├── integration/ ← 53 tests con SQLite en memoria + states de Reflex
│ ├── test_league_integration.py
│ ├── test_tournament_integration.py
│ └── test_casual_integration.py
└── e2e/
├── pages/ ← Page Objects (POM), incl. CasualPage
└── specs/ ← Tests Playwright (incl. test_casual_match_spec.py)
.github/workflows/ ← CI (unit+integration) y E2E nightly con Playwright
Dockerfile ← Imagen Python 3.12.3-slim + Node 20 + Reflex
docker-compose.yml ← Stack de desarrollo con hot-reload y SQLite
CHANGELOG.md ← Historial de cambios (Keep a Changelog + SemVer)
¿Bug, idea o mejora? Abre un issue o un pull request.
Antes de mandar un PR:
- Asegura que
pytest tests/unit tests/integrationpasa al 100 %. - Si tocas la UI, ejecuta los E2E (
pytest tests/e2e --headed) y adjunta capturas si añades flujos. - Sigue el sistema de diseño documentado en
DESIGN.md.
MIT — ver LICENSE.
Hecho con 🎾 y mucho café.




