03 — Reglas, máquina de estados, GameEngine y puntuación¶
1. Reglas: qué dice el Reglamento FEM y qué no¶
El Reglamento FEM regula sobre todo procedimiento (sorteo, reparto, mus, envites, tanteo, señas, errores y penalizaciones), pero no define el valor de las cartas ni de las jugadas: los da por sabidos. Por eso las reglas se dividen en dos tipos:
- [FEM]: implementada según el artículo citado.
- [CONVENCIÓN]: regla tradicional del Mus (ocho reyes) que la FEM no escribe. Se implementa
en
config/rules.default.toml, se documenta y se lista en las preguntas al profesor (P5).
1.1 Baraja y valores¶
| Regla | Tipo | Detalle |
|---|---|---|
| Baraja española de 40 naipes (1-7, sota, caballo, rey; oros, copas, espadas, bastos) | FEM II.3 | Sin 8 ni 9 |
| Ocho reyes y ocho ases: el 3 cuenta como rey y el 2 como as | FEM II.3 | Configurable (modalidad = "8R8A"); otra modalidad solo por configuración |
| Orden en grande: R(12,3) > C(11) > S(10) > 7 > 6 > 5 > 4 > A(1,2) | CONVENCIÓN | Chica: el orden inverso |
| Valor para juego/punto: R, C, S = 10 (el 3, al ser rey, vale 10); A (1 y 2) = 1; resto, su número | CONVENCIÓN | |
| Pares: par (2 iguales), medias (3), duples (2 pares o 4 iguales) | FEM VIII.2 (los nombra) / CONVENCIÓN (valor) | |
| Juego: suma ≥ 31. Orden: 31 > 32 > 40 > 37 > 36 > 35 > 34 > 33 | FEM nombra 31 como máximo y 33 como mínimo (VI.7-8) / CONVENCIÓN (orden completo) | |
| Punto: si nadie tiene juego, gana la suma más alta (máximo 30) | FEM VI.7, VIII.8 | |
| Empate en cualquier lance: gana el más cercano al mano | CONVENCIÓN |
1.2 Desarrollo de una jugada¶
| Paso | Regla | Fuente |
|---|---|---|
| Asientos | 4 asientos fijos; pareja A = {0, 2}, pareja B = {1, 3}. No cambian durante la partida | FEM II.1-2 |
| Primer reparto | Por sorteo de palo (se simula con la seed) | FEM III.1 |
| Reparto | 4 cartas, de una en una, de derecha a izquierda (antihorario) | FEM III.2 |
| Mano | El jugador a la derecha del que reparte; la mano rota cada jugada | FEM VIII.8 |
| Visibilidad | Nadie ve las cartas hasta terminar el reparto | FEM III.6 (garantizado por diseño) |
| Mus | Habla primero el mano. El postre de la pareja contraria solo puede cortar después de que el mano dé mus; el compañero del mano puede cortar siempre; el cuarto, cuando la pareja mano lo haya dado | FEM IV.4 |
| Descartes | Si los cuatro dan mus, cada uno descarta de 1 a 4 cartas y se repone; si se acaba el mazo, se rebaraja el descarte | FEM III.11, III.15 · CONVENCIÓN (mínimo 1) |
| A falta de tanto | El mus puede cortarse en cualquier momento | FEM IV.7 (ver P6) |
| Lances | Grande → Chica → Pares → Juego o Punto | FEM VII.2 |
| Declaración | En pares y juego se declara públicamente sí/no; solo hablan quienes los tienen | FEM VI.5, VI.22, IX.1 |
| Mano del lance | En pares/juego, el primero que los tiene | FEM VIII.8 |
| Órdago | Solo a un lance cada vez | FEM IV.6 |
| Mostrar cartas | Los cuatro, al acabar el lance de juego o punto, o cuando se acepta un órdago | FEM VII.9, VI.6 |
Decisión de diseño — turnos secuenciales. En la mesa física los jugadores pueden hablar "a la vez" (FEM VI.1, VI.4, IV.4). En digital, el motor da la palabra de uno en uno en orden antihorario empezando por el mano del lance. Las reglas de prioridad de la FEM se traducen así:
- VI.4 ("si uno quiere y su compañero no quiere, vale el quiero"): si el primer rival responde no quiero, la palabra pasa a su compañero. Solo es no quiero si lo dicen los dos.
- VI.2 (no se sube hasta que los contrarios respondan): sale solo, porque mientras hay una apuesta pendiente el turno es de la pareja contraria.
- VI.1 (envites distintos simultáneos): no puede ocurrir.
- IV.4: el orden secuencial cumple siempre las condiciones del artículo.
1.3 Envites¶
| Acción | Significado | Fuente |
|---|---|---|
Pass (paso) |
No envidar | FEM VIII.8 |
Bet(n) |
Envido: n = 2 por defecto; "envido N" con N ≥ 2 | FEM VIII.8 |
Raise(n) |
Reenvite: subir la apuesta (ver P7: FEM define "reenvido" como doblar) | FEM VIII.8 |
Accept (quiero) |
FEM VI.3-4 | |
Reject (no quiero) |
FEM VI.3-4 | |
Ordago |
Apostar el juego completo | FEM VIII.8 |
AcceptNoLose ("quiero y no pierdo") |
La única forma legal de que el postre con 33 acepte un órdago a juego | FEM VI.9-10 |
Restricciones que el RulesEngine comprueba:
- Solo puede envidar o responder en pares/juego quien los tiene (FEM VI.5, VI.22-23).
- El postre con 33 no puede aceptar un órdago a juego salvo con
AcceptNoLose(FEM VI.9). - Cualquier envite puede aceptarse, aunque la cantidad supere los tantos que faltan (FEM VI.6, VI.8).
1.4 Artículos FEM que no aplican en digital (y por qué)¶
| Artículos | Situación física | Tratamiento digital |
|---|---|---|
| III.4-5, III.7-10, III.12-14, III.16 | Naipes de más/menos, mus visto, carta descubierta, contar el mazo | Imposibles por construcción. El motor reparte siempre 4 cartas y la UI no expone el mazo. Se documentan para justificarlo en la defensa. |
| VI.10-11, VI.13, VI.24-25, VII.10-11 | Enseñar naipes, separar amarracos, mirar cartas del contrario | Imposible: no existe acción "enseñar". |
| VI.15-20, VIII.4-6 | Declarar jugadas, señas parciales | La declaración sí/no la hace el motor (siempre veraz) y las señas van por SignalSystem. |
| IX.1 | Errores al declarar pares/juego | Imposible: el motor calcula la declaración. |
| V | Tiempo de juego | Se traduce en un timeout por acción (configurable) en el GameRunner. |
| I, X, XI, XII | Torneos presenciales, árbitros, actas | Inspiran el diseño de la Arena; no son reglas del motor. |
1.5 SignalSystem (preparado, desactivado en v1)¶
Modela las señas oficiales de FEM VIII.2 (dos reyes, dos ases, pares, medias de reyes, medias de ases, medias, duples, juego, treinta y una, treinta, veintinueve, ciego, cortar el mus, envidar más) con sus restricciones (VIII.3-6: no antes de cortar el mus, medias no-reyes tras la grande, sin señas parciales, solo para transmitir la jugada propia).
Una seña es una SignalAction que el RulesEngine valida contra las cartas reales del
emisor (no se puede mentir con una seña, VIII.4). Genera un evento privado para el
compañero, con probabilidad configurable de que lo vea también cada rival (interception_p),
para simular que en la mesa física los rivales pueden ver las señas. Queda desactivado en v1
(signals.enabled = false): con señas, el modelado de información se complica mucho y eso
debe validarse antes.
2. Máquina de estados¶
2.1 Estado de la partida (runtime)¶
WAITING ──start──► PLAYING ◄──resume── PAUSED
│ └──pause/desconexión──►┘
├──fin de juego──► FINISHED
└──error irrecuperable──► ERROR (la partida se guarda; el torneo continúa)
2.2 Fases de una jugada (engine)¶
┌──────────────────────────────────────────────────────────┐
▼ │
DEALING │
▼ │
MUS_DECISION ──todos "mus"──► DISCARD ──reposición──┐ │
│ alguien corta │ │
▼ ◄──────────────────────┘ (vuelve a MUS_DECISION)
GRANDE ──órdago aceptado──────────────────────┐ │
▼ │ │
CHICA ──órdago aceptado─────────────────────►│ │
▼ │ │
PARES_DECLARATION │ │
├─ ambas parejas tienen ─► PARES ─órdago──►│ │
├─ solo una / ninguna (sin envites) ───┐ │ │
▼ ▼ │ │
JUEGO_DECLARATION ◄─────────────────────────────┘ │ │
├─ ambas tienen juego ─► JUEGO ─órdago────►│ │
├─ una sola tiene juego (sin envites) ─┐ │ │
├─ nadie tiene juego ─► PUNTO ─órdago─►│ │ │
▼ ▼ ▼ │
SHOWDOWN (los 4 enseñan, FEM VII.9) ◄──────── ORDAGO_SHOWDOWN │
▼ │ │
SCORING (tanteo en orden de lances) │ │
▼ ▼ │
CHECK_GAME_END ──no──► rotar mano ────────────────────────────────┘
│ sí
▼
GAME_OVER
Cada lance de envites tiene su propia sub-máquina:
OPEN(turno = mano del lance) ──Pass──► siguiente jugador / si todos pasan ► CLOSED_PASSED
│ Bet / Ordago
▼
PENDING(apuesta, pareja que responde)
├─ Accept / AcceptNoLose ─────────────► CLOSED_ACCEPTED (o ORDAGO_SHOWDOWN)
├─ Reject (primer rival) ─────────────► PENDING (responde su compañero)
├─ Reject (los dos) ──────────────────► CLOSED_REJECTED (negada o deje en el acto)
└─ Raise / Ordago ────────────────────► PENDING (responde la otra pareja)
Las transiciones están en una tabla explícita (fsm.py: ALLOWED: dict[Phase, set[Phase]]).
Cualquier transición que no esté en la tabla lanza IllegalTransition, y un test recorre la
tabla completa.
3. Diseño del GameEngine¶
3.1 Acciones tipadas¶
@dataclass(frozen=True)
class MusAction: # dar mus
@dataclass(frozen=True)
class CutMusAction: # cortar ("no hay mus")
@dataclass(frozen=True)
class DiscardAction: cards: tuple[Card, ...] # 1..4
@dataclass(frozen=True)
class PassAction:
@dataclass(frozen=True)
class BetAction: amount: int # ≥ min_bet (2)
@dataclass(frozen=True)
class RaiseAction: amount: int # incremento sobre la apuesta vigente
@dataclass(frozen=True)
class AcceptAction:
@dataclass(frozen=True)
class AcceptNoLoseAction: # "quiero y no pierdo" (FEM VI.9-10)
@dataclass(frozen=True)
class RejectAction:
@dataclass(frozen=True)
class OrdagoAction:
@dataclass(frozen=True)
class SignalAction: signal: Signal # desactivado en v1
Action = MusAction | CutMusAction | DiscardAction | ... # unión cerrada
Las acciones se serializan a JSON con un discriminador "type" para el API web y el sandbox.
3.2 Estado (secreto, solo en el motor)¶
@dataclass(frozen=True)
class GameState:
config: RulesConfig
seats: tuple[SeatInfo, SeatInfo, SeatInfo, SeatInfo]
score: dict[TeamId, int]
dealer: Seat
phase: Phase
round: RoundState | None # jugada en curso
rng_state: RngState # para reproducibilidad
status: GameStatus
@dataclass(frozen=True)
class RoundState:
hands: tuple[Hand, Hand, Hand, Hand] # ← SECRETO
deck: tuple[Card, ...] # ← SECRETO
discard_pile: tuple[Card, ...] # ← SECRETO
mus_rounds: int
turn: Seat | None
lances: dict[Lance, LanceState] # apuestas, resultado pendiente
declarations: dict[Lance, dict[Seat, bool]] # pares sí/no, juego sí/no (público)
pending_points: list[PendingPoints] # se tantea al final (FEM VI.6)
3.3 Interfaz pública del motor¶
class GameEngine:
def new_game(self, config: RulesConfig, seed: int) -> tuple[GameState, list[GameEvent]]: ...
def apply(
self, state: GameState, seat: Seat, action: Action
) -> tuple[GameState, list[GameEvent]]:
"""Valida con RulesEngine; si es ilegal, lanza IllegalAction y no cambia nada."""
class RulesEngine:
def legal_actions(self, state: GameState, seat: Seat) -> list[ActionTemplate]: ...
def validate(self, state: GameState, seat: Seat, action: Action) -> Validation: ...
- Funciones puras: el mismo estado y la misma acción dan siempre el mismo resultado. El
azar sale de un
random.Random(seed)cuyo estado forma parte delGameState. - Nadie fuera del motor construye un
GameState: los bots recibenObservation. ActionTemplatedescribe acciones parametrizadas (por ejemploBet(min=2, max=None),Discard(min=1, max=4)), para que el frontend y los bots sepan qué pueden hacer.- API de alto nivel para el Req 1.1:
from musst import play_game
from musst.bots.builtin import SimpleBot
result = play_game(bots=[SimpleBot(), SimpleBot(), SimpleBot(), SimpleBot()], seed=12345)
print(result.winner, result.final_score, len(result.events))
4. Sistema de puntuación¶
4.1 Cuándo se apunta (FEM VII.2, VI.6)¶
| Momento | Qué se apunta |
|---|---|
| En el acto, durante el lance | Negada (1 tanto por no aceptar la primera apuesta de un lance) o el valor de los envites revocados y no aceptados; el deje en pares/juego/punto |
| Al final de la jugada, en orden grande → chica → pares → juego/punto | Envites aceptados; grande y chica en paso (1); valor de los pares; valor del juego; punto (1) |
| Órdago aceptado | Se enseñan las cartas; quien gana el lance gana el juego. Se anulan los envites aceptados en lances anteriores (FEM VII.12) |
4.2 Valores [CONVENCIÓN, configurables]¶
| Concepto | Valor por defecto |
|---|---|
| Negada | 1 |
| Grande o chica en paso | 1 |
| Par / medias / duples | 1 / 2 / 3 por cada jugador de la pareja ganadora que los tenga |
| Juego 31 / otro juego | 3 / 2 por cada jugador de la pareja ganadora con juego |
| Punto | 1 |
| Envite rechazado tras subidas | La pareja que subió gana el valor de la apuesta vigente antes de su subida |
4.3 Fin del juego¶
- Objetivo por defecto: 40 tantos (FEM Intro E: entre 40 y 60).
- Como el tanteo final va en orden de lances (FEM VII.2), gana la primera pareja que llegue al objetivo al contar, aunque la otra fuese a sumar más en un lance posterior.
- Si una negada en el acto lleva a una pareja al objetivo, ¿termina el juego inmediatamente? → configurable, por defecto sí (pregunta P8).
- Al faltar ≤ 4 tantos: evento
TeamIsIn("estamos dentro", FEM VII.7) y se activa el modo "a falta de tanto" (FEM IV.7). - Partida = N juegos (FEM: "a cinco juegos"); v1 usa
games_to_win = 1(pregunta P4).
4.4 Amarracos¶
Representación visual: 1 amarraco = 5 tantos; los tantos sueltos se muestran como piedras
(FEM VII.3, VII.7). Es solo presentación (score // 5, score % 5); el motor trabaja en tantos.