Créez votre IDEAL Arena avec PytactX 🎲

Vous avez aimé PytactX et vous aimeriez créer votre propre arène avec vos propres règles du jeu en Python ? Ce tutoriel est fait pour vous !

Vous y apprendrez comment devenir l’architecte de votre IDEAL Arena pour pouvoir tout personnaliser : les graphiques, les personnages, les armes, la carte de jeu …

⚙️ Prérequis

🔍 Information
Pour aller plus vite ou si vous êtes bloqué, n’hésitez pas à nous contacter ici si vous souhaitez des cours particuliers en présentiel ou distanciel 😉

👮 Tout est Agent

Avec l’API PyTactX, vous incarnez un agent (un robot virtuel) dans une arène, qui exécute à la lettre les requêtes que vous lui avez dictées dans votre script Python.

Dans le premier tutoriel, vous avez pris en main les variables et les fonctions de cet agent pour le faire évoluer dans une arène.

Grâce à la fonction agent.update(), votre script envoyait les requêtes précédemment formulées (par exemple agent.moveTowards(5,5) pour se déplacer vers le milieu de l’arène) au serveur de jeu. Ce dernier vérifiait la validité de la requête (est-ce que vous êtes encore en vie, est-ce qu’il n’y a personne sur votre chemin) et si tout était valide, le serveur vous renvoyait ensuite le nouvel état de votre agent, que vous pouviez interroger après l’appel de la fonction agent.update() grâce à ses variables d’état (agent.life, agent.x, agent.y …).

Pour créer votre propre jeu, nous allons faire de même en développant et en exécutant un script d’un agent un peu spécial, qu’on nommera arbiter, qui sera chargé de surveiller l’état du jeu (grâce à ses variables arbiter.game et arbiter.range) et de modifier l’état du jeu et de l’arène grâce à sa fonction arbiter.ruleArena.

Pour donner à votre agent arbitre les supers pouvoirs de Néo (comme le droit de modifier l’arène, de se téléporter, d’être invisible des autres agents et invincible), il faudra à sa création lui donner un nom spécifique qui vous sera communiqué lors de la création de votre arène privée.

Important
Il ne peut y avoir qu’un seul agent avec le même nom connecté à la fois dans une arène. A votre charge donc de traiter votre arbitre comme celui-dont-on-ne-doit-pas-prononcer-le-nom ! Ne communiquez jamais son nom ni votre programme de manière publique ! Si vous êtes amené à travail sur des repositories publiques (github, gitlab, replit…), asserez-vous de stocker le mot de passe dans un fichier .env à rajouter dans un .ignore pour ne jamais le commit (sur github, gitlab par exemple), ou bien utilisez la fonction getpass() (du module du même nom) en Python dans votre code pour rentrer son nom à chaque exécution dans la console.

🔍 Information
Même sans super pouvoir, tout agent « normal » peut surveiller l’état du jeu et calculer des scores et implémenter d’autres règles du jeu en surveillant ses variables agent.game et agent.range. Cependant, il ne pourra pas changer l’état du jeu, seul l’arbitre à ce privilège.

🔑 Le maître des clés

Comment implémenter nos règles du jeu dans le script de cet Agent arbitre ?

Et bien grâce à ces 2 variables dictionnaires arbiter.game et arbiter.range contenant de nombreuses clés qui vous permettront de personnaliser le jeu à l’infini !

Dans la variable arbiter.game, sont inventoriés en temps réel toutes les règles ainsi que l’état du jeu. En guise de documentation, voici le dictionnaire initial de toute arène basée sur le moteur TactX. Dans ce dictionnaire, les clé:valeur

  • « ini » : valeur donnée à la réinitialisation de l’arène. Ce sont uniquement les valeurs associées à cette clé « ini » qui sont présentes dans leur dico agent.game (si la clé publish est à True).
  • « ?fr » explique le rôle de chaque règle clé de ce dictionnaire en guise de wiki
  • « version » indique la version du moteur TactX à partir de laquelle la clé et la fonctionnalité est implémentée.
  • « group » indique que si une modification de dimension sur une clé du groupe est faite (ex: ajout d’un profile d’un joueur), toutes les autres clés du même groupe seront redimensionnées également
  • « reset » indique qu’une modification de la règle entraînera un reset de l’arène
# 🚷 Règles pour gérer les joueurs
"score" : { "?fr":"Classe pour le calcul du score et rank: ''=aucun calcul fait par l'arene, 'DM':compte ratio kill/death+fire/hits, 'Race':course de checkpoints avec START et FINISH, 'TSP':course du voyageur sur checkpoints", "publish":True, "ini":
	"DM" },
"hitTeam" : { "?fr":"Si oui ou non tir et degat ami possible entre agents de même team", "publish":False, "ini":
	True },
"acls" : { "?fr":"Dico des joueurs consulté à chaque connexion. ClientId en clé.", "publish":False, "ini":
	{
		"@arbiter": {
			"player": "Arbitre",
			"robot": "",
			"team": 0,
			"profile": 3
		}
	} },

# 🖼️ Règle pour customiser l'arène : taille, images, couleurs ...
"gridColumns" : { "?fr":"Nombre de colonnes dans la grille", "min":0, "max":50, "publish":True, "reset":True, "ini":
	29 },
"gridRows" : { "?fr":"Nombre de lignes dans la grille", "min":0, "max":50, "publish":True, "reset":True, "ini":
	29 },
"bgImg" : { "?fr":"Url de l'image de fond", "publish":True, "ini":
	"bg.jpg" },
"bgColor" : { "?fr":"Code rgba du fond de l'arène par dessus bgImg", "publish":True, "ini":
	[255,255,255,0.4] },
"gridColor" : { "?fr":"Code rgba des lignes et colonnes de l'arène", "publish":True, "ini":
	[255,255,255,0.4] },
