05 — API de bots, sandbox, torneo y scoreboard¶
1. Contrato público (Req 2.1)¶
# musst/bots/api.py — la ÚNICA superficie que un bot externo necesita importar
from abc import ABC, abstractmethod
class Bot(ABC):
"""Jugador autónomo de Mus. Controla UN asiento."""
name: str = "unnamed"
author: str = ""
version: str = "0.1"
def on_game_start(self, info: GameStartInfo) -> None:
"""Opcional. Asiento, pareja, configuración pública."""
@abstractmethod
def decide(self, observation: Observation) -> Action:
"""Devuelve una acción de observation.legal_actions. Obligatorio."""
def on_event(self, event: PublicEventView) -> None:
"""Opcional. Recibe cada evento público (y los privados dirigidos a este asiento)."""
def on_game_end(self, result: GameResultView) -> None:
"""Opcional. Incluye las manos reveladas: útil para aprender."""
Bot mínimo:
from musst.bots.api import Bot, Observation, Action
import random
class RandomBot(Bot):
name = "random"
def decide(self, observation: Observation) -> Action:
return random.choice(observation.legal_actions).sample()
Utilidades públicas (musst.bots.toolkit): evaluar la mano propia en cada lance, enumerar
manos rivales compatibles, simular Monte Carlo sobre las cartas no vistas. Son funciones
puras que trabajan sobre la Observation, nunca sobre el estado.
2. Ciclo de vida y protocolo¶
GameRunner SandboxRunner Proceso del bot
│ necesita acción del asiento 2 │ │
│──Observation────────────────────► │──JSON línea por stdin──────────► │ decide()
│ │ ◄──JSON acción por stdout────────│
│ │ (timeout: 2 s por defecto) │
│◄──Action | BotFailure─────────────│ │
│ RulesEngine.validate │ │
- Protocolo JSON por líneas, versionado (
"protocol": 1). - Si el bot devuelve una acción ilegal, excede el timeout, lanza una excepción o muere →
BotFailure(kind). - Fuera del torneo (partida con humanos): se registra el fallo y el asiento juega con un
FallbackBot(el legal más conservador: paso / no quiero / cortar mus / descartar 1) para no arruinar la partida del humano. - En el torneo: la pareja de ese bot pierde la partida (Req 4.1) y se registra el error.
3. Sandbox — qué se garantiza y qué no¶
Python no tiene un sandbox seguro dentro del mismo proceso. Por eso no se finge: la protección es por capas y está documentada.
| Capa | Mecanismo | Protege contra |
|---|---|---|
| Separación de información | El bot solo recibe la Observation JSON; nunca hay objetos del motor en su proceso |
Leer el estado secreto, manipular el marcador |
| Proceso separado | subprocess con un intérprete limpio (python -I), cwd temporal vacío, entorno mínimo sin secretos |
Acceso a memoria del motor, variables de entorno |
| Límites | resource.setrlimit (CPU, memoria, ficheros abiertos) en Linux; timeout de pared por decisión |
Bucles infinitos, consumo de memoria |
| Revisión | Los bots externos entran solo por PR revisado; bot-pr.yml ejecuta los tests y un análisis estático (prohibidos os.system, subprocess, socket, open fuera de su carpeta…) |
Código malicioso evidente |
| Entorno de torneo | GitHub Actions: contenedor efímero, sin secretos en el job del torneo, permisos contents: read |
Daño persistente |
| Producción web | El servidor solo ejecuta bots fusionados en main; nunca sube código de usuarios en caliente |
Ejecución arbitraria en el servidor |
Limitación declarada: un bot malicioso podría intentar leer ficheros del contenedor del CI.
Se mitiga con la revisión del PR y con que el job no tiene nada valioso. Si hiciera falta
aislamiento fuerte, la evolución sería ejecutar cada bot en un contenedor sin red (Docker con
--network none).
4. Registro y tests de conformidad (Req 2.2)¶
bots/
└── mi_bot/
├── bot.py # define una clase que hereda de musst.bots.api.Bot
├── bot.toml # name, author, entrypoint = "bot:MiBot", version
└── README.md # estrategia en 5-10 líneas
tests/bot_conformance/ se parametriza automáticamente con todos los bots registrados:
- Se importa y se instancia sin errores, con metadatos válidos.
- Juega 20 partidas completas (seeds fijas) contra
RandomBotySimpleBotsin acciones ilegales. - Respeta el timeout en el percentil 99.
- Es determinista si declara
deterministic = true. - No modifica la
Observation(es inmutable; se comprueba igualmente). - Supera la serialización por el protocolo de sandbox.
El workflow bot-pr.yml se dispara en los PR que tocan bots/** y publica un resumen en el PR.
5. Torneo (Req 4.1)¶
Unidad de competición: la pareja. Cada participante del torneo es un bot que ocupa los dos asientos de su pareja como dos instancias independientes (dos procesos que no comparten memoria; cada uno ve solo sus cartas). Así:
- un enfrentamiento es participante A contra participante B: un emparejamiento de dos contendientes, como pide la práctica (ver P1);
- no se rompe la información privada entre compañeros.
Formato
- Liga todos contra todos. Para cada par (A, B),
Npartidas (por defecto 20) con seeds fijas. - Mus duplicado para reducir la suerte: cada seed se juega dos veces cambiando a las parejas de asiento, de modo que cada participante recibe las cartas que tuvo el otro.
- Resultado de un fallo: derrota de la pareja infractora, error anotado; el torneo sigue.
- Reproducible:
musst-arena run --seed 2026 --games 20.
6. Clasificación y publicación (Req 4.2)¶
| Columna | Definición |
|---|---|
| Bot | nombre@versión |
| Partidas | jugadas |
| Victorias / Derrotas | |
| % Victorias | con intervalo de confianza de Wilson al 95 % |
| Tantos | a favor − en contra (media por partida) |
| Errores | ilegales + timeouts + crashes |
| Elo | opcional, para ordenar con muchos bots |
tournament.yml se ejecuta al hacer merge en main y cada noche. Genera scoreboard.json y
scoreboard.html y los publica en GitHub Pages. La web de MUS.ST lee ese JSON en la pantalla
de la Bot Arena.