embedded-astar 0.1.0

Bibliothèque de recherche de chemin A* no_std pour systèmes embarqués — zéro allocation, dimensions const-génériques.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
// embedded-astar : Algorithme A* no_std pour systèmes embarqués
// Copyright (C) 2024  Jorge Andre Castro
//
// Ce programme est un logiciel libre ; vous pouvez le redistribuer et/ou
// le modifier selon les termes de la Licence Publique Générale GNU publiée
// par la Free Software Foundation ; soit la version 2 de la Licence, soit
// (à votre choix) toute version ultérieure.
//
// Ce programme est distribué dans l'espoir qu'il sera utile, mais SANS
// AUCUNE GARANTIE ; sans même la garantie implicite de COMMERCIALISABILITÉ
// ou d'ADÉQUATION À UN USAGE PARTICULIER. Voir la Licence Publique Générale
// GNU pour plus de détails.
//
// Vous devriez avoir reçu une copie de la Licence Publique Générale GNU avec
// ce programme ; si ce n'est pas le cas, consultez <https://www.gnu.org/licenses/>.

//! # embedded-astar
//!
//! Bibliothèque de recherche de chemin A\* `no_std` pour systèmes embarqués.
//!
//! Conçue pour les microcontrôleurs et les environnements à ressources limitées,
//! cette crate fournit une implémentation complète d'A\* **sans allocation sur le tas**,
//! reposant uniquement sur des structures de taille fixe allouées sur la pile
//! grâce à [`heapless`].
//!
//! ## Fonctionnalités
//!
//! - **Compatible `no_std`** — aucun tas, aucun allocateur requis
//! - **Dimensions configurables** — `LARGEUR`, `HAUTEUR` et `CAPACITE_MAX` fixés à la compilation
//! - **Heuristique de Manhattan** — optimale pour les grilles à 4 directions
//! - **Sécurité mémoire** — dépassement de l'open set géré sans panique
//! - **Zéro code `unsafe`**
//!
//! ## Démarrage rapide
//!
//! ```rust
//! use embedded_astar::{trouver_chemin, Point};
//!
//! // Grille 8×8 : true = obstacle, false = case libre
//! // Indexée sous la forme grille[y][x]
//! let mut grille = [[false; 8]; 8];
//! grille[1][2] = true; // obstacle en (x=2, y=1)
//!
//! let depart  = Point::nouveau(0, 0);
//! let arrivee = Point::nouveau(7, 7);
//!
//! // Paramètres de type : <LARGEUR, HAUTEUR, CAPACITE_MAX>
//! if let Some(chemin) = trouver_chemin::<8, 8, 64>(depart, arrivee, &grille) {
//!     for p in chemin.iter() {
//!         // utiliser p.x, p.y
//!     }
//! }
//! ```
//!
//! ## Convention de grille
//!
//! La grille est indexée sous la forme `grille[y][x]` :
//! - `true`  → obstacle (case infranchissable)
//! - `false` → case libre (franchissable)
//!
//! ## Réglage des capacités
//!
//! Trois génériques constants pilotent l'utilisation mémoire :
//!
//! | Paramètre     | Rôle                             | Recommandation           |
//! |---------------|----------------------------------|--------------------------|
//! | `LARGEUR`     | Largeur de la grille (axe X)     | = largeur de votre carte |
//! | `HAUTEUR`     | Hauteur de la grille (axe Y)     | = hauteur de votre carte |
//! | `CAPACITE_MAX`| Capacité de l'open set et du chemin retourné | ≥ 2 × √(L × H) |
//!
//! Une grille 32×32 fonctionne bien avec `CAPACITE_MAX = 128`.
//! Une grille 64×64 nécessite `CAPACITE_MAX = 256` ou plus.

#![no_std]
#![deny(missing_docs)]
#![deny(unsafe_code)]

use heapless::Vec;

// ───────────────────────────────────────────────
//  Types publics
// ───────────────────────────────────────────────

/// Coordonnée entière 2D sur la grille.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Point {
    /// Position horizontale (indice de colonne).
    pub x: i32,
    /// Position verticale (indice de ligne).
    pub y: i32,
}

impl Point {
    /// Crée un nouveau [`Point`] à partir de ses coordonnées.
    #[inline]
    pub const fn nouveau(x: i32, y: i32) -> Self {
        Self { x, y }
    }
}