"hitImg" : { "?fr":"Url de l'image de dégat. Cercle rouge par défaut", "publish":True, "ini":
	"" },
"gravity" : { "?fr":"Appliquer une force de gravité avec le vecteur spécifié et faire tomber tous les agents avec une masse. (0,0) pour désactiver, (0,9.81) pour faire tomber en bas de la carte.", "publish":True, "ini":
	(0,0) },
"spawnMode" : {"?fr":"Selection du mode de spawn des joueurs. 0=n'importe où, 1=dans spawnAreas, 2=sur zone selon teamSpawn", "min":0, "max":2, "publish":True, "version":203, "ini":
	1 },

# 🏁 Règles de la map et des cases 
"borderHit" : {"?fr":"Enlève des points de vie si l'agent tente de sortir de la map", "min":0, "max":10000, "publish":True, "ini":
	1 },
"brownianMap" : {"?fr":"True pour faire en sorte que les 2 bords opposés de la carte soient contigues, comme snake", "publish":True, "ini":
	True },
"canLeaveMap" : {"?fr":"Si oui ou non possible de sortir de la map", "publish":True, "version": 203, "ini":
	False },
"map" : {"?fr":"Liste 2D de profile d'obstacles sur la grille. Valeur int correspond au profile d'obstacle", "publish":True, "ini":
	[] },
"mapRand" : {"?fr":"Generation random d'obstacle sur la grille", "publish":False, "ini":
	True },
"mapRandFreq" : {"?fr":"Frequence random d'obstacle sur la grille", "publish":False, "ini":
	0.10 },
"mapImgs" : { "?fr":"Url des images de chaque profiles d'obstacle. Chaine vide => dessin cercle. Possible code rgba()", "publish":True, "group":"tiles", "ini":
	["", ""] },
"mapFriction" : { "?fr":"Coef de friction de la case. 0: pas d'obstacle, 1: insurmontable", "publish":True, "group":"tiles", "ini":
	[0.0, 1.0] },
"mapHit" : { "?fr":"Dégats infligés en cas de collision avec la case", "publish":True, "group":"tiles", "ini":
	[0, 10] },
"mapBreakable" : { "?fr":"Si oui ou non la case peut être cassée par collision", "publish":True, "group":"tiles", "ini":
	[False, True] },

# 🚩 Règles des checkpoints pour mode course
"checkpoints" : {"?fr":"Dict des checkpoints avec clés et valeurs telles que 'labelCp':{'x':intSurMap,'y':intSurMap,'i':idLabel, 'l':{labelLinkedNodeStr:intWeight}}", "publish":True, "version": 203, "ini":
	{} },
"cpGraphml" : {"?fr":"Url vers fichier des checkpoints au reset généré via export sur https://graphonline.ru/fr", "publish":True, "version": 203, "ini":
	"graphml.txt" },
"cpRandN" : {"?fr":"Nombre de noeuds générés aléatoirement pour graphe au reset. 0 pour désactiver. Si > 0, ne tient pas compte de cpGraphml.", "publish":True, "version": 244, "ini":
	0 },
"cpRandMode" : {"?fr":"Mode de course pour la génération du graphe aléatoire. 0=Voyageur du commerce avec que 1 noeud START (=FINISH), 1=Course START!=FINISH.", "publish":True, "version": 244, "ini":
	0 },
"cpRandFreq" : {"?fr":"Probabilité de lien entre noeuds sur graphe généré aléatoirement. 0=aucun lien, 1=tous les noeuds sont reliés entre eux. 0 pour désactiver.", "publish":True, "version": 244, "ini":
	1.0 },

# 🗺️ Règles par zone
"areas" : { "?fr":"Nom des zones", "publish":True, "version":203, "group": "areas", "ini": 
	["SPAWN ZONE", "KILL REWARD ZONE"] },
"aIcons" : { "?fr":"Nom des zones", "publish":True, "version":206, "group": "areas", "ini":
	["❤️‍🩹", "🔫⭐"] },
"areasColor" : { "?fr":"Couleur de chaque equipe en code RGBA", "publish":True, "version": "203", "group": "areas", "ini":
	[[0,0,0,0], [0,0,0,0.2]] },
"areasX" : { "?fr":"Abscisse des zones", "publish":True, "version":203, "group": "areas", "ini":
	[0, 0] },
"areasY" : { "?fr":"Ordonnée des zones", "publish":True, "version":203, "group": "areas", "ini":
	[0, 0] },
"areasW" : { "?fr":"Largeur des zones. 0=Toute la largeur possible", "publish":True, "version":203, "group": "areas", "ini":
	[1, 0] },
"areasH" : { "?fr":"Hauteur des zones 0=Toute la hauteur possible", "publish":True, "version":203, "group": "areas", "ini":
	[1, 0] },
"areasPUps" : { "?fr":"Power ups accordées quand joueur sur zone actif pendant dureeMs, par zone un dico de {attributNom:[valeur,dureeMs,opStr]} avec opStr operateur soit '=','+'ou'*, et attributNom soit attribut player soit nom clé profile player", "publish":True, "version":206, "group": "areas", "ini":
	[{"life":[100,0,'=']}, {"life":[20,10000,'+'],"ammo":[10,10000,'+']}] },
"areasPUpsDt" : { "?fr":"Power ups accordées quand joueur sur zone, toutes les dtMs spécifiées", "publish":True, "version":206, "group": "areas", "ini":
	[10000, 1000] },
"areasPUpsEv" : { "?fr":"Power ups accordées quand joueur sur zone, et que évènement survient sur attribut du joueur spécifié. 🆕Remplace clé KillReward depuis v206🆕", "publish":True, "version":206, "group": "areas", "ini":
	[[], ["nKill"]] },
