embedded_astar/lib.rs
1// embedded-astar : Algorithme A* no_std pour systèmes embarqués
2// Copyright (C) 2024 Jorge Andre Castro
3//
4// Ce programme est un logiciel libre ; vous pouvez le redistribuer et/ou
5// le modifier selon les termes de la Licence Publique Générale GNU publiée
6// par la Free Software Foundation ; soit la version 2 de la Licence, soit
7// (à votre choix) toute version ultérieure.
8//
9// Ce programme est distribué dans l'espoir qu'il sera utile, mais SANS
10// AUCUNE GARANTIE ; sans même la garantie implicite de COMMERCIALISABILITÉ
11// ou d'ADÉQUATION À UN USAGE PARTICULIER. Voir la Licence Publique Générale
12// GNU pour plus de détails.
13//
14// Vous devriez avoir reçu une copie de la Licence Publique Générale GNU avec
15// ce programme ; si ce n'est pas le cas, consultez <https://www.gnu.org/licenses/>.
16
17//! # embedded-astar
18//!
19//! Bibliothèque de recherche de chemin A\* `no_std` pour systèmes embarqués.
20//!
21//! Conçue pour les microcontrôleurs et les environnements à ressources limitées,
22//! cette crate fournit une implémentation complète d'A\* **sans allocation sur le tas**,
23//! reposant uniquement sur des structures de taille fixe allouées sur la pile
24//! grâce à [`heapless`].
25//!
26//! ## Fonctionnalités
27//!
28//! - **Compatible `no_std`** — aucun tas, aucun allocateur requis
29//! - **Dimensions configurables** — `LARGEUR`, `HAUTEUR` et `CAPACITE_MAX` fixés à la compilation
30//! - **Heuristique de Manhattan** — optimale pour les grilles à 4 directions
31//! - **Sécurité mémoire** — dépassement de l'open set géré sans panique
32//! - **Zéro code `unsafe`**
33//!
34//! ## Démarrage rapide
35//!
36//! ```rust
37//! use embedded_astar::{trouver_chemin, Point};
38//!
39//! // Grille 8×8 : true = obstacle, false = case libre
40//! // Indexée sous la forme grille[y][x]
41//! let mut grille = [[false; 8]; 8];
42//! grille[1][2] = true; // obstacle en (x=2, y=1)
43//!
44//! let depart = Point::nouveau(0, 0);
45//! let arrivee = Point::nouveau(7, 7);
46//!
47//! // Paramètres de type : <LARGEUR, HAUTEUR, CAPACITE_MAX>
48//! if let Some(chemin) = trouver_chemin::<8, 8, 64>(depart, arrivee, &grille) {
49//! for p in chemin.iter() {
50//! // utiliser p.x, p.y
51//! }
52//! }
53//! ```
54//!
55//! ## Convention de grille
56//!
57//! La grille est indexée sous la forme `grille[y][x]` :
58//! - `true` → obstacle (case infranchissable)
59//! - `false` → case libre (franchissable)
60//!
61//! ## Réglage des capacités
62//!
63//! Trois génériques constants pilotent l'utilisation mémoire :
64//!
65//! | Paramètre | Rôle | Recommandation |
66//! |---------------|----------------------------------|--------------------------|
67//! | `LARGEUR` | Largeur de la grille (axe X) | = largeur de votre carte |
68//! | `HAUTEUR` | Hauteur de la grille (axe Y) | = hauteur de votre carte |
69//! | `CAPACITE_MAX`| Capacité de l'open set et du chemin retourné | ≥ 2 × √(L × H) |
70//!
71//! Une grille 32×32 fonctionne bien avec `CAPACITE_MAX = 128`.
72//! Une grille 64×64 nécessite `CAPACITE_MAX = 256` ou plus.
73
74#![no_std]
75#![deny(missing_docs)]
76#![deny(unsafe_code)]
77
78use heapless::Vec;
79
80// ───────────────────────────────────────────────
81// Types publics
82// ───────────────────────────────────────────────
83
84/// Coordonnée entière 2D sur la grille.
85#[derive(Clone, Copy, PartialEq, Eq, Debug)]
86pub struct Point {
87 /// Position horizontale (indice de colonne).
88 pub x: i32,
89 /// Position verticale (indice de ligne).
90 pub y: i32,
91}
92
93impl Point {
94 /// Crée un nouveau [`Point`] à partir de ses coordonnées.
95 #[inline]
96 pub const fn nouveau(x: i32, y: i32) -> Self {
97 Self { x, y }
98 }
99}
100
101// ───────────────────────────────────────────────
102// Nœud interne
103// ───────────────────────────────────────────────
104
105#[derive(Clone, Copy)]
106struct Noeud {
107 point: Point,
108 /// Coût réel depuis le départ jusqu'à ce nœud.
109 cout_g: i32,
110 /// Coût estimé total : cout_g + heuristique vers l'arrivée.
111 cout_f: i32,
112}
113
114// ───────────────────────────────────────────────
115// Heuristique
116// ───────────────────────────────────────────────
117
118/// Distance de Manhattan : heuristique admissible pour les grilles à 4 directions.
119///
120/// Pour un déplacement diagonal, remplacer par la distance de Chebyshev.
121#[inline]
122fn heuristique(a: Point, b: Point) -> i32 {
123 (a.x - b.x).abs() + (a.y - b.y).abs()
124}
125
126// ───────────────────────────────────────────────
127// Vérification de validité
128// ───────────────────────────────────────────────
129
130/// Retourne `true` si le point est dans les bornes et n'est pas un obstacle.
131#[inline]
132fn est_valide<const L: usize, const H: usize>(
133 p: Point,
134 grille: &[[bool; L]; H],
135) -> bool {
136 p.x >= 0
137 && (p.x as usize) < L
138 && p.y >= 0
139 && (p.y as usize) < H
140 && !grille[p.y as usize][p.x as usize]
141}
142
143// ───────────────────────────────────────────────
144// API publique
145// ───────────────────────────────────────────────
146
147/// Exécute l'algorithme A\* sur une grille booléenne d'obstacles.
148///
149/// # Paramètres de type
150///
151/// - `LARGEUR` — nombre de colonnes de la grille
152/// - `HAUTEUR` — nombre de lignes de la grille
153/// - `CAPACITE_MAX` — capacité maximale de l'open set **et** du chemin retourné.
154/// Doit être suffisamment grand pour contenir le chemin le plus long attendu.
155///
156/// # Arguments
157///
158/// - `depart` — case de départ (doit être libre et dans les bornes)
159/// - `arrivee` — case d'arrivée (doit être libre et dans les bornes)
160/// - `grille` — tableau booléen 2D indexé en `grille[y][x]`.
161/// `true` = obstacle, `false` = case franchissable.
162///
163/// # Valeur de retour
164///
165/// `Some(chemin)`: un [`heapless::Vec`] de [`Point`]s ordonné du départ (inclus)
166/// jusqu'à l'arrivée (incluse), représentant le chemin optimal.
167///
168/// `None` : aucun chemin n'existe, un point de terminaison est un obstacle ou
169/// hors bornes, ou le chemin dépasse la capacité `CAPACITE_MAX`.
170///
171/// # Exemple
172///
173/// ```rust
174/// use embedded_astar::{trouver_chemin, Point};
175///
176/// let grille = [[false; 4]; 4]; // grille 4×4 vide
177/// let chemin = trouver_chemin::<4, 4, 16>(
178/// Point::nouveau(0, 0),
179/// Point::nouveau(3, 3),
180/// &grille,
181/// )
182/// .expect("un chemin doit exister sur une grille vide");
183///
184/// assert_eq!(*chemin.first().unwrap(), Point::nouveau(0, 0));
185/// assert_eq!(*chemin.last().unwrap(), Point::nouveau(3, 3));
186/// ```
187pub fn trouver_chemin<const LARGEUR: usize, const HAUTEUR: usize, const CAPACITE_MAX: usize>(
188 depart: Point,
189 arrivee: Point,
190 grille: &[[bool; LARGEUR]; HAUTEUR],
191) -> Option<Vec<Point, CAPACITE_MAX>> {
192 // Rejeter immédiatement les points invalides ou bloqués.
193 if !est_valide::<LARGEUR, HAUTEUR>(depart, grille)
194 || !est_valide::<LARGEUR, HAUTEUR>(arrivee, grille)
195 {
196 return None;
197 }
198
199 // ── Structures de données ─────────────────────────────────────────────────
200
201 // Open set : nœuds en attente d'évaluation.
202 let mut ouvert: Vec<Noeud, CAPACITE_MAX> = Vec::new();
203
204 // Ensemble fermé : cases déjà entièrement évaluées (évite les ré-expansions).
205 let mut ferme: [[bool; LARGEUR]; HAUTEUR] = [[false; LARGEUR]; HAUTEUR];
206
207 // Carte des parents : pour chaque case, quelle case l'a atteinte.
208 // Utilisée pour reconstruire le chemin en fin d'algorithme.
209 let mut parents: [[Option<Point>; LARGEUR]; HAUTEUR] = [[None; LARGEUR]; HAUTEUR];
210
211 // ── Initialisation ────────────────────────────────────────────────────────
212
213 let noeud_depart = Noeud {
214 point: depart,
215 cout_g: 0,
216 cout_f: heuristique(depart, arrivee),
217 };
218 // L'open set est vide : le push ne peut pas échouer ici.
219 let _ = ouvert.push(noeud_depart);
220
221 // Directions : Haut, Droite, Bas, Gauche (4-directionnel).
222 // Pour activer les diagonales, ajouter (±1, ±1) et utiliser la distance de Chebyshev.
223 const DIRECTIONS: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
224
225 // ── Boucle principale ─────────────────────────────────────────────────────
226
227 while !ouvert.is_empty() {
228 // Sélectionner le nœud au cout_f le plus faible via scan linéaire.
229 // Adapté aux petits open sets typiques des grilles embarquées.
230 // Pour CAPACITE_MAX > 256, envisager un tas min.
231 let mut idx_meilleur = 0;
232 for i in 1..ouvert.len() {
233 if ouvert[i].cout_f < ouvert[idx_meilleur].cout_f {
234 idx_meilleur = i;
235 }
236 }
237
238 // Extraire le meilleur nœud en O(1) via swap_remove.
239 let courant = ouvert.swap_remove(idx_meilleur);
240 let pc = courant.point;
241
242 // ── Arrivée atteinte ──────────────────────────────────────────────────
243
244 if pc == arrivee {
245 return reconstruire_chemin::<LARGEUR, HAUTEUR, CAPACITE_MAX>(
246 arrivee,
247 depart,
248 &parents,
249 );
250 }
251
252 // Marquer la case comme visitée.
253 ferme[pc.y as usize][pc.x as usize] = true;
254
255 // ── Expansion des voisins ─────────────────────────────────────────────
256
257 for &(dx, dy) in &DIRECTIONS {
258 let voisin = Point {
259 x: pc.x + dx,
260 y: pc.y + dy,
261 };
262
263 // Ignorer les cases hors bornes, les obstacles et les cases déjà évaluées.
264 if !est_valide::<LARGEUR, HAUTEUR>(voisin, grille)
265 || ferme[voisin.y as usize][voisin.x as usize]
266 {
267 continue;
268 }
269
270 let g_tentative = courant.cout_g + 1; // coût uniforme : 1 par déplacement
271
272 // Chercher si ce voisin est déjà dans l'open set.
273 let existant = ouvert.iter_mut().find(|n| n.point == voisin);
274
275 match existant {
276 Some(noeud) if g_tentative < noeud.cout_g => {
277 // Meilleur chemin trouvé vers un voisin déjà en file : mise à jour.
278 noeud.cout_g = g_tentative;
279 noeud.cout_f = g_tentative + heuristique(voisin, arrivee);
280 parents[voisin.y as usize][voisin.x as usize] = Some(pc);
281 }
282 None => {
283 // Première découverte de ce voisin.
284 parents[voisin.y as usize][voisin.x as usize] = Some(pc);
285 let noeud_voisin = Noeud {
286 point: voisin,
287 cout_g: g_tentative,
288 cout_f: g_tentative + heuristique(voisin, arrivee),
289 };
290 // Si l'open set est plein, ignorer ce voisin sans paniquer.
291 let _ = ouvert.push(noeud_voisin);
292 }
293 _ => { /* le nœud existant a déjà un cout_g égal ou meilleur */ }
294 }
295 }
296 }
297
298 None // Tous les nœuds accessibles ont été explorés sans atteindre l'arrivée.
299}
300
301// ───────────────────────────────────────────────
302// Reconstruction du chemin (interne)
303// ───────────────────────────────────────────────
304
305fn reconstruire_chemin<const L: usize, const H: usize, const CAP: usize>(
306 arrivee: Point,
307 depart: Point,
308 parents: &[[Option<Point>; L]; H],
309) -> Option<Vec<Point, CAP>> {
310 let mut chemin: Vec<Point, CAP> = Vec::new();
311 let mut courant = arrivee;
312
313 // Remonter de l'arrivée vers le départ via la carte des parents.
314 loop {
315 if chemin.push(courant).is_err() {
316 // Le chemin dépasse la capacité allouée : signaler l'échec proprement.
317 return None;
318 }
319 if courant == depart {
320 break;
321 }
322 match parents[courant.y as usize][courant.x as usize] {
323 Some(p) => courant = p,
324 None => return None, // Déconnexion : ne devrait pas survenir en usage normal.
325 }
326 }
327
328 chemin.reverse();
329 Some(chemin)
330}
331
332// ───────────────────────────────────────────────
333// Tests
334// ───────────────────────────────────────────────
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 type Grille4 = [[bool; 4]; 4];
341
342 fn grille_vide() -> Grille4 {
343 [[false; 4]; 4]
344 }
345
346 #[test]
347 fn depart_egal_arrivee() {
348 let grille = grille_vide();
349 let p = Point::nouveau(1, 1);
350 let chemin = trouver_chemin::<4, 4, 16>(p, p, &grille).unwrap();
351 assert_eq!(chemin.len(), 1);
352 assert_eq!(chemin[0], p);
353 }
354
355 #[test]
356 fn deplacement_horizontal() {
357 let grille = grille_vide();
358 let chemin = trouver_chemin::<4, 4, 16>(
359 Point::nouveau(0, 0),
360 Point::nouveau(3, 0),
361 &grille,
362 )
363 .unwrap();
364 assert_eq!(chemin.first().copied(), Some(Point::nouveau(0, 0)));
365 assert_eq!(chemin.last().copied(), Some(Point::nouveau(3, 0)));
366 assert_eq!(chemin.len(), 4); // (0,0)→(1,0)→(2,0)→(3,0)
367 }
368
369 #[test]
370 fn deplacement_vertical() {
371 let grille = grille_vide();
372 let chemin = trouver_chemin::<4, 4, 16>(
373 Point::nouveau(0, 0),
374 Point::nouveau(0, 3),
375 &grille,
376 )
377 .unwrap();
378 assert_eq!(chemin.len(), 4);
379 }
380
381 #[test]
382 fn chemin_diagonal() {
383 let grille = grille_vide();
384 let chemin = trouver_chemin::<4, 4, 16>(
385 Point::nouveau(0, 0),
386 Point::nouveau(3, 3),
387 &grille,
388 )
389 .unwrap();
390 assert_eq!(chemin.first().copied(), Some(Point::nouveau(0, 0)));
391 assert_eq!(chemin.last().copied(), Some(Point::nouveau(3, 3)));
392 // Optimal Manhattan = 6 déplacements → 7 points
393 assert_eq!(chemin.len(), 7);
394 }
395
396 #[test]
397 fn mur_avec_passage() {
398 // Grille :
399 // . . . .
400 // # # . # # = obstacle
401 // . . . .
402 // . . . .
403 // Passage en (x=2, y=1) ; le chemin doit y passer.
404 let mut grille = grille_vide();
405 grille[1][0] = true;
406 grille[1][1] = true;
407 grille[1][3] = true;
408
409 let chemin = trouver_chemin::<4, 4, 16>(
410 Point::nouveau(0, 0),
411 Point::nouveau(0, 2),
412 &grille,
413 )
414 .unwrap();
415 assert_eq!(chemin.first().copied(), Some(Point::nouveau(0, 0)));
416 assert_eq!(chemin.last().copied(), Some(Point::nouveau(0, 2)));
417 }
418
419 #[test]
420 fn aucun_chemin_mur_total() {
421 let mut grille = grille_vide();
422 // Ligne 1 entièrement bloquée aucun passage possible.
423 grille[1][0] = true;
424 grille[1][1] = true;
425 grille[1][2] = true;
426 grille[1][3] = true;
427
428 let resultat = trouver_chemin::<4, 4, 16>(
429 Point::nouveau(0, 0),
430 Point::nouveau(0, 2),
431 &grille,
432 );
433 assert!(resultat.is_none());
434 }
435
436 #[test]
437 fn depart_obstacle() {
438 let mut grille = grille_vide();
439 grille[0][0] = true;
440 let resultat = trouver_chemin::<4, 4, 16>(
441 Point::nouveau(0, 0),
442 Point::nouveau(3, 3),
443 &grille,
444 );
445 assert!(resultat.is_none());
446 }
447
448 #[test]
449 fn arrivee_obstacle() {
450 let mut grille = grille_vide();
451 grille[3][3] = true;
452 let resultat = trouver_chemin::<4, 4, 16>(
453 Point::nouveau(0, 0),
454 Point::nouveau(3, 3),
455 &grille,
456 );
457 assert!(resultat.is_none());
458 }
459
460 #[test]
461 fn hors_bornes() {
462 let grille = grille_vide();
463 let resultat = trouver_chemin::<4, 4, 16>(
464 Point::nouveau(0, 0),
465 Point::nouveau(99, 99),
466 &grille,
467 );
468 assert!(resultat.is_none());
469 }
470}