GameArena est un prototype d'une plateforme de programmation compétitive qui permet d'exécuter des "bots" (codes écrits par des programmeurs) qui s'affrontent dans une arène. Le rôle du Referee est de définir les règles du jeu et de piloter l'avancement tour par tour ; il n'est pas un adversaire : les joueurs sont les bots.
Résumé rapide
- But : démontrer une architecture modulaire et sécurisée pour organiser des matchs entre programmes intelligents (bots) et expérimenter des règles de jeux.
- Stack : Python (Flask) pour le backend, React + Vite pour le frontend, Docker pour l'isolation des bots.
Fonctionnalités clés
- Referee extensible : chaque jeu est implémenté comme une sous-classe de
game_sdk.Referee(contrat : init_game, step, get_state, parse_bot_output, on_bot_timeout, ...). LeRefereedéfinit le protocole, valide les actions des bots à chaque tour et met à jour l'état du jeu. - BotRunner abstrait : exécute le code des bots via plusieurs stratégies (docker, subprocess, mode parsed optimisé) sans coupler la logique du jeu à l'exécution.
- Sandbox Docker : exécution isolée des bots avec nettoyage et limites ressources — fallback en
subprocesssi utile en dev. - Frontend interactif : éditeur de code (Monaco), visualizer et panneau de logs pour rejouer l'historique d'une partie.
- Persistance minimale : bots persistants sous
persistent_bots/et index JSON pour métadonnées.
Arenas, ligues et boss
- L'arène est organisée en ligues. Chaque ligue contient des joueurs (bots) qui s'affrontent entre eux selon les règles définies pour cette ligue.
- Chaque ligue peut comporter un "boss" (un bot spécial ou une entité) que les joueurs doivent vaincre pour prétendre à la promotion.
- Progression : les bots accumulent des victoires/points dans leur ligue ; lorsqu'ils remplissent les conditions (p.ex. classement suffisant et avoir vaincu le boss), ils peuvent être promus vers la ligue supérieure, qui peut ajouter des règles ou contraintes additionnelles.
- Ce modèle permet d'introduire des règles graduelles et des variantes de jeu entre ligues sans modifier les joueurs : le
Refereeassocié à la ligue pilote ces règles.
Structure et rôles des composants
Backend (API)
- Fichier principal :
app.py— gère les routes HTTP et la validation des entrées. Le fichier contient actuellement une logique qui peut être extraite en services. - Recommandation structurelle : séparer l'API (routes) des services métier et des repositories (p.ex.
services/game_service.py,repositories/game_repository.py). Cela améliore la testabilité et le respect du principe SRP. - Endpoints clés :
- GET
/api/referees— liste des referees et description du protocole attendu par le frontend. - POST
/api/games— création d'une nouvelle partie (payload JSON avecreferee,player_code, options). - POST
/api/games/<id>/step— avancer une seule étape (tour) de la partie. - GET
/api/games/<id>/history— récupérer l'historique complet (formathistory_entrydécrit ci‑dessous).
- GET
Domain (game_sdk)
game_sdk.pycontient les abstractions centrales :Referee: contrat template-method pour exécuter une partie (init_game, step, is_finished, make_bot_input, parse_bot_output, on_bot_timeout, get_state).BotRunner: interface/stratégie pour l'exécution de bots. Implémenterun(code, input_data, timeout_ms)et gère la sélection de la stratégie (auto|docker|subprocess).
- Pattern utilisés : Template Method (Referee) et Strategy (BotRunner).
Referees (règles de jeu)
- Emplacement :
referees/. - Chaque referee implémente la logique métier du jeu : validation des mouvements, collisions, scoring, conditions de fin.
- Exemples de comportements que les referees peuvent implémenter :
- MOVE vers une cellule non adjacente : accepter une action MOVE vers une cible distante et la convertir en une série de déplacements le long du plus court chemin (algorithme BFS/A* selon la topologie).
- Evitement de collision : règles pour rejeter ou ajuster un MOVE si deux pacs tentent la même case (priorité, file d'attente, ou redirection automatique).
- Fin anticipée : terminer la partie si un joueur a suffisamment de pellets pour être mathématiquement assuré de la victoire (score actuel > adversaire + pellets_restants).
BotRunner et runners concrets
- Implémentations :
runner/docker_runner.py: exécution isolée via Docker, gestion des limites (CPU, mémoire, pids), nettoyage et collecte de logs.- Subprocess runner : exécution locale plus rapide pour le dev, sans isolation forte.
- Mode parsed : optimisation où le code du bot est analysé et exécuté en mode interactif (init + tours) pour éviter le coût de startup à chaque tour.
- Politique de sélection : par défaut
auto(heuristique basée sur timeout et configuration), sinon respecter la variableBOT_RUNNER.
Persistance et stockage
- Bots persistants :
persistent_bots/<bot_id>/bot.pyet métadonnées danspersistent_bots/games_index.json. - Base locale : SQLite (
gamearena.db) utilisée pour stocker objets applicatifs (utilisateurs, parties, leagues) selonmodels.py. - Recommandation : séparer entités domain et modèles SQLAlchemy si une refactorisation est entreprise.
Frontend
- Emplacement :
frontend/(React + Vite). Contient l'éditeur (Monaco), la visualisation (PixiJS) et les composants UI. - Fichiers notables :
frontend/src/components/MonacoEditor.jsx— éditeur de code.frontend/src/components/Visualizer.jsx— rendu et animation.frontend/src/pages/PlaygroundPage.jsx— orchestration (peut être découpée en hooks et services).
- Communication : le frontend consomme l'API JSON exposée par le backend. Pour le dev, configurez
VITE_API_BASEsi nécessaire.
Format history_entry (aperçu)
- Un
history_entryreprésente l'état et les actions d'un tour. Exemple schématique :- { "turn": 3, "actions": [{"player_id":"p1","action":"MOVE 3 2"}, ...], "state": { ... }, "timestamp": "..." }
- Le frontend utilise la liste des
history_entrypour reproduire la partie image par image.
Ci-dessous la spécification complète et normative du format JSON renvoyé par l'API (POST /api/games/<game_id>/step) dans le champ history_entry, et stocké dans Referee.history.
Objectif
- Permettre au frontend (visualizer), aux outils d'analyse et aux tests de rejouer précisément chaque tour.
- Fournir suffisamment d'information pour : afficher l'état, montrer les sorties des bots, diagnostiquer erreurs et rejouer la logique côté client.
Contrat général (résumé)
- Type racine : objet JSON
- Champs principaux :
turn(int) : numéro du tour (0-based ou 1-based selon referee, vérifier le referee; la plupart des referees utilisent 0-based pour le premier état après init). Obligatoire.state(object) : snapshot sérialisable de l'état du jeu après application des actions de ce tour. Obligatoire.actions(array) : liste des actions demandées/appliquées ce tour. Optionnel mais recommandé (vide si aucun mouvement). Chaque entrée d'action décrit qui a joué et quelle action a été appliquée.bot_logs(object) : logs produits par les runners pour chaque rôle/joueur (structure injectée côté API — voirapp.py). Optionnel mais très utile pour debug.stdout,stderr(string) : sorties textuelles provenant du referee (résumé du turn) — facultatif.__global_stdout,__global_stderr(string) : champs spéciaux parfois ajoutés par l'API pour contenir sorties globales retournées lors d'un stepfinished(voir usage dans le frontend). Optionnel.timestamp(string) : horodatage ISO 8601 du moment où le tour a été produit. Optionnel mais recommandé.meta(object) : champ libre pour ajouter métadonnées (ex: events, collision resolution details, reason_for_end). Optionnel.
Exemple JSON minimal
{ "turn": 12, "state": { /* état sérialisable pour UI (positions, scores, map, pellets, etc.) */ }, "actions": [ { "player_id": "player", "raw": "MOVE 5 3", "resolved": "MOVE 5 3" }, { "player_id": "opponent", "raw": "SPEED", "resolved": "SPEED" } ], "bot_logs": { "player": { "stdout": "...", "stderr": "...", "rc": 0, "runner": "docker" }, "opponent": { "stdout": "...", "stderr": "...", "rc": 0, "runner": "subprocess" } }, "timestamp": "2025-11-20T12:34:56.789Z" }
Schema détaillé (description)
-
turn: integer — numéro du tour (obligatoire)
-
state: object — représentation complète de l'état du jeu après le tour. Contenu dépend du Referee mais doit être entièrement sérialisable JSON et contenir au minimum les éléments nécessaires au visualizer :
- positions des entités (pacs, players) avec leurs identifiants
- carte / grille (si nécessaire) ou delta d'affichage
- scores / points par joueur
- pellets restants / leur valeur si applicable
- flags game-specific (ex: abilities actives : speed_remaining, switched_type)
Exemples de clés communes dans
state(non obligatoires universellement) :players: { "player": {"pacs": [{"id": "p1", "x": 3, "y": 2, "alive": true, "ability": null}], "score": 10 }, "opponent": {...} }map: { "width": 10, "height": 8, "cells": ["....", "##..", ...] }turn: duplicate facultatif du champ racine pour facilité
-
actions: array of objects. Chaque objet d'action peut contenir :
player_id(string) — rôle/id du joueur (par ex. "player", "opponent", ou "bot:123")raw(string) — la sortie brute du bot ou la commande demandéeparsed(object|string) — (optionnel) représentation parsée de l'action (ex: {"type":"MOVE","target":{"x":5,"y":3}})resolved(string) — (optionnel) action réellement appliquée après validation/résolution de conflitnotes(string) — (optionnel) message expliquant invalidation ou ajustement
-
bot_logs: object mapping role -> log object
- pour chaque rôle attendu (p.ex. "player", "opponent") :
stdout(string) : sortie standard complète collectée pendant l'exécution (init + turn si parsed)stderr(string) : sortie d'erreurrc(int) : return code (0 pour succès, -1 ou autre pour erreur)runner(string) : stratégie runner utilisée ("docker", "subprocess", "parsed", "exception")
Observations :
app.pyinjecte ces logs après exécution via : entry['bot_logs'] = {'player': player_log, 'opponent': opponent_log}- Les champs peuvent contenir plusieurs lignes; le frontend concatène
stdout/stderrpour l'affichage.
- pour chaque rôle attendu (p.ex. "player", "opponent") :
-
stdout / stderr : chaînes (optionnel)
stdoutetstderrpeuvent contenir résumés ou sorties spécifiques générées par le Referee (ex: messages sur collisions, scoring changes). L'API ajoute parfois__global_stdout/__global_stderrpour messages envoyés lors d'une réponsefinished.
-
meta: object (optionnel)
events: liste d'événements survenus (p.ex. [ {"type":"collision","players":["player","opponent"],"pos":{"x":3,"y":2},"resolution":"player_survives"} ])end_reason: si la partie se termine, fournir une explication technique (ex: "math_win_by_pellets_remaining")pathfinding_debug: (optionnel) array ou object décrivant les chemins calculés pour MOVE non-adjacent
Bonnes pratiques pour la production des history_entry
- Rendre
stateaussi compacte que possible tout en conservant l'information nécessaire au visualizer (p.ex. serializer minimal des entités au lieu d'objets lourds). - Utiliser des types simples (int/string/array/object) et éviter les objets non sérialisables (datetime natif, sets, objets Python complexes) — sérialiser en ISO8601 pour les timestamps.
- Toujours inclure
bot_logsquand un bot a été exécuté : cela facilite le debug et l'audit. - Documenter dans le
Referee.get_protocol()les attentes sur le formatstateet les clés essentielles que le frontend utilisera.
Compatibilité frontend
- Le frontend s'attend à trouver au minimum :
turn,stateet/oustdout/stderrpour chaquehistory_entry. - Si l'API retourne des objets
__global_stdoutet__global_stderrlors d'un dernier step (finished), le frontend les traite comme des entrées globales séparées. Le champhistoryretourné parGET /api/games/<id>/historyest une liste où certains éléments peuvent être des objets spéciaux contenant__global_stdoutkeys.
Exemples complets
- Tour normal avec deux actions et logs :
{ "turn": 5, "state": { "players": { "player": {"score": 12, "pacs": [{"id":"p1","x":4,"y":2,"alive":true}]}, "opponent": {"score": 8, "pacs": [{"id":"o1","x":7,"y":1,"alive":true}]} }, "map": {"width":10, "height":8} }, "actions": [ {"player_id":"player","raw":"MOVE 4 2","parsed":{"type":"MOVE","x":4,"y":2},"resolved":"MOVE 4 2"}, {"player_id":"opponent","raw":"SPEED","resolved":"SPEED"} ], "bot_logs": { "player": {"stdout":"init\nMOVE 4 2\n","stderr":"","rc":0,"runner":"docker"}, "opponent": {"stdout":"SPEED\n","stderr":"","rc":0,"runner":"subprocess"} }, "timestamp": "2025-11-20T12:34:56.789Z" }
- Réponse finale (finished) — l'API peut renvoyer un
historycomplet et des entrées globales :
[ { /* history_entry tour 0 / }, { / history_entry tour N */ }, { "__global_stdout": "Match finished: player wins, final score 20-10" } ]
Notes pour les auteurs de Referee
- Lorsque vous remplissez
ref.history.append(entry)dans votre Referee, respectez le contrat ci-dessus :entry['turn']etentry['state']doivent toujours être présents.- Ajoutez
entry['bot_logs']uniquement si votre Referee n'attend pas que l'API (layer supérieur) injecte les logs ; par défautapp.pyinjecte les logs côté routestep. - Pour la compatibilité avec le visualizer, exposez les clés attendues (positions, scores, map).
Tests et validation
- Écrire des tests unitaires qui vérifient la présence des champs obligatoires et la sérialisation JSON (ex: tests/tests_referee_history.py).
- Exemple d'assertions :
assert 'turn' in entry and isinstance(entry['turn'], int)assert 'state' in entry and isinstance(entry['state'], dict)if 'bot_logs' in entry: assert 'player' in entry['bot_logs']
Si vous souhaitez que j'ajoute une section détaillée sur le format exact de history_entry, un diagramme d'architecture SVG simple, ou un guide pour écrire un nouveau Referee étape par étape, dites-moi lequel et je l'ajoute.