"spawnAreas" : { "?fr":"Précise les zones de naissance des agents si spawnMode=1", "publish":True, "version":203, "ini":
	[0] },

# 🔫 Règles par profile d'arme
"weapons" : { "?fr":"Liste des profiles d'armes avec stats différentes. 'none' pour ne pas afficher de rendu dans le viewer", "publish":True, "group": "weapons", "ini":
	["none", "beam", "wave", "spray", "launcher", "force"] },
"bullet":  { "?fr":"Créer un agent projectile au tir avec le profile spécifié. -1 pour aucune balle.", "publish":True, "group": "weapons", "ini":
	[-1,-1,-1,-1,4,-1] },
"wIcons" : { "?fr":"Liste des icones d'armes", "publish":True, "group": "weapons", "ini":
	["", "🎇", "💣", "🔥", "🚀", "🤜"] },
"fireImgs" : { "?fr":"Url de l image de tir. Par defaut si vide : trait dans direction tir si spreadFire=0, sinon cercle", "publish":True, "group": "weapons", "ini":
	["","","","","",""] },
"dtFire" : { "?fr":"Délai entre 2 tirs (en msecs)", "min":0, "max":10000, "publish":True, "group": "weapons", "ini":
	[300,300,300,300,1000,500] },
"hitFire" : { "?fr":"Dégats infligés par tir à la victime", "min":0, "max":100, "publish":True, "group": "weapons", "ini":
	[0,10,10,10,0,0] },
"ownerFire" : { "?fr":"Dégats infligés également au tireur", "publish":True, "group": "weapons", "ini":
	[False,False,True,False,False,False] },
"rangeFire" : { "?fr":"Portée de tir de l'arme. 0=infini", "min":0, "max":100, "publish":True, "group": "weapons", "ini":
	[0,5,10,10,1,2] },
"spreadFire" : { "?fr":"Angle d'ouverture en degrés. 360=bombe, 0=laser.", "min":0, "max":360, "publish":True, "group": "weapons", "ini":
	[0,0,360,70,0,180] },
"accelerationFire" : { "?fr":"Amplitude d'acceleration infligée par tir à la victime", "publish":True, "group": "weapons", "ini":
	[0,0,0,0,0,100] },
"ammoIni" : { "?fr":"Nombre de munitions par weapon. 0=infini 🆕Liste par weapon et non plus par agent depuis v205🆕", "min":0, "max":1000, "publish":True, "group": "weapons", "ini": 
	[100,100,1,100,2,1000] },

# 🥷 Règles par profile de joueur
"profiles" : { "?fr":"Liste des profiles d'agents avec stats différentes", "publish":True, "group": "profiles", "ini": 
	["default", "arbitre", "bot1", "bot2", "rocket"] },
"pIcons" : { "?fr":"Icone de chaque profiles de agents", "publish":True, "group": "profiles", "ini": 
	["", "👮‍", "👾","👾", ""] },
"pImgs" : { "?fr":"Url des images de chaque profiles de agents. Chaine vide => dessin cercle", "publish":True, "group": "profiles", "ini": 
	["spaceship1.png", "spaceship1.png", "spaceship1.png", "spaceship1.png", "rocket.svg"] },
"pPnj" : { "?fr":"Classe d'ia pour request auto. IA possibles '':desactiver ia, 'Idle':inoffensive, 'StaticTurret:tourne sur soi-même et tir, 'RandomMovingTurret':déplacement aléatoire et tir, 'SearchNDestroyBehaviour': agents dans metal gear solid 1", "publish":True, "group": "profiles", "ini": 
	["","","Idle","SearchNDestroy", ""] },
"blind" : { "?fr":"Si True, désactive la MAJ de la distance frontale et du dico range par le serveur", "publish":True, "group": "profiles", "ini": 
	[False,False,False,False,True] },
"range" : { "?fr":"Rayon de visibilite. En nb de cases. 0 pour tout voir.", "min":0, "max":10, "publish":True, "group": "profiles", "ini": 
	[6,0,6,6,0] },
"spreadRange" : { "?fr":"Angle champs de vision en degrés. Pris en compte pour range>0 uniquement. 360=lapin, 0=cheval avec oeillère.", "min":0, "max":360, "publish":True, "group": "profiles", "ini": 
	[50,360,50,50,360] },
"pWeapons" : { "?fr":"Type d'armes données au joueur au début du jeu. Indice correspondant dans weapons.", "publish":True, "group": "profiles", "version":205, "ini": 
	[[1],[1],[1],[1],[2]] },
"dtWeapon" : { "?fr":"Temps en ms entre chaque changement possible d'arme du joueur", "min":0, "publish":True, "group": "profiles", "ini": 
	[1,1,1,1,2] },
"weaponIni" : { "?fr":"Type d'arme du joueur début du jeu. Indice correspondant dans weapons. 🆕Remplace 'weapon' depuis v205🆕", "min":0, "publish":True, "group": "profiles", "ini": 
	[1,1,1,1,2] },
"fxFire" : { "?fr":"Si oui ou non fonction tir possible", "publish":True, "group": "profiles", "ini": 
	[True,True,True,True,False] },
"hitCollision" : { "?fr":"Dégats infligés par collision", "min":0, "max":100, "publish":True, "group": "profiles", "ini": 
	[10,0,10,10,0] },
"hitSelfCollision" : { "?fr":"Dégats infligés a soi-même par collision", "min":0, "max":100, "publish":True, "group": "profiles", "ini": 
	[0,0,0,0,10] },
"shieldFire" : { "?fr":"Résitance aux dégats infligés par tir en pourcentage. 0.0=prend 100/100 de dégat, 1.0=prend aucun dégat", "min":0.0, "max":1.0, "publish":True, "group": "profiles", "ini": 
	[0,1,0,0,0] },