// ───────────────────────────────────────────────
//  Nœud interne
// ───────────────────────────────────────────────

#[derive(Clone, Copy)]
struct Noeud {
    point: Point,
    /// Coût réel depuis le départ jusqu'à ce nœud.
    cout_g: i32,
    /// Coût estimé total : cout_g + heuristique vers l'arrivée.
    cout_f: i32,
}

// ───────────────────────────────────────────────
//  Heuristique
// ───────────────────────────────────────────────

/// Distance de Manhattan : heuristique admissible pour les grilles à 4 directions.
///
/// Pour un déplacement diagonal, remplacer par la distance de Chebyshev.
#[inline]
fn heuristique(a: Point, b: Point) -> i32 {
    (a.x - b.x).abs() + (a.y - b.y).abs()
}

// ───────────────────────────────────────────────
//  Vérification de validité
// ───────────────────────────────────────────────

/// Retourne `true` si le point est dans les bornes et n'est pas un obstacle.
#[inline]
fn est_valide<const L: usize, const H: usize>(
    p: Point,
    grille: &[[bool; L]; H],
) -> bool {
    p.x >= 0
        && (p.x as usize) < L
        && p.y >= 0
        && (p.y as usize) < H
        && !grille[p.y as usize][p.x as usize]
}

// ───────────────────────────────────────────────
//  API publique
// ───────────────────────────────────────────────

