Stratégie (FSM)¶
Le cerveau de l'IA est une machine à états finis (FSM) : à chaque tour de boucle, l'état courant produit une (ou plusieurs) commande(s), et la FSM décide s'il faut basculer vers un autre état. La survie prime toujours sur tout le reste.
Cette page décrit le contrôleur (ai/src/strategy/fsm.py), la convention de
commande multi-étapes « pointée », les mises à jour locales optimistes, puis
le comportement réel de chaque état. Elle couvre enfin les deux modules de
support : incantation.py (prérequis) et pathfinding.py (géométrie torique).
Le contrôleur FSM¶
À la construction, la FSM instancie immédiatement les 7 états et leur donne
une référence retour vers elle-même (ai/src/strategy/fsm.py:47) :
self._states: dict[str, State] = {
"survive": SurviveState(drone, client),
"rally": RallyState(drone, client),
"collect": CollectState(drone, client),
"explore": ExploreState(drone, client),
"fork": ForkState(drone, client),
"incant_prep": IncantPrepState(drone, client),
"incant": IncantState(drone, client),
}
L'état initial est survive : main.run() appelle fsm.transition_to("survive")
juste après la création du drone (ai/src/main.py:125).
transition_to et le cas spécial explore¶
transition_to(name) (ai/src/strategy/fsm.py:62) valide le nom, journalise la
transition, puis appelle on_enter() du nouvel état. Il traite explore de
façon particulière : avant d'y entrer, il déduit quelle ressource chercher
et vers quel état revenir via _resource_for_return()
(ai/src/strategy/fsm.py:84), puis configure l'instance ExploreState par
set_target(resource, return_state) :
- état courant
survive→ chercher"food", revenir àsurvive; - état courant
collect→ chercher la pierre cible, revenir àcollect; - sinon →
("food", "survive")par défaut.
C'est nécessaire parce que ExploreState est un état paramétrable réutilisé
par plusieurs demandeurs : il ne sait pas seul ce qu'il cherche.
decide_next_command : le moteur central¶
decide_next_command() (ai/src/strategy/fsm.py:101) est appelé en boucle tant
que la file de commandes peut accepter des envois. Son déroulé :
- demander à l'état courant
should_transition(); si un nom est renvoyé, transiter immédiatement (ai/src/strategy/fsm.py:105-108) ; - demander la prochaine commande via
next_command(). Si elle est vide, retournerFalse(rien à envoyer ce tour-ci) ; - découper la commande sur le point
.et envoyer chaque sous-commande.
flowchart TD
A[decide_next_command] --> B{should_transition ?}
B -- oui --> C[transition_to]
B -- non --> D[next_command]
C --> D
D --> E{commande vide ?}
E -- oui --> F[return False]
E -- non --> G["split sur '.'"]
G --> H[Pour chaque sous-commande :<br/>MAJ optimiste + send_command]
H --> I[return True]
La convention de commande « pointée »¶
Un état ne renvoie jamais qu'une seule action atomique : il peut renvoyer une
séquence pointée "cmd1.cmd2.cmd3". Par exemple, pour partir vers l'Est
quand on regarde au Nord, RallyState renvoie Right.Forward
(ai/src/strategy/states/rally.py:184). La FSM fait
command.split(".") (ai/src/strategy/fsm.py:115) et envoie chaque morceau
comme une commande serveur distincte.
Pourquoi enchaîner plusieurs commandes ?
Le serveur tolère jusqu'à 10 requêtes en vol (voir
Réseau & file de commandes). Émettre Right.Forward d'un seul
coup remplit le pipeline et évite un aller-retour de boucle par micro-action,
ce qui accélère la réactivité du drone.
Les mises à jour locales optimistes¶
Avant que le serveur ne confirme quoi que ce soit, la FSM anticipe l'effet
de chaque sous-commande sur l'état local du drone
(ai/src/strategy/fsm.py:121-167). C'est le match subcommand :
| Sous-commande | Effet optimiste appliqué tout de suite |
|---|---|
Right |
rotation horaire de drone.direction dans ORIENTATIONS = ["N","E","S","W"] |
Left |
rotation antihoraire de drone.direction |
Forward |
avance drone.pos sur le tore (E → x+1, W → x−1, S → y+1, N → y−1), avec modulo largeur/hauteur |
Set <r> |
décrémente inventory[r] (borné à 0) et dépose la ressource sur la carte mentale à la position courante |
Pourquoi anticiper ?
Le drone doit décider la commande suivante (par ex. un Look) avec la
bonne position/direction, sans attendre l'aller-retour réseau. L'état local
sert de modèle prédictif ; la réponse serveur reste la source de vérité
et corrige toute divergence. C'est cohérent avec le principe « le serveur est
autoritaire » (voir Vue d'ensemble).
Le callback par commande : ré-injection de pos/direction¶
Pour chaque sous-commande, la FSM enregistre un callback capturant un
instantané de la position et de la direction au moment de l'envoi
(ai/src/strategy/fsm.py:169-182) :
def make_callback(pos, direction):
def callback(line):
msg = parse_server_line(line, self._client)
if isinstance(msg, LookResult):
msg = LookResult(tiles=msg.tiles, pos=pos, direction=direction)
if self._state:
self._state.on_response(msg)
return callback
Quand la réponse arrive, le callback re-parse la ligne brute puis, s'il
s'agit d'un LookResult, y réinjecte la pos/direction capturées au moment
de l'émission du Look. C'est indispensable : entre l'envoi du Look et sa
réponse, plusieurs Forward/Right optimistes peuvent avoir déplacé drone.pos.
Sans cet instantané, la vision serait intégrée à la carte au mauvais endroit.
Cas Incantation et rafraîchissement périodique¶
- Après une
Incantation, la FSM ajoute un callback supplémentaire viaadd_callback(ai/src/strategy/fsm.py:184-190) pour traiter le message non sollicité de suivi (Current level: k). - Toutes les
INVENTORY_UPDATE_TU(= 10,ai/src/config.py:98) sous-commandes, unInventoryest injecté automatiquement (ai/src/strategy/fsm.py:192-201) pour resynchroniser le stock defoodet les pierres avant qu'une ressource ne réapparaisse.
Les états, un par un¶
survive — manger avant tout¶
SurviveState (ai/src/strategy/states/survive.py) est l'état par défaut et la
priorité absolue. Il cherche la food connue la plus proche (carte mentale +
vision via nearest_food), Take food quand il est dessus, et délègue le
déplacement à rally en lui fixant une destination
(ai/src/strategy/states/survive.py:154-159). Si aucune food n'est connue, il
bascule en explore. La transition de sortie (should_transition,
ai/src/strategy/states/survive.py:138) vise collect une fois nourri.
Probable bug d'unité dans le seuil de food
should_transition compare drone.food (un compte d'unités) à
FOOD_SURVIVAL_THRESHOLD * LIFE_PER_FOOD (= 5 × 126 = 630, des unités de
temps). Le drone n'ayant jamais 630 unités de food, la sortie de
survive n'est en pratique jamais atteinte. Le même mélange compte/temps se
retrouve dans collect, incant_prep et incant. Voir
Limitations connues.
rally — le moteur de déplacement réutilisable¶
RallyState (ai/src/strategy/states/rally.py) est la brique de navigation
générique. Un état demandeur l'arme par set_destination(target, return_state,
on_arrived, take_resource) puis demande la transition rally. À chaque tour,
il calcule le déplacement signé optimal avec toric_signed_delta
(ai/src/strategy/states/rally.py:137), puis _navigate(dx, dy)
(ai/src/strategy/states/rally.py:170) traduit ce delta + la direction courante
en une séquence pointée de Left/Right/Forward. Arrivé sur la cible, il émet
Take + Inventory (si take_resource est fourni), invoque on_arrived, puis
transite vers return_state.
Priorité aux axes
_navigate traite d'abord l'axe X (Est/Ouest), puis l'axe Y (Nord/Sud) :
le drone s'aligne horizontalement avant de monter/descendre. Simple et
déterministe, mais non optimal en nombre de virages.
collect — viser une pierre manquante¶
CollectState (ai/src/strategy/states/collect.py) calcule les pierres
manquantes pour la prochaine incantation (missing_stones), choisit une
ressource cible, cherche sa position connue la plus proche et délègue à
rally. Si la pierre est inconnue → explore ; s'il ne manque plus rien →
incant_prep ; si la food chute → survive.
Logs de débogage résiduels
collect.py contient des print() oubliés
(ai/src/strategy/states/collect.py:50 et :95), de même que
incant_prep.py:77. Voir Limitations connues.
explore — spirale carrée cartographiante¶
ExploreState (ai/src/strategy/states/explore.py) parcourt le monde en
spirale carrée : des tronçons de Forward, un Right en fin de tronçon, et
la longueur du tronçon augmente tous les 2 virages
(ai/src/strategy/states/explore.py:122-130). Un Look est intercalé tous les
_LOOK_EVERY = 3 pas (ai/src/strategy/states/explore.py:22), intégré à la
carte mentale. Dès que la ressource cible devient connue, l'état retourne vers
_return_state (should_transition, ai/src/strategy/states/explore.py:132).
incant_prep — préparer la tuile de rituel¶
IncantPrepState (ai/src/strategy/states/incant_prep.py) fixe à l'entrée les
pierres requises (requirements_for), puis next_command dépose (Set) les
pierres manquantes une à une tant qu'il en a en inventaire, sinon il émet un
Look de vérification (ai/src/strategy/states/incant_prep.py:56-64). Il passe
à incant dès que can_incant(...) est satisfait. Implémentation volontairement
simple (pas de coordination multi-joueurs ici).
incant — déclencher l'incantation¶
IncantState (ai/src/strategy/states/incant.py) envoie Incantation une
seule fois (drapeau incant_send, ai/src/strategy/states/incant.py:57-59),
attend les messages serveur (Elevation underway puis Current level: k ou
ko), met à jour drone.level au succès, puis transite : explore au succès,
incant_prep à l'échec.
Pas de coordination multi-joueurs
incant envoie l'incantation sans élection de meneur ni synchronisation du
nombre de joueurs présents sur la tuile. Voir
Limitations connues.
fork — un stub complet (jamais atteint)¶
ForkState (ai/src/strategy/states/fork.py) est un stub intégral : ses
quatre méthodes lèvent NotImplementedError
(ai/src/strategy/states/fork.py:27,33,37,41). Il est enregistré dans la FSM
mais aucune transition n'y mène ; y entrer ferait planter le drone. La
reproduction (Fork / Connect_nbr) n'est donc pas implémentée.
Reproduction non implémentée
ForkState est inerte : aucun état n'appelle transition_to("fork"). Voir
Limitations connues.
Diagramme d'états (transitions implémentées)¶
stateDiagram-v2
[*] --> survive
survive --> explore: aucune food connue
survive --> rally: food repérée (s'y rendre)
survive --> collect: nourri (seuil, cf. bug d'unité)
collect --> rally: pierre repérée
collect --> explore: pierre inconnue
collect --> incant_prep: rien ne manque
collect --> survive: food basse
explore --> survive: food trouvée
explore --> collect: pierre trouvée
rally --> survive: arrivée (retour demandeur)
rally --> collect: arrivée (retour demandeur)
incant_prep --> incant: conditions réunies
incant_prep --> survive: food basse
incant --> explore: succès
incant --> incant_prep: échec
incant --> survive: food basse
fork: fork (STUB inatteignable)
rally est un état de transit
rally n'est jamais une destination finale : il revient toujours vers le
return_state que lui a passé le demandeur (survive ou collect).
Module incantation.py — prérequis¶
ai/src/strategy/incantation.py ne contient que des fonctions pures sur la
table INCANTATION_REQUIREMENTS (ai/src/config.py:73) :
requirements_for(level)→(nb_joueurs, {pierre: quantité})(ai/src/strategy/incantation.py:15) ;can_incant(tile_objects, players_on_tile, level)→ vérifie joueurs et pierres au sol (ai/src/strategy/incantation.py:19) ;missing_stones(inventory_on_tile, level)→ pierres encore manquantes (ai/src/strategy/incantation.py:34).
Niveau 8 non couvert
La table s'arrête au niveau 7 ; requirements_for(8) lève KeyError. Aucun
garde-fou n'empêche cet appel. Voir Limitations connues.
Module pathfinding.py — géométrie torique¶
ai/src/strategy/pathfinding.py, fonctions pures sur un monde torique :
toric_distance(p1, p2, w, h)(ai/src/strategy/pathfinding.py:14) renvoie un tuple(min(dx, w−dx), min(dy, h−dy)), pas un scalaire. Utilisé pour comparer des distances (nearest_food/nearest_resource), jamais pour décider d'un sens.toric_signed_delta(p1, p2, w, h)(ai/src/strategy/pathfinding.py:31) est la vraie primitive de navigation : il renvoie le déplacement signé le plus court par axe (dx>0 = Est, dy>0 = Sud). C'est lui qu'utiliseRallyState.
Code implémenté mais inutilisé
bfs_toric (BFS torique 4-voisins, ai/src/strategy/pathfinding.py:73) et
direction_from_sound (ai/src/strategy/pathfinding.py:121) sont
pleinement implémentés mais n'ont aucun appelant. La navigation réelle se
fait par toric_signed_delta, et l'écoute des broadcasts (qui nourrirait
direction_from_sound) n'est pas branchée. Voir
Limitations connues.
Pour aller plus loin¶
- Boucle de décision — comment la FSM s'imbrique dans la boucle réseau bout-en-bout.
- État interne —
DroneState, carte torique, vision. - Coordination d'équipe — couche chiffrée inter-drones (non branchée).