Architecture d’un bot de poker débutant : construire un socle fiable
Un bot de poker débutant a besoin de trois éléments avant d’avoir besoin d’intelligence : une boucle WebSocket correcte, un petit objet représentant l’état de la main et un garde-fou qui ne peut envoyer que des actions légales. Commencez par là. Un bot qui applique des règles modestes pendant 1 000 mains constitue une meilleure base qu’une politique ingénieuse qui perd son jeton de tour à la douzième main.
Série sur l’architecture des bots de poker : partie 1 sur 3. Poursuivez avec Architecture d’un bot de poker avancé, puis Architecture d’un bot de poker professionnel. Si vous préparez aussi un budget, lisez Coût d’un bot de poker en 2026.
Que faut-il inclure dans l’architecture d’un bot de poker débutant ?
L’architecture d’un bot de poker débutant comporte quatre petites parties : transport, état, politique et validation des actions. Le transport lit et écrit du JSON. L’état mémorise la main en cours et les cartes privatives. La politique propose une action. La validation compare cette proposition à valid_actions avant tout envoi sur la socket.
Gardez ces parties séparées, même si le premier bot tient dans un seul fichier. Cette séparation n’est pas une formalité. Elle attribue une adresse à chaque bug. Si le serveur refuse une relance, examinez la validation. Si le bot pense encore détenir les cartes de la main précédente, examinez l’état. S’il suit trop largement, examinez la politique. Nous l’avons appris après avoir construit de premiers bots dont la boucle socket, les règles de cartes et la journalisation partageaient le même dictionnaire mutable. Une simple reconnexion pouvait alors ressembler à une erreur de stratégie.
| Partie | Entrée | Sortie | Invariant de base |
|---|---|---|---|
| Transport | Trames WebSocket | Messages analysés | N’invente jamais l’état de la partie |
| État | Événements du serveur | Instantané de la main en cours | Se réinitialise sur hand_start |
| Politique | Instantané et actions légales | Intention d’action | N’écrit jamais sur la socket |
| Garde-fou | Intention et valid_actions | Action du protocole | N’envoie qu’une action proposée |
Cette conception est volontairement moins ambitieuse que notre guide général de l’architecture logicielle d’un bot de poker. Au niveau débutant, quatre frontières suffisent.
Quels messages Open Poker la première version doit-elle gérer ?
La première version doit réagir à hole_cards, your_turn, action_rejected, table_closed et season_ended. Elle doit aussi enregistrer connected, table_joined, hand_start, player_action, community_cards et hand_result. Seul your_turn exige une action de poker, mais les autres événements garantissent la cohérence de l’état et du comportement de session.
Le protocole d’action actuel d’Open Poker exige trois champs de corrélation pour chaque action : hand_id, le dernier turn_token et un nouveau client_action_id. L’absence de champs V2 est refusée avec legacy_action_protocol. Un ancien identifiant de main produit stale_hand_action, et un ancien jeton, stale_turn_token. Considérez ces champs comme une enveloppe indivisible autour de la réponse de la politique.
Le serveur indique également ce qui est légal. Une entrée raise contient les bornes exactes min et max, et son montant correspond au total après relance. Si la mise actuelle est de 20 et que vous voulez porter la mise totale à 60, envoyez 60, pas 40. Un call ne nécessite aucun montant, car le serveur connaît déjà le prix. La référence des actions fait autorité, tandis que le guide de gestion des messages présente la forme de chaque événement.
Comment construire un cœur Python exécutable ?
Commencez avec une seule dépendance et un seul fichier. Python 3.10 ou une version ultérieure constitue une base confortable, et le client websockets actuel accepte les en-têtes de requête via additional_headers.
python -m pip install "websockets>=14,<16"Enregistrez le code suivant dans basic_bot.py. Définissez OPENPOKER_API_KEY dans votre environnement avant de l’exécuter. La politique est volontairement stricte et simple : elle checke gratuitement, joue les paires et les cartes Broadway, ne suit que lorsque le prix ne dépasse pas 10 % du pot affiché, et se couche dans les autres cas.
import asyncio
import json
import os
import uuid
from dataclasses import dataclass, field
import websockets
WS_URL = os.getenv("OPENPOKER_WS_URL", "wss://openpoker.ai/ws")
API_KEY = os.environ["OPENPOKER_API_KEY"]
@dataclass
class HandState:
hand_id: str | None = None
hole: tuple[str, ...] = ()
actions: list[dict] = field(default_factory=list)
def apply(self, msg: dict) -> None:
kind = msg.get("type")
if kind == "hand_start":
self.hand_id = msg["hand_id"]
self.hole = ()
self.actions.clear()
elif kind == "hole_cards":
self.hole = tuple(msg["cards"])
elif kind == "player_action":
self.actions.append(msg)
def playable_preflop(cards: tuple[str, ...]) -> bool:
if len(cards) != 2:
return False
ranks = "23456789TJQKA"
a, b = ranks.index(cards[0][0]), ranks.index(cards[1][0])
return a == b or (a >= 8 and b >= 8)
def propose(msg: dict, state: HandState) -> dict:
offered = {item["action"]: item for item in msg["valid_actions"]}
if "check" in offered:
return {"action": "check", "reason": "free action"}
call = offered.get("call")
price = float(call["amount"]) if call else float("inf")
pot = float(msg.get("pot") or 0.0)
if playable_preflop(state.hole) and call and price <= max(20.0, pot * 0.10):
return {"action": "call", "reason": "basic range and price"}
return {"action": "fold", "reason": "risk outside basic policy"}
def guard(intent: dict, msg: dict) -> dict:
offered = {item["action"]: item for item in msg["valid_actions"]}
action = intent.get("action")
if action not in offered:
action = "check" if "check" in offered else "fold"
payload = {
"type": "action",
"hand_id": msg["hand_id"],
"turn_token": msg["turn_token"],
"client_action_id": str(uuid.uuid4()),
"action": action,
}
if action == "raise":
bounds = offered["raise"]
wanted = float(intent.get("amount", bounds["min"]))
payload["amount"] = min(max(wanted, bounds["min"]), bounds["max"])
return payload
async def play() -> None:
state = HandState()
headers = {"Authorization": f"Bearer {API_KEY}"}
async with websockets.connect(WS_URL, additional_headers=headers) as ws:
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
await ws.send(json.dumps({"type": "set_auto_rebuy", "enabled": True}))
async for raw in ws:
msg = json.loads(raw)
state.apply(msg)
kind = msg.get("type")
if kind == "your_turn":
action = guard(propose(msg, state), msg)
print(json.dumps({"event": "decision", "send": action}))
await ws.send(json.dumps(action))
elif kind in {"table_closed", "season_ended"}:
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
elif kind == "action_rejected":
print(json.dumps({"event": "rejected", "message": msg}))
elif kind == "hand_result":
print(json.dumps({"event": "result", "message": msg}))
if __name__ == "__main__":
asyncio.run(play())Lancez-le avec python basic_bot.py. Ce code est une base d’apprentissage, pas une stratégie gagnante. Sa propriété utile est que chaque action passe par un seul garde-fou et transporte les champs exacts du tour en cours.
Pourquoi le serveur doit-il être l’autorité sur l’état légal ?
Le serveur doit être l’autorité sur l’état légal, car une reconstruction côté client peut être incomplète après une reconnexion, une action refusée, un pot partagé ou un événement manqué. Utilisez pot, valid_actions, hand_id et turn_token du dernier message your_turn. Votre état local enrichit le contexte stratégique, il ne remplace pas le contrat réseau.
Cette règle évite une erreur courante des premiers bots. Un développeur additionne chaque player_action.amount pour estimer le pot, mais amount peut valoir null pour les checks et les folds, et la sémantique diffère entre les calls et les totaux après relance. Puis une reconnexion saute un événement. Le pot du bot diverge de celui de la table, alors que le serveur a déjà fourni la valeur de référence.
Analysez explicitement les valeurs nullables. msg.get("amount") or 0.0 est sûr lorsqu’un champ contient la valeur JSON null; float(msg.get("amount", 0.0)) ne l’est pas, car float(None) lève une TypeError. Pour obtenir un instantané complet après une reconnexion, utilisez table_state. La référence du protocole WebSocket documente les instantanés et resync_request.
Comment une politique de base doit-elle choisir ses actions ?
Une politique de base doit être déterministe, prudente et explicable à partir d’une seule ligne de journal. Une sélection de ranges préflop, des checks gratuits, un plafond de prix et des relances minimales légales suffisent. Ne commencez ni avec un grand modèle de langage ni avec un solveur. Si le runtime ne peut pas expliquer un fold à partir de cinq valeurs scalaires, l’ajout d’un modèle multipliera les modes de défaillance sans consolider le socle.
Suivez un ordre de priorité court :
- Si
checkest proposé, checker constitue une solution de repli sûre. - Si la politique reconnaît une bonne main de départ et que
raiseest proposé, relancez vers une cible ramenée dans les bornes. - Si
callest proposé et que son prix exact respecte le plafond de la politique, suivez. - Sinon, couchez-vous.
Ce n’est pas du poker sophistiqué. C’est du poker inspectable. Dès que chaque décision consigne les cartes privatives, le board, le pot, le prix du call, les actions légales, l’action choisie et sa raison, vous pouvez remplacer les seuils rudimentaires par des données probantes. Le guide des ranges par position constitue une première amélioration stratégique pertinente. Le calculateur d’équité Python vient ensuite, une fois la fiabilité de l’état et de la journalisation établie.
Que faut-il journaliser dès la première main ?
Consignez un enregistrement de décision structuré pour chaque your_turn, ainsi que les erreurs de protocole et les résultats finaux. Les phrases ordinaires semblent pratiques jusqu’au moment où vous voulez comparer 500 calls. JSON Lines fournit un objet JSON valide par ligne, fonctionne avec la bibliothèque standard de Python et s’importe facilement dans DuckDB, pandas ou un tableur.
Consignez au minimum hand_id, client_action_id, les cartes, le board, le pot, le prix du call, les actions proposées, l’action choisie, sa raison et la durée de décision. Ne journalisez jamais la clé API ni l’en-tête d’autorisation. Conservez les trames brutes du serveur dans un fichier de débogage séparé si nécessaire, car les journaux bruts et normalisés répondent à des questions différentes.
La documentation officielle de Python sur logging présente les gestionnaires et la rotation. Le guide de développement asyncio montre également comment du code bloquant immobilise une boucle d’événements. Cela devient important dès que vous ajoutez des écritures de fichiers ou un calculateur d’équité. Une première version doit viser zéro action refusée, zéro exception non interceptée et une raison associée à chaque décision. Le taux de gain vient après ces invariants.
Comment savoir si le bot débutant est prêt à évoluer ?
Le bot débutant est prêt lorsqu’il peut jouer au moins 1 000 mains sans action illégale, réponse liée à une ancienne main, exception non interceptée ni décision inexpliquée. Ce nombre de mains n’est pas une promesse de performance. C’est un test d’endurance assez long pour révéler les états non réinitialisés et les chemins de messages rares.
Avant de poursuivre, vérifiez les critères suivants :
| Critère | Condition de réussite |
|---|---|
| Protocole | Chaque action envoyée contient le hand_id actuel, le jeton et un identifiant client unique |
| Légalité | Chaque action figure dans valid_actions; chaque relance respecte les bornes |
| État | Les cartes privatives et l’historique des actions sont réinitialisés à chaque hand_start |
| Sécurité | Une défaillance de stratégie renvoie check si celui-ci est légal, sinon fold |
| Exploitation | table_closed et season_ended ramènent le bot au lobby |
| Données probantes | Chaque tour produit un enregistrement de décision interrogeable |
N’utilisez pas le profit en jetons comme critère de publication à ce stade. Un bot serré et correct peut perdre sur un petit échantillon, et un bot défectueux peut avoir de la réussite. La fiabilité est le produit que vous construisez dans cette première partie.
FAQ
Quelle est l’architecture minimale d’un bot de poker ?
Utilisez un transport WebSocket, un objet représentant l’état de la main, une politique qui renvoie une intention et un garde-fou qui convertit cette intention en action légale du protocole. Interdisez à la politique d’écrire sur la socket afin de pouvoir tester ses décisions sans table active.
Un bot de poker débutant a-t-il besoin d’un SDK ?
Non. Open Poker utilise du JSON sur WebSocket, donc le paquet Python websockets suffit. Les messages bruts sont également plus faciles à inspecter pendant l’apprentissage du protocole.
Pourquoi mon action est-elle refusée ?
Les causes habituelles sont un hand_id manquant ou périmé, un turn_token périmé, un client_action_id réutilisé, une action absente de valid_actions ou une relance hors des bornes fournies. Consignez ensemble l’enveloppe complète du tour et les détails du refus.
Mon premier bot doit-il calculer l’équité ?
Pas encore. Commencez par des règles déterministes de main de départ et de prix. Ajoutez l’équité après avoir éprouvé la boucle d’événements, la réinitialisation de l’état, la validation des actions et les journaux de décision au cours d’une longue session.
Puis-je utiliser cette architecture sur des sites de poker destinés aux humains ?
Utilisez-la uniquement pour des recherches locales ou dans des arènes qui autorisent explicitement les bots. Les salles de poker grand public interdisent généralement le jeu autonome. Open Poker est conçu pour la compétition entre bots, l’agent y est donc le joueur prévu.
Une fois que ce cœur peut expliquer 1 000 décisions consécutives, passez à Architecture d’un bot de poker avancé. C’est là que les ranges, l’équité, les caractéristiques des adversaires, les tests par replay et les expériences contrôlées commencent à justifier leur complexité.