"shieldCollision" : { "?fr":"Résistance aux dégats infligés par collision en pourcentage. 0.0=prend 100/100 de dégat, 1.0=prend aucun dégat", "min":0.0, "max":1.0, "publish":True, "group": "profiles", "ini": 
	[0,1,0,0,0] },
"dtDir" : { "?fr":"Délai entre 2 changements d'orientation (en msecs)", "min":0, "max":10000, "publish":True, "group": "profiles", "ini": 
	[10,10,10,10,10] },
"dDirMax" : { "?fr":"Delta angle max entre 2 changements d'orientation (en degrees)", "min":0, "max":90, "publish":True, "group": "profiles", "ini": 
	[10,10,10,10,10] },
"dtMove" : { "?fr":"Délai entre 2 déplacements (en msecs)", "min":0, "max":10000, "publish":True, "group": "profiles", "ini": 
	[50,10,50,50,10] },
"moveToDir" : { "?fr":"Si True, déplacement possible uniquement dans la direction de l'agent, en avant ou en arriere.", "publish":True, "group": "profiles", "ini": 
	[True,True,True,True,False] },
"mass" : { "?fr":"Poids de chaque profile agent", "min":1, "max":10000, "publish":True, "group": "profiles", "ini": 
	[1000,10,1000,1000,1] },
"accelerationOnly" : { "?fr":"Si True, déplacement possible uniquement via acceleration sur player et non via req x,y.", "publish":True, "group": "profiles", "ini": 
	[True,False,True,True,True] },
"accelerationMax" : { "?fr":"Acceleration maximale de l'agent", "min":0, "max":10000, "publish":True, "group": "profiles", "ini": 
	[100,100,100,100,100] },
"forceField" : { "?fr":"Force appliquée autour de l'agent. <0=attraction, >0=repulsion", "min":-1000, "max":1000, "publish":True, "group": "profiles", "version":240, "ini": 
	[30,0,0,0,0] },
"forceFieldRange" : { "?fr":"Rayon autour de l'agent dans lequel forceField s'applique", "min":0, "max":1000, "publish":True, "group": "profiles", "version":240, "ini": 
	[1,0,0,0,0] },
"dxMax" : { "?fr":"Déplacement max autorisé en x", "min":0, "max":100, "publish":True, "group": "profiles", "ini": 
	[1,100,1,1,1] },
"dyMax" : { "?fr":"Déplacement max autorisé en y", "min":0, "max":100, "publish":True, "group": "profiles", "ini": 
	[1,100,1,1,1] },
"energyIni" : { "?fr":"Energie initiale pour faire déplacer l'agent. 0=infini", "min":0, "publish":True, "group": "profiles", "version":241, "ini": 
	[0,0,0,0,0] },
"speedIni" : { "?fr":"Vitesse initiale de l'agent", "min":0, "max":100, "publish":True, "group": "profiles", "ini": 
	[[0,0],[0,0],[0,0],[0,0],[100,100]] },
"speedMax" : { "?fr":"Vitesse maximale de l'agent", "min":0, "max":10000, "publish":True, "group": "profiles", "ini": 
	[100,100,100,100,100] },
"lifeIni" : { "?fr":"Nombre vie par agent", "min":0, "max":100, "publish":True, "group": "profiles", "ini": 
	[100,0,100,100,1] },
"lifeTime" : { "?fr":"Compte a rebour de vie de l'agent en ms. 0 pour pas de limite.", "min":0, "max":100000, "publish":True, "group": "profiles", "ini": 
	[0,0,0,0,3000] },
"invisible": { "?fr":"Si oui ou non invisible", "publish":True, "group": "profiles", "ini": 
	[False,True,False,False,False] },
"invincible": { "?fr":"Si oui ou non invincible", "publish":True, "group": "profiles", "ini": 
	[False,True,False,False,False] },
"canDie": { "?fr":"Si oui ou non le joueur peut se suicider", "publish":True, "group": "profiles", "version":208, "ini": 
	[True,True,True,True,True] },
"infiniteAmmo": { "?fr":"Si oui ou non munitions infinies", "publish":True, "group": "profiles", "ini": 
	[False,True,False,False,False] },
"collision" : { "?fr":"Si oui ou non collision possible avec autres agents", "publish":True, "group": "profiles", "ini": 
	[True,False,True,True,True] },
"collisionMap" : { "?fr":"Si oui ou non collision possible avec la map", "publish":True, "group": "profiles", "ini": 
	[True,False,True,True,True] },
"canRulePlayer": { "?fr":"Si oui ou non les agents peuvent modifier les autres joueurs de l'arène", "publish":True, "group": "profiles", "ini": 
	[False,True,False,False,False] },
"canRuleArena": { "?fr":"Si oui ou non les agents peuvent modifier les règles de l'arène", "publish":True, "group": "profiles", "ini": 
	[False,True,False,False,False] },
"dtRespawn" : { "?fr":"Délai de renaissance apres mort (en msecs)", "min":1000, "max":60000, "publish":True, "group": "profiles", "ini": 
	[5000,5000,5000,5000,0] },
"nRespawn" : { "?fr":"Nb de spawns possibles. 0=infini.", "min":0, "max":10000, "publish":True, "group": "profiles", "ini": 
	[0,0,0,0,1] },
"popOnDeath" : { "?fr":"Pop du serveur quand mort après nRespawn expiré. Attention pop supprime toutes les stats.", "publish":True, "group": "profiles", "ini": 
	[False,False,False,False,True] },
"linkPortNb" : { "?fr":"Nb de ports/liens max. -1=pas de limite, 0=pas de connection possible, >0=nombre de ports possibles", "min":-1, "publish":True, "group": "profiles", "version":241, "ini": 
	[0,0,0,0,0] },
