02 — Arquitectura¶
Nota de implementación. Tres elecciones de esta tabla cambiaron al implementar: frontend en JavaScript sin compilación (ADR-005),
sqlite3en lugar de SQLAlchemy (ADR-006) yscrypten lugar de argon2 (ADR-007). Ver docs/adr.
1. Vista general¶
La arquitectura es hexagonal: un núcleo puro (motor de Mus) sin dependencias, rodeado de adaptadores (bots, servidor web, persistencia, torneo). Las dependencias apuntan siempre hacia dentro: el motor no conoce bots, web ni base de datos.
┌─────────────────────────────────────────────────────────────────────────┐
│ FRONTEND (navegador) — TypeScript, solo recibe Observations │
│ Mesa · Tutorial · Replay · Estadísticas · Perfil · Bot Arena │
└───────────────▲─────────────────────────────────┬───────────────────────┘
│ Observation (JSON) │ ActionRequest (JSON)
│ WebSocket / HTTP ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ musst.server — FastAPI │
│ Auth · Validación Pydantic · GameController · SessionManager │
└───────────────▲─────────────────────────────────┬───────────────────────┘
│ │
┌───────────────┴─────────────────────────────────▼───────────────────────┐
│ musst.runtime — GameRunner (orquesta asientos humanos/bots, timeouts) │
│ │
│ ┌──────────────────────── musst.engine (NÚCLEO PURO) ───────────────┐ │
│ │ GameEngine ──► RulesEngine (legalidad) │ │
│ │ │ ──► Scoring (tanteo) │ │
│ │ │ ──► HandEvaluator (grande/chica/pares/juego/punto) │ │
│ │ ▼ │ │
│ │ GameState (secreto) ──► EventLog ──► ObservationBuilder │ │
│ └────────────────────────────────────────────────┬──────────────────┘ │
│ │ Observation │
│ ┌──────────────── musst.bots ────────────────────▼──────────────────┐ │
│ │ Bot API · SandboxRunner (subproceso) · Bots internos │ │
│ │ musst.agents: Orquestador → Agentes → Juez → Decision │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────────────────────┘
│ eventos + resúmenes
┌──────────────────────▼──────────────────────────────────────────────────┐
│ musst.persistence (SQLAlchemy: SQLite → PostgreSQL) │
│ musst.learning (PlayerModel, actualización post-partida) │
│ musst.analytics (estadísticas, momentos clave, simulación contrafactual)│
│ musst.arena (torneo, scoreboard) │
└─────────────────────────────────────────────────────────────────────────┘
Flujo de una decisión de bot (ninguna flecha salta el motor)¶
GameEngine ──state──► ObservationBuilder ──Observation(asiento)──► SandboxRunner
│
┌─────────────────────────────────┘
▼
BOT ORCHESTRATOR
┌──────┬──────┼────────┬─────────┐
▼ ▼ ▼ ▼ ▼
CARTAS PROB. COMPORT. MARCADOR PAREJA …
└──────┴──────┼────────┴─────────┘
▼
ESTRATEGIA (candidatas ⊆ legal_actions)
▼
JUEZ → Decision
│ Action
▼
RulesEngine.validate(state, seat, action)
├─ legal → GameEngine.apply → eventos
└─ ilegal → rechazo (en torneo: derrota)
2. Componentes¶
| Componente | Paquete | Responsabilidad | No hace |
|---|---|---|---|
| Cartas y baraja | engine.cards |
Card, Suit, Rank, Deck con RNG inyectado |
— |
| HandEvaluator | engine.evaluation |
Comparar manos en grande/chica; calcular pares, juego y punto | Decidir |
| RulesEngine | engine.rules |
legal_actions(state, seat), validate(...) |
Mutar estado |
| GameEngine | engine.game |
apply(state, seat, action) -> (state', events); transiciones de la FSM |
Elegir acciones |
| Scoring | engine.scoring |
Tanteo de cada lance y al final de la jugada; fin de juego | — |
| ObservationBuilder | engine.observation |
Proyecta el estado y los eventos a la vista de un asiento | — |
| EventLog | engine.events |
Eventos inmutables, públicos o privados | — |
| SignalSystem | engine.signals |
Señas FEM (desactivado en v1) | — |
| GameRunner | runtime |
Bucle de partida, asientos, timeouts, abandono, pausa | Reglas |
| Bot API | bots.api |
Contrato público Bot, Observation, Action |
— |
| SandboxRunner | bots.sandbox |
Ejecuta bots en subproceso con timeout | — |
| Agentes | agents.* |
IA interna (doc 06) | Validar ni ejecutar |
| PlayerModel | learning |
Modelo estadístico de los jugadores | — |
| Arena | arena |
Torneo, clasificación, exportación del scoreboard | — |
| Servidor | server |
HTTP/WS, auth, sesiones, permisos | Lógica de juego |
| Persistencia | persistence |
Repositorios, migraciones | — |
| Frontend | web/ |
Renderizar observaciones y enviar intenciones | Validar reglas (solo oculta botones) |
3. Decisiones tecnológicas¶
Cada decisión se documenta como un ADR breve en docs/adr/. Resumen:
| Decisión | Elección | Alternativas | Por qué | Limitaciones |
|---|---|---|---|---|
| Lenguaje del núcleo | Python 3.12, solo stdlib | — | Lo exige la práctica; sin dependencias el motor es instalable en cualquier sitio | Rendimiento de Monte Carlo (mitigable) |
| Modelado de estado | dataclass(frozen=True) + transiciones puras |
Objetos mutables | Replay, tests y simulación triviales; ningún agente puede mutar el estado | Copias (aceptable: estado pequeño) |
| Servidor | FastAPI + Uvicorn | Streamlit, Flask, Django | WebSockets, validación Pydantic, OpenAPI automático; el servidor guarda el secreto | Requiere hosting con proceso (no Pages) |
| Frontend | TypeScript + Vite, sin framework pesado (Preact si crece) | Streamlit, React, Svelte | Control total de la mesa y las animaciones; estático, se sirve en Pages | Más trabajo que Streamlit |
| BD | SQLAlchemy 2 + Alembic, SQLite → PostgreSQL | ORM propio, SQL crudo | Migración sin reescribir | — |
| Hash de contraseñas | argon2id (argon2-cffi) |
bcrypt | Recomendado por OWASP | — |
| Configuración | TOML (tomllib) + dataclasses validadas |
YAML, env | stdlib, tipado | — |
| Docs | MkDocs Material + mkdocstrings → ReadTheDocs | Sphinx | Lo recomienda la práctica; referencia del API desde docstrings | — |
| Calidad | pytest, hypothesis, ruff, mypy | — | Estándar | — |
Por qué NO hacer todo en el navegador (Pyodide / GitHub Pages solo)¶
Una web estática que ejecuta el motor en el navegador tendría las cartas de los bots en la memoria del cliente: cualquiera las vería con las DevTools. Eso incumple RNF-02 y el apartado 5.1.1 de la práctica. Por eso el estado secreto vive en el servidor y el navegador solo recibe observaciones.
4. Estructura de carpetas¶
mus-sara/
├── README.md LICENSE .gitignore pyproject.toml CONTRIBUTING.md
├── config/
│ ├── rules.default.toml # objetivo de tantos, convenciones de tanteo
│ ├── agents.toml # pesos y umbrales de agentes por nivel
│ └── ui.toml
├── src/musst/
│ ├── __init__.py # API de alto nivel: play_game(...)
│ ├── engine/
│ │ ├── cards.py deck.py evaluation.py
│ │ ├── actions.py # acciones tipadas
│ │ ├── state.py # GameState, RoundState, LanceState, BetState
│ │ ├── fsm.py # fases y transiciones permitidas
│ │ ├── rules.py # RulesEngine
│ │ ├── game.py # GameEngine.apply
│ │ ├── scoring.py
│ │ ├── events.py
│ │ ├── observation.py # ObservationBuilder
│ │ ├── signals.py # SignalSystem (desactivado)
│ │ └── config.py
│ ├── runtime/ runner.py seats.py timeouts.py
│ ├── bots/
│ │ ├── api.py # ← CONTRATO PÚBLICO
│ │ ├── sandbox.py registry.py
│ │ └── builtin/ random_bot.py simple.py medium.py pro.py
│ ├── agents/
│ │ ├── base.py orchestrator.py judge.py explanation.py
│ │ ├── cards.py probability.py behaviour.py bluff.py
│ │ ├── score.py history.py partner.py strategy.py rules_agent.py
│ ├── learning/ player_model.py memory.py updater.py drift.py
│ ├── analytics/ stats.py key_moments.py counterfactual.py calibration.py
│ ├── arena/ tournament.py scoreboard.py cli.py
│ ├── persistence/ models.py repositories.py migrations/
│ └── server/ app.py auth.py routes/ ws.py schemas.py dev_mode.py
├── bots/ # bots externos (uno por carpeta, vía PR)
│ └── example_bot/ bot.py README.md
├── web/ # frontend (TypeScript + Vite)
├── experiments/ # experimentos reproducibles (notebooks + scripts)
├── tests/
│ ├── unit/ integration/ privacy/ property/ bot_conformance/ scenarios/
├── docs/ # MkDocs
│ ├── index.md architecture.md game-rules.md bot-api.md agents.md
│ ├── player-model.md testing.md deployment.md adr/ design/
└── .github/
├── workflows/ ci.yml bot-pr.yml tournament.yml docs.yml deploy.yml
└── PULL_REQUEST_TEMPLATE.md
5. Flujo Git¶
main: protegida; solo recibe merges desdedevelopen hitos (punto de control, entrega).develop: integración; protegida, PR con CI verde y 1 revisión.feature/fase-NN-descripcion: una por fase o subtarea.- Bots externos: PR a
developdesde un fork, solo con cambios enbots/<nombre>/.
6. Despliegue (gratuito)¶
| Pieza | Plataforma | Notas |
|---|---|---|
| Frontend estático | GitHub Pages | Recomendado por la práctica |
| API + estado secreto | Render (free web service) o Hugging Face Spaces (Docker) | Arranque en frío ~30-60 s en Render; aceptable para la práctica |
| BD | SQLite en disco del servicio → Neon/Supabase PostgreSQL gratuito cuando haga falta persistencia real | En planes gratuitos el disco de Render es efímero |
| Documentación | ReadTheDocs | Req 2.3 |
| Scoreboard | GitHub Pages (JSON + HTML generados por Actions) | Req 4.2 |
Plan B para Req 1.3 si el backend se retrasa: una app Streamlit (Streamlit Cloud, también recomendado por la práctica). Mantiene el estado en el servidor y no rompe la privacidad, con menos control visual. Al ser solo un adaptador, el motor no cambia.