Skip to main content

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}