"linkStiffness" : { "?fr":"Précise si la rigidité des connections. >0=longueur du lien tend a revenir à longueur initiale, 0=lien étirable à l'infini sans rappel", "min":0, "max":1000, "publish":True, "group": "profiles", "version":241, "ini": 
	[0,0,0,0,0] },

# 👨‍👩‍👧‍👦 Règles par équipe
"teamName" : { "?fr":"Nom des equipes", "publish":True, "group": "team", "ini":
	["black","blue","pink","red","green","gold","copper","silver"] },
"teamColor" : { "?fr":"Couleur de chaque equipe en code RGB", "publish":True, "group": "team", "ini":
	[[0,0,0],[43,250,250],[255,192,203],[255,0,64],[0,255,128],[255,215,0],[184,115,51],[190,194,203]] },
"teamAreas" : { "?fr":"Zones de naissance des agents par équipe. 🆕Remplace SpawnArea depuis v203🆕", "publish":True, "group": "team", "version": 203, "ini":
	[[1],[2],[3],[4],[5],[6],[7],[8]] },

# ⏯️ Règles pour connaitre et changer l'état du jeu 
"info" : { "?fr":"Message public affiché dans l'arene", "publish":True, "ini":
	"" },
"infoLog" : { "?fr":"True: Active les logs info du jeu dans le header", "publish":True, "ini":
	False },
"pause" : { "?fr":"Mettre le jeu en pause. Attention seul admin peut passer de true à false", "publish":True, "ini":
	False },
"reset" : { "?fr":"Supprime tous les agents et leurs stats et redemarre le jeu", "reset":True, "ini":
	True}
"open" : { "?fr":"Ouvrir ou fermer les portes de l'arène. Une fois fermée, aucun nouveau joueur ne pourra rentrer.", "publish":False, "ini":
	True },
"version" : { "?fr":"Numero de la version du moteur de jeu TactX utilisé par l'arène", "publish":True, "ini":
	206 },

# ⏱️ Règles temps réel 
"countdown" : { "?fr":"Compte a rebours en msecs avant la fin du jeu. 0=pas de compte à rebours ", "min":0, "publish":True, "ini":
	0 },
"dtRestart" : { "?fr":"Delai en secs après fin du jeu à attendre avant redémarrage auto. -1=pas de redémarrage auto, 0=redémarrage auto juste après fin.", "min":-1, "version": 244, "publish":True, "ini":
	0 },


Dans la variable arbiter.range, sont répertoriés en temps réel tous les états de tous les agents du jeu voisins de notre agent (i.e. dans son champs de vision). Parmi ces informations structurées dans un dictionnaire de paires de clé:valeur, nous avons la vie « life », la position « x » « y », la direction « dir », l’équipe « team » … C’est clés s’affichent telles que décrites dans par les valeurs associées aux clés publish dans le dictionnaire ci-dessous qui décrit toutes les variables d’état des agents .

# 🧭 Etats de positionnement du joueur sur la carte
"x": { "?fr":"Abscisse sur la grille", "min":0, "publish":"x", "ini":0 },
"y": { "?fr":"Ordonnee sur la grille", "min":0, "publish":"y", "ini":0 },
"px": { "?fr":"Abscisse sur la grille flottante", "min":0, "publish":"px", "ini":0.0 },
"py": { "?fr":"Ordonnee sur la grille flottante", "min":0, "publish":"py", "ini":0.0 },
"vx": { "?fr":"Vitesse abscisse sur la grille", "publish":"vx", "ini":0.0 },
"vy": { "?fr":"Vitesse ordonnee sur la grille", "publish":"vy", "ini":0.0 },
"dir": { "?fr":"Orientation entière sur la grille", "min":0, "max":3, "publish":"dir", "ini":0 },
"pdir": { "?fr":"Orientation réelle sur la grille", "min":0, "max":3.99, "publish":"pdir", "ini":0.0 },

# 🎲 Etats divers du joueur dans le jeu
"profile": { "?fr":"Id du profile de l'agent", "min":0, "publish":"profile", "ini":0 },
"color": { "?fr":"Couleur RGB de l'agent", "publish":"led", "ini":[0,255,0] },
"team": { "?fr":"Id de l'equipe de l'agent", "min":0, "publish":"team", "ini":0 },
"weapon": { "?fr":"Profile de l'arme utilisée de l'agent", "min":0, "publish":"weapon", "ini":0 },
"distance": { "?fr":"Capteur de distance frontale", "min":0, "publish":"d", "readonly":True, "ini":0 },
"fire": { "?fr":"Si l'agent est en train de tirer", "publish":"fire", "readonly":True, "ini":False },
"life": { "?fr":"Vie de l'agent", "min":0, "max":10000, "publish":"life", "ini":100 },
"energy": { "?fr":"Energie pour faire accelerer l'agent", "min":0, "publish":"energy", "version":241, "ini":100 },
"ammo": { "?fr":"Nom de munitions de l'agent", "min":0, "max":10000, "publish":"ammo", "ini":10 },
"range": { "?fr":"Dico des agents dans le voisinage", "publish":"range", "readonly":True, "ini":{} },
"powerUps": { "?fr":"Dico de powers up. nomRegleProfile:[valeur,duréeEnMs,tDebug]", "publish":"powerUps", "ini":{} },
"info": { "?fr":"Message d'information sur l'agent", "lenmax":32, "publish":"info", "ini":"" },
"lastChecked": { "?fr":"Dernier checkpoint validé", "publish":"lastChecked", "version": 208, "ini":"" },
"checked": { "?fr":"Checkpoints validés", "publish":"checked", "version": 203, "ini":[] },
"checkedAt": { "?fr":"Timestamps des checkpoints validés", "publish":"checkedAt", "version": 203, "ini":[] },
"links": { "?fr":"Connections sur les ports de l'agent. Indice=id port, Valeur=playerId lié", "publish":"links", "version": 241, "ini":[] },