/// Exécute l'algorithme A\* sur une grille booléenne d'obstacles.
///
/// # Paramètres de type
///
/// - `LARGEUR`      — nombre de colonnes de la grille
/// - `HAUTEUR`      — nombre de lignes de la grille
/// - `CAPACITE_MAX` — capacité maximale de l'open set **et** du chemin retourné.
///   Doit être suffisamment grand pour contenir le chemin le plus long attendu.
///
/// # Arguments
///
/// - `depart`  — case de départ (doit être libre et dans les bornes)
/// - `arrivee` — case d'arrivée (doit être libre et dans les bornes)
/// - `grille`  — tableau booléen 2D indexé en `grille[y][x]`.
///   `true` = obstacle, `false` = case franchissable.
///
/// # Valeur de retour
///
/// `Some(chemin)`: un [`heapless::Vec`] de [`Point`]s ordonné du départ (inclus)
/// jusqu'à l'arrivée (incluse), représentant le chemin optimal.
///
/// `None` : aucun chemin n'existe, un point de terminaison est un obstacle ou
/// hors bornes, ou le chemin dépasse la capacité `CAPACITE_MAX`.
///
/// # Exemple
///
/// ```rust
/// use embedded_astar::{trouver_chemin, Point};
///
/// let grille = [[false; 4]; 4]; // grille 4×4 vide
/// let chemin = trouver_chemin::<4, 4, 16>(
///     Point::nouveau(0, 0),
///     Point::nouveau(3, 3),
///     &grille,
/// )
/// .expect("un chemin doit exister sur une grille vide");
///
/// assert_eq!(*chemin.first().unwrap(), Point::nouveau(0, 0));
/// assert_eq!(*chemin.last().unwrap(),  Point::nouveau(3, 3));
/// ```
pub fn trouver_chemin<const LARGEUR: usize, const HAUTEUR: usize, const CAPACITE_MAX: usize>(
    depart: Point,
    arrivee: Point,
    grille: &[[bool; LARGEUR]; HAUTEUR],
) -> Option<Vec<Point, CAPACITE_MAX>> {
    // Rejeter immédiatement les points invalides ou bloqués.
    if !est_valide::<LARGEUR, HAUTEUR>(depart, grille)
        || !est_valide::<LARGEUR, HAUTEUR>(arrivee, grille)
    {
        return None;
    }

    // ── Structures de données ─────────────────────────────────────────────────

    // Open set : nœuds en attente d'évaluation.
    let mut ouvert: Vec<Noeud, CAPACITE_MAX> = Vec::new();

    // Ensemble fermé : cases déjà entièrement évaluées (évite les ré-expansions).
    let mut ferme: [[bool; LARGEUR]; HAUTEUR] = [[false; LARGEUR]; HAUTEUR];

    // Carte des parents : pour chaque case, quelle case l'a atteinte.
    // Utilisée pour reconstruire le chemin en fin d'algorithme.
    let mut parents: [[Option<Point>; LARGEUR]; HAUTEUR] = [[None; LARGEUR]; HAUTEUR];

    // ── Initialisation ────────────────────────────────────────────────────────

    let noeud_depart = Noeud {
        point: depart,
        cout_g: 0,
        cout_f: heuristique(depart, arrivee),
    };
    // L'open set est vide : le push ne peut pas échouer ici.
    let _ = ouvert.push(noeud_depart);

    // Directions : Haut, Droite, Bas, Gauche (4-directionnel).
    // Pour activer les diagonales, ajouter (±1, ±1) et utiliser la distance de Chebyshev.
    const DIRECTIONS: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];

    // ── Boucle principale ─────────────────────────────────────────────────────

    while !ouvert.is_empty() {
        // Sélectionner le nœud au cout_f le plus faible via scan linéaire.
        // Adapté aux petits open sets typiques des grilles embarquées.
        // Pour CAPACITE_MAX > 256, envisager un tas min.
        let mut idx_meilleur = 0;
        for i in 1..ouvert.len() {
            if ouvert[i].cout_f < ouvert[idx_meilleur].cout_f {
                idx_meilleur = i;
            }
        }

        // Extraire le meilleur nœud en O(1) via swap_remove.
        let courant = ouvert.swap_remove(idx_meilleur);
        let pc = courant.point;

        // ── Arrivée atteinte ──────────────────────────────────────────────────

        if pc == arrivee {
            return reconstruire_chemin::<LARGEUR, HAUTEUR, CAPACITE_MAX>(
                arrivee,
                depart,
                &parents,
            );
        }

        // Marquer la case comme visitée.
        ferme[pc.y as usize][pc.x as usize] = true;

        // ── Expansion des voisins ─────────────────────────────────────────────

        for &(dx, dy) in &DIRECTIONS {
            let voisin = Point {
                x: pc.x + dx,
                y: pc.y + dy,
            };

            // Ignorer les cases hors bornes, les obstacles et les cases déjà évaluées.
            if !est_valide::<LARGEUR, HAUTEUR>(voisin, grille)
                || ferme[voisin.y as usize][voisin.x as usize]
            {
                continue;
            }

            let g_tentative = courant.cout_g + 1; // coût uniforme : 1 par déplacement

            // Chercher si ce voisin est déjà dans l'open set.
            let existant = ouvert.iter_mut().find(|n| n.point == voisin);

            match existant {
                Some(noeud) if g_tentative < noeud.cout_g => {
                    // Meilleur chemin trouvé vers un voisin déjà en file : mise à jour.
                    noeud.cout_g = g_tentative;
                    noeud.cout_f = g_tentative + heuristique(voisin, arrivee);
                    parents[voisin.y as usize][voisin.x as usize] = Some(pc);
                }
                None => {
                    // Première découverte de ce voisin.
                    parents[voisin.y as usize][voisin.x as usize] = Some(pc);
                    let noeud_voisin = Noeud {
                        point: voisin,
                        cout_g: g_tentative,
                        cout_f: g_tentative + heuristique(voisin, arrivee),
                    };
                    // Si l'open set est plein, ignorer ce voisin sans paniquer.
                    let _ = ouvert.push(noeud_voisin);
                }
                _ => { /* le nœud existant a déjà un cout_g égal ou meilleur */ }
            }
        }
    }

    None // Tous les nœuds accessibles ont été explorés sans atteindre l'arrivée.
}

// ───────────────────────────────────────────────
//  Reconstruction du chemin (interne)
// ───────────────────────────────────────────────

fn reconstruire_chemin<const L: usize, const H: usize, const CAP: usize>(
    arrivee: Point,
    depart: Point,
    parents: &[[Option<Point>; L]; H],
) -> Option<Vec<Point, CAP>> {
    let mut chemin: Vec<Point, CAP> = Vec::new();
    let mut courant = arrivee;

    // Remonter de l'arrivée vers le départ via la carte des parents.
    loop {
        if chemin.push(courant).is_err() {
            // Le chemin dépasse la capacité allouée : signaler l'échec proprement.
            return None;
        }
        if courant == depart {
            break;
        }
        match parents[courant.y as usize][courant.x as usize] {
            Some(p) => courant = p,
            None => return None, // Déconnexion : ne devrait pas survenir en usage normal.
        }
    }

    chemin.reverse();
    Some(chemin)
}

// ───────────────────────────────────────────────
//  Tests
// ───────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    type Grille4 = [[bool; 4]; 4];

    fn grille_vide() -> Grille4 {
        [[false; 4]; 4]
    }

    #[test]
    fn depart_egal_arrivee() {
        let grille = grille_vide();
        let p = Point::nouveau(1, 1);
        let chemin = trouver_chemin::<4, 4, 16>(p, p, &grille).unwrap();
        assert_eq!(chemin.len(), 1);
        assert_eq!(chemin[0], p);
    }

    #[test]
    fn deplacement_horizontal() {
        let grille = grille_vide();
        let chemin = trouver_chemin::<4, 4, 16>(
            Point::nouveau(0, 0),
            Point::nouveau(3, 0),
            &grille,
        )
        .unwrap();
        assert_eq!(chemin.first().copied(), Some(Point::nouveau(0, 0)));
        assert_eq!(chemin.last().copied(),  Some(Point::nouveau(3, 0)));
        assert_eq!(chemin.len(), 4); // (0,0)→(1,0)→(2,0)→(3,0)
    }

    #[test]
    fn deplacement_vertical() {
        let grille = grille_vide();
        let chemin = trouver_chemin::<4, 4, 16>(
            Point::nouveau(0, 0),
            Point::nouveau(0, 3),
            &grille,
        )
        .unwrap();
        assert_eq!(chemin.len(), 4);
    }

    #[test]
    fn chemin_diagonal() {
        let grille = grille_vide();
        let chemin = trouver_chemin::<4, 4, 16>(
            Point::nouveau(0, 0),
            Point::nouveau(3, 3),
            &grille,
        )
        .unwrap();
        assert_eq!(chemin.first().copied(), Some(Point::nouveau(0, 0)));
        assert_eq!(chemin.last().copied(),  Some(Point::nouveau(3, 3)));
        // Optimal Manhattan = 6 déplacements → 7 points
        assert_eq!(chemin.len(), 7);
    }

    #[test]
    fn mur_avec_passage() {
        // Grille :
        //  . . . .
        //  # # . #     # = obstacle
        //  . . . .
        //  . . . .
        // Passage en (x=2, y=1) ; le chemin doit y passer.
        let mut grille = grille_vide();
        grille[1][0] = true;
        grille[1][1] = true;
        grille[1][3] = true;

        let chemin = trouver_chemin::<4, 4, 16>(
            Point::nouveau(0, 0),
            Point::nouveau(0, 2),
            &grille,
        )
        .unwrap();
        assert_eq!(chemin.first().copied(), Some(Point::nouveau(0, 0)));
        assert_eq!(chemin.last().copied(),  Some(Point::nouveau(0, 2)));
    }

    #[test]
    fn aucun_chemin_mur_total() {
        let mut grille = grille_vide();
        // Ligne 1 entièrement bloquée aucun passage possible.
        grille[1][0] = true;
        grille[1][1] = true;
        grille[1][2] = true;
        grille[1][3] = true;

        let resultat = trouver_chemin::<4, 4, 16>(
            Point::nouveau(0, 0),
            Point::nouveau(0, 2),
            &grille,
        );
        assert!(resultat.is_none());
    }

    #[test]
    fn depart_obstacle() {
        let mut grille = grille_vide();
        grille[0][0] = true;
        let resultat = trouver_chemin::<4, 4, 16>(
            Point::nouveau(0, 0),
            Point::nouveau(3, 3),
            &grille,
        );
        assert!(resultat.is_none());
    }

    #[test]
    fn arrivee_obstacle() {
        let mut grille = grille_vide();
        grille[3][3] = true;
        let resultat = trouver_chemin::<4, 4, 16>(
            Point::nouveau(0, 0),
            Point::nouveau(3, 3),
            &grille,
        );
        assert!(resultat.is_none());
    }

    #[test]
    fn hors_bornes() {
        let grille = grille_vide();
        let resultat = trouver_chemin::<4, 4, 16>(
            Point::nouveau(0, 0),
            Point::nouveau(99, 99),
            &grille,
        );
        assert!(resultat.is_none());
    }
}