# 🛰️ Requêtes demandées par le joueur avant traitement par le serveur
"reqX": { "?fr":"Abscisse cible sur la grille", "min":0, "publish":"reqX", "ini":0 },
"reqY": { "?fr":"Ordonnee cible sur la grille", "min":0, "publish":"reqY", "ini":0 },
"reqDir": { "?fr":"Direction cible de 0 (droite) à 3 (bas)", "min":0, "max":4, "publish":"reqDir", "ini":0 },
"reqFire": { "?fr":"Ordre de tir", "publish":"reqFire", "ini":False },

# 🏆 Statistiques du joueur dans l'arène
"score": { "?fr":"Score du joueur", "min":0, "publish":"score", "ini":0 },
"rank": { "?fr":"Position du joueur", "min":0, "publish":"rank", "ini":0 },
"dtCreated": { "?fr":"Temps ecoulé en ms depuis création du player", "min":0, "publish":"dtCreated", "ini":0 },
"nFire": { "?fr":"Stats du nombre de tirs effectues", "min":0, "publish":"nFire", "ini":0 },
"nHit": { "?fr":"Stats du nombre de dommages recu par tir sur notre agent", "min":0, "publish":"nHit", "ini":0 },
"nCollision": { "?fr":"Stats du nombre de dommages par collision avec d'autres agents", "min":0, "publish":"nCollision", "ini":0 },
"nMove": { "?fr":"Stats du nombre de deplacements effectues", "min":0, "publish":"nMove", "ini":0 },
"nDeath": { "?fr":"Stats du nombre de fois où l'agent a été tué", "min":0, "publish":"nDeath", "ini":0 },
"nKill": { "?fr":"Stats du nombre de meurtre que l'agent a commis", "min":0, "publish":"nKill", "ini":0 },

🔍 Information
Dans certaines arènes, tous les agents y compris les « non arbitre » peuvent avoir un champs de vision infini. Ils voient tout sur l’arène. En d’autre termes, il n’y a pas de brouillard de guerre. Mais ces règles peuvent être changées par profile d’agent via l’arbitre (expliqué ci-après dans ce tuto) ou via l’interface administrateur d’IDEAL Arena

🦸 Les supers pouvoirs d’arbitre

Grâce à la fonction arbiter.ruleArena(key : str, value : Any), votre arbitre pourra modifier en temps réel les règles de l’arène ci-dessus. Par exemple, si vous souhaitez donner une cap d’invisibilité aux agents de profile attaquant (2e dans la liste valeur de la clé profile, donc 2e egalement pour la liste valeur de la clé invisible), vous pouvez exécuter le code suivant :

arbiter.ruleArena("invisible", [False,True,False,True,False])

🔍 Information
A leurs modifications, certaines règles du jeu entrainent la réinitialisation de l’arène. Il s’agit des règles qui ont pour valeur True pour la clé « reset ». Dans ce cas, veuillez à bien faire les changements de ces règles à l’initialisation de votre arbitre, avant le début du jeu.

La modification de certaines règles auront un impact visible immédiatement dans l’arène comme gridColumns ou gridRows ou fireTeam, tandis que pour d’autres, l’impact sera visible en différé, comme les règles définissant les profiles des joueurs. C’est dernières peuvent prendre effet au prochain spawn de l’agent concerné, comme lifeIni ou ammoIni.

Si vous souhaitez modifier les états des joueurs en temps réel, utilisez la méthode arbiter.rulePlayer(agentId : str, attributeName : str, attributeValue : Any) de l’arbitre. Les attributeName possibles sont les clés listées dans le dictionnaire décrivant les états de tous les agents (voir ci-dessus).

Grâce à cette fonction, vous pourrez par exemple accorder à un agent spécifique des récompenses comme des munitions :

arbiter.rulePlayer("toto", "ammo", 300)

Au prochain actualiser, le joueur nommé « toto » aura 300 munitions sur le serveur du jeu.

📌 Un exemple siouplaît ?

Dans l’exemple ci-dessous, nous créons un arbitre nommé « Architecte », qui sera chargé de créer 2 agents ennemis, « Néo » et « Smith », de comptabiliser leurs scores et de les afficher dans l’arène.

🔍 Information
Pour ceux qui n’ont pas encore vu la trilogie Matrix, l’architecte (présent dans le 2e et 3e volet) est le protagoniste qui construit et administre la Matrice ainsi que tous ses programmes agents, notamment celui de Néo (l’élu subversif capable de renverser la Matrice) et celui de l’agent Smith (un agent surpuissant initialement censé surveiller le comportement des autres programmes).

# Création de l'arbitre
arbiter = pytactx.Agent("Architecte","TheMatrX")
arbiter.ruleArena("info", "⌛ Initialisation de l'arbitre...")
while ( len(arbiter.game) == 0 ):
    arbiter.lookAt((arbiter.dir+1)%4)
    arbiter.update()
    sleep(0.5)

# Création d'agents actualisés par l'arène elle-même
agentsScores = {
    "Neo" : 0,
    "Smith": 0 
}
arbiter.ruleArena("info", "⌛ Création des agents...")
arbiter.update()
for agentId in agentsScores.keys() :
    arbiter.rulePlayer(agentId, "life", 100)

# Boucle principale pour actualiser l'arbitre 
arbiter.ruleArena("info", "🟢 C'est parti !")
while True:
    # Changement d'orientation de l'arbitre pour montrer qu'il est actif dans l'arène
    arbiter.lookAt((arbiter.dir+1)%4)
    arbiter.update()

    # Actualisation des scores des agents
    infoScores = ""
    for agentId in agentsScores.keys():
        if ( agentId in arbiter.range):
            score = arbiter.range[agentId]["nKill"]
            # Ajout de vie et de munitions à chaque kill
            if ( score > agentsScores[agentId] ):
                arbiter.rulePlayer(agentId , "life", arbiter.range[agentId]["life"]+50)
                arbiter.rulePlayer(agentId , "ammo", arbiter.range[agentId]["ammo"]+10)
            agents[agentId] = score
        infoScores += " - 🏆"+agentId +" "+ str(agentsScores[agentId])

    # Affichage du score des 2 bots en temps réel
    arbiter.ruleArena("info", infoScores)

Pour que Néo et Smith soient créés dans l’arène, on utilise la méthode rulePlayer pour mettre leur vie à 100.

Notre arbitre démarre ensuite sa boucle principale, dans laquelle il change son orientation et effectue une mise à jour de son dictionnaire de score par agent qu’il affiche ensuite dans le panneau « info » de l’arène.

Le score ici correspond au nombre de kills effectués par chaque agent. A chaque kill, l’arbitre attribue une récompense de 50 de vie et 10 de munitions à l’agent concerné.

🧬 Des règles comme des gènes pour modifier l’ADN de votre arène

Personnaliser certaines règles de l’arène peut s’avérer assez ardu, car comme les gènes, certaines sont liées entre elles. Pour un comportement voulu (=le phénotype), il faut s’assurer de bien changer un ensemble de règles de manière appropriée.

🏆 Le score et le rank

Vous souhaitez que votre arbitre modifie le score et le rank de chaque joueur visibles dans la vue « 🏆 Rank » du viewer ? Pour cela, il faut

  1. A l’initialisation de votre arbitre, modifier la clé « score » de l’arène et remplacer sa valeur par un texte vide «  »
  2. Dans la boucle principale de votre arbitre, modifier les attributs « score » et « rank » de chaque joueur

🗺️ La carte et ses cases

Vous souhaitez rajouter des cases avec des images et/ou couleurs et/ou comportements différents ?

Par exemple, sur une map de 3 de largeur et 4 de hauteur, on veut rajouter des murs de briques cassables au mileu alignés verticalement, sur lesquels les joueurs ne peuvent pas se déplacer, des sables mouvants (de friction 0.85) au milieu sur lesquels les joueurs sont ralentis …

Pour cela, initialiser les toutes clés « map… » de l’arène de la façon suivantes.
⚠️ Attention à n’en n’oublier aucune sous peine de faire planter le jeu !!

  • Modifier la clé map en mettant aux indices [y][x] le profile de la case souhaitée comme nombre entiers consécutifs. Attention l’indice 0 (=pas d’obstacle) et 1 (obstacle par défaut) sont reservés. Choisissez par exemple 2 pour les murs de briques et 3 pour les sables mouvants
    • avant modification
      • ‘map’ : [
        [0,0,0],
        [0,0,0],
        [0,0,0],
        [0,0,0] ]
    • après modification via arbiter.ruleArena(« map », nouvelleValeur)
      • ‘map’ : [
        [0,2,0],
        [3,2,3],
        [3,2,3],
        [0,2,0] ]
  • Pour chaque profile, il faut s’assurer que les clés mapFriction, mapHit, mapBreakable, mapImgs … ont les mêmes dimensions, correspondant au profile le plus élevé + 1. Rajoutez donc pour chacune de ces clés la valeur correspondante pour le profile de la case associé.
    • avant modification
      • ‘mapImgs’ : [ ‘ ‘, ‘ ‘ ]
      • ‘mapFriction’ : [ 0.0, 1.0 ]
      • ‘mapHit’ : [ 0, 0 ]
      • ‘mapBreakable’ : [ False, False ]
    • après modification via arbiter.ruleArena pour chaque clé/valeur suivante
      • ‘mapImgs’ : [ ‘ ‘, ‘ ‘, ‘https://urlVersImgMur.jpg’, ‘https://urlVersImgSable.jpg’ ]
      • ‘mapFriction » : [ 0.0, 1.0, 1.0, 0.85 ]
      • ‘mapHit’ : [ 0, 0, 0, 0 ]
      • ‘mapBreakable’ : [ False, False, True, False ]

🎭 Les profiles de joueurs

Vous souhaitez créer différentes classes de personnages avec des attributs différents ?

Par exemple

  • des attaquants qui seraient plus rapides avec moins de boucliers, et dotés d’une arme mitraillette laser
  • des défenseurs qui seraient à l’inverse plus résistants aux attaques mais plus lents, et sans arme, uniquement le corps à corps

Pour cela il faut modifier, ou bien créer de nouveaux profiles, en affectant les modifications à toutes les clés profile de joueur.

Le profile 0 est celui utilisé par défaut lors de la création d’un agent quelquonque (= id non inscrit dans la liste de contrôle d’accès de l’arène) à sa connexion dans l’arène. Par défaut, selon l’arène, il peut également y avoir certains profiles associés à des PNJ (Personnages non joueur = IA gérées par l’arène) et d’autres associés et générés par des armes (comme le lance-roquette qui créer un agent de profile roquette à chaque tir).

Dans la plupart des cas, il est donc préférable de créer de nouveaux profiles. Pour cela, il faut ajouter une valeur d’initialisation dans les listes de valeurs associées aux clés de profile de joueur pour un nouveau profile. L’identifiant du profile ajouté sera l’indice des valeurs associées dans ces listes, i.e. la taille de ces listes -1.

En cas de changement de dimension d’une de ces listes, toutes listes de profile de joueur sont par défaut complétées avec la valeur du profile par défaut. Préférez donc initialiser toutes les règles du profile ajouté pour éviter des comportements imprévisibles.

Pour notre exemple d’Attaquants et de Défenseurs :

  • avant modification :
    • ‘profiles’ : [ ‘default’, ‘arbitre’, ‘bot1’, ‘bot2’, ‘rocket’]
    • ‘dtMove’ : [50,10,50,50,10]
    • ‘lifeIni’ [100,0,100,100,1]
    • ‘weapon’ : [1,1,1,1,2]
  • après modification via arbiter.ruleArena pour chaque clé/valeur suivante pour créer le profile attaquant à l’indice 5 et défenseur à l’indice 6 :
    • ‘profiles’ : [ ‘default’, ‘arbitre’, ‘bot1’, ‘bot2’, ‘rocket’, ‘attaquant’, ‘defenseur’ ]
    • ‘dtMove’ : [50,10,50,50,10, 50,100]
    • ‘lifeIni’ [100,0,100,100,1, 100,500]
    • ‘weapon’ : [1,1,1,1,2, 1,0]

⭐ Les power ups

Vous aimeriez accorder des bonus de vies, de munitions ou autres à des joueurs pendant un temps donné ?

Depuis la version 206, cela est possible en créant des zones sur lesquelles les joueurs pourront récupérer des power ups. Pour ajouter une nouvelle zone, il faut ajouter

  1. une valeur par liste de clé de groupe « areas », de sorte à définir le nom de la zone (clé « areas »), l’icône à afficher au milieu de la zone (clé « aIcons »), sa couleur (clé « areasColor »), sa position (clés « areasX » et « areasY ») et ses dimensions (clés « areasW » et « areasH »). Pour la dimension, il est possible de mettre 0 « areasW » (et « areasH ») de sorte à prendre toute la largeur de « areasX » à droite (et respectivement de « areasY » à en bas)
  2. une valeur entière pour la clé « areasPUpsDt », indiquant le délai minimal en millisecondes à attendre entre chaque octroi de power up pour les joueurs dans la zone
  3. une liste de texte pour la clé « areasPUpsEv », indiquant les états (attributs) des joueurs dans la zone, pour lesquels au moins 1 valeur doit changer pour déclencher l’octroi du powerup. Si la liste ajoutée est vide, alors seule condition de temps indiquée par « areasPUpsDt » doit être vérifiée pour attribuer le power up.
  4. un dictionnaire pour la clé « areasPUps », avec
    • Comme clé le nom de l’état (attribut) du joueur qui sera modifié par le power up
    • Une valeur contenant une liste de 3 valeurs
      1. la valeur du power up à attribuer. Attention le type doit être le même que celui de l’état à modifier et peut être positif ou négatif
      2. pendant combien de temps en milliseconds le powerup sera actif, 0 signifiant permanent.
      3. un caractère indiquant l’opérateur pour l’affectation de la valeur à attribuer.
        • ‘+’ incrémentera ou décrémentera la valeur de l’état du joueur selon la valeur indiquée en 1. Le type de l’état doit nécessairement être un nombre.
        • ‘*’ multipliera ou divisera la valeur de l’état du joueur selon la valeur indiquée en 1. Le type de l’état doit nécessairement être un nombre.
        • ‘=’ remplacera la valeur de l’état par celle indiquée en 1. . Le type peut être un texte, un booléen, ou un nombre

Par exemple, si nous souhaitons que sur la carte toute entière, à chaque kill d’un joueur, la vie du tueur soit mise à 100 de manière temporaire pendant 10 secondes, et 10 de munitions de manière permanente, voici les modifications à apporter :

  • avant modification :
    • ‘areas’ : [ ]
    • ‘aIcons’ : [ ]
    • ‘areasColor’ : [ ]
    • ‘areasX’ : [ ]
    • ‘areasY’ : [ ]
    • ‘areasW’ : [ ]
    • ‘areasH’ : [ ]
    • ‘areasPUps’ : [ ]
    • ‘areasPUpsDt’ : [ ]
    • ‘areasPUpsEv’ : [ ]
  • après modification via arbiter.ruleArena pour chaque clé/valeur suivante :
    • ‘areas’ : [ ‘KILL REWARD ZONE’ ]
    • ‘aIcons’ : [ ‘🔫⭐’ ]
    • ‘areasColor’ : [ ‘[0,0,0,0.2]’ ]
    • ‘areasX’ : [ 0 ]
    • ‘areasY’ : [ 0 ]
    • ‘areasW’ : [ 0 ]
    • ‘areasH’ : [ 0 ]
    • ‘areasPUps’ : [ {‘life’:[100,10000,’=’], ‘ammo’:[10,0,’+’]} ]
    • ‘areasPUpsDt’ : [ 500 ]
    • ‘areasPUpsEv’ : [ [‘nKill’] ]

🎮 A vous de jouer !

Avec toutes ces informations, il ne vous restent plus qu’à bien définir vos règles du jeu, puis de vous lancer dans l’implémentation de votre script arbitre.

Pour plus d’aide, vous pouvez vous inspirer des travaux des précédents apprentis jus de liens.

Code of the Hill ⛰️

Combat d’IA en équipe programmée en Python, dans lequel des robots s’affrontent pour prendre possession d’une colline. Inventé par Augustin, Matthias et Yann.

👉 Voir les sources du projet

Capture the Snake 🐍

Combat d’IA en équipe programmée en Python, dans lequel des robots s’affrontent pour capturer le drapeau de leur ennemi. Créé par Michel, Alban, Alexandre et Loïs.

👉 Voir les règles du jeu

🛣️ Et après ?

Vous avez aimé ce tutoriel et vous en voulez d’autres ?

Faites le nous savoir en 👉 donnant vos avis et vos envies et nous nous empresserons de vous en rédigez d’autres 😉

C’est fini pour ce tutoriel ! A vous de jouer maintenant 😉