📖 À propos du projet
Lem-in est un projet algorithmique qui consiste à faire passer un nombre donné de fourmis d'un point de départ à un point d'arrivée dans le minimum de tours, en évitant les collisions. Le projet utilise un algorithme de BFS (Breadth-First Search) combiné à une gestion de flot maximum inspirée d'Edmonds-Karp.
Cette version Rust est une traduction fidèle de la version C originale. Elle produit exactement le même nombre de tours sur toutes les maps de test (validé par stress test sur 20 maps aléatoires).
Avantages de la version Rust :
- ✅ Sécurité mémoire garantie — pas de segfault, pas de fuite, pas de double-free (ownership +
Drop) - ✅ Performance —
HashMap/HashSetO(1) au lieu de listes chaînées O(n) - ✅ BFS itératif — pas de risque de stack overflow sur grandes maps
- ✅ Code modulaire — 8 modules clairement séparés
- ✅ Compilation WASM native avec
wasm-pack
🎨 Visualiseur Web & Optimisations de performance
Le visualiseur inclus dans ce projet (disponible dans docs/visualizer.html) est une application monopage ultra-fluide qui combine des moteurs de rendu 2D et 3D dans une esthétique sombre inspirée par NieR Automata.
Pour garantir des performances optimales (60 FPS constants) sur des cartes contenant des milliers de nœuds et des dizaines de milliers de fourmis, les optimisations architecturales suivantes ont été mises en œuvre :
⚡ Moteur 2D (Canvas & d3.js)
- ✨ Sprite Caching (Offscreen Rendering) : Les nœuds et les fourmis ne sont pas redessinés via des fonctions vectorielles CPU coûteuses (comme
ctx.arc()oucreateRadialGradient) à chaque frame. Ils sont pré-générés dans des canvas hors-écran et copiés instantanément via le GPU (ctx.drawImage()). - ✨ Batching des tracés (Liens) : Les milliers de liaisons du graphe sont regroupées par état (normal, sélectionné, heatmap) et dessinées en une seule passe d'appels système graphiques pour minimiser les draw calls.
- ✨ d3-quadtree spatial indexing : Pour l'interaction utilisateur, la détection du nœud survolé par la souris utilise une structure d'octree 2D, réduisant la complexité de recherche de $O(n)$ à $O(\log n)$ (11 comparaisons au lieu de 1500 sur les grandes cartes).
- ✨ Lignes droites adaptatives : Sur les cartes de plus de 200 nœuds, le tracé des courbes de Bézier quadratiques est automatiquement remplacé par des lignes droites pour ménager le processeur graphique.
🌌 Moteur 3D (Three.js & WebGL)
- ✨ WebGL Adaptive LOD (Level of Detail) : La géométrie des pièces s'ajuste dynamiquement à la taille de la carte. Au-delà de 500 nœuds, les sphères haute résolution deviennent des icosaèdres simples (20 triangles), et des octaèdres (8 triangles) au-delà de 1500 nœuds.
- ✨ Post-processing UnrealBloomPass : Les fourmis brillent d'un éclat rouge néon vif émissif très puissant, contrastant avec le rendu laqué/céramique glacée des nœuds (MeshPhysicalMaterial à haute réflectivité, ior 1.8 et vernis clearcoat de 1.0).
- ✨ LOD sur le CSS2DRenderer & les Halos : Les étiquettes HTML et les halos de lueur transparente sont automatiquement désactivés sur les cartes massives pour économiser le rendu du DOM et les calculs d'opacité.
- ✨ Idle Render Throttling : Le moteur de rendu Three.js et le label renderer s'endorment (GPU à 0%) lorsqu'aucun mouvement ou action utilisateur n'est détecté.
⚙️ Multithreading (Web Workers)
- ✨ Résolution asynchrone hors-thread : L'exécution du code WebAssembly de résolution (compilé depuis Rust) est entièrement déléguée à un Web Worker en arrière-plan. Cela empêche le navigateur d'afficher le message "le script ne répond pas" et garde l'interface 100% interactive, même pendant la résolution de graphes de superposition gigantesques.
🏗️ Structures de données
Définies dans src/models.rs. La version Rust utilise Rc<RefCell<Room>> (comptage de références + mutation intérieure) pour permettre le partage de propriété dans le graphe cyclique — l'équivalent des pointeurs bruts t_room * du C, mais avec une sécurité garantie à la compilation.
Coord — Coordonnées 2D
#[derive(Clone, Debug, Default)]
pub struct Coord {
pub x: i32,
pub y: i32,
}
RoomRef — Type alias pour référence partagée
pub type RoomRef = Rc<RefCell<Room>>;
Rc = Reference Counted (comptage de références automatique). RefCell = mutation intérieure (borrow-checking runtime). Ensemble, ils remplacent les t_room * + malloc/free manuels du C.
Room — La Structure Centrale
pub struct Room {
pub name: String, // Nom (propriétaire, pas de malloc/ft_strdup)
pub coord: Coord,
pub ant: i32, // ID de la fourmi (0 si vide)
pub start: bool, // bool natif (vs int 0/1 en C)
pub end: bool,
pub links: Vec<RoomRef>, // Vec contigu (vs t_list *lnks chaîné en C)
pub f_to: Option<RoomRef>, // Option (vs NULL pointer en C)
pub f_fm: Option<RoomRef>, // Option = impossible d'oublier le check
}
Stringau lieu dechar *+ft_strdup— pas de gestion mémoire manuelleVec<RoomRef>au lieu det_list *lnks— contigu en mémoire (meilleur cache locality)Option<RoomRef>au lieu destruct s_room *f_to+ NULL — le compilateur force le checkboolau lieu deint start/end— plus clair- Pas de
next—Farm.roomsest unVec - Pas de
t_room **(double pointeur) —Rc::ptr_eq+borrow_mutsuffisent
Farm — Conteneur Principal
pub struct Farm {
pub ants: i32,
pub rooms: Vec<RoomRef>, // Vec (vs liste chaînée en C)
pub name_map: HashMap<String, RoomRef>, // Lookup O(1) par nom
}
Le name_map est un ajout Rust : en C, find_name() parcourt toute la liste en O(n). En Rust, farm.find_by_name() est O(1) grâce à la HashMap.
Path — Chemin pour la simulation
pub struct Path {
pub rooms: Vec<RoomRef>, // [première_salleAprèsStart, ..., end]
pub send_ants: i32, // temporaire, utilisé pendant path_config
pub min_ants: i32, // allocation finale calculée par path_config
}
En C, cette structure s'appelle t_bfs et contient aussi path_size, prev, next. En Rust, path_size est calculé via rooms.len(), et prev/next ne sont plus nécessaires (le BFS utilise une HashMap de parents).
🔍 Parsing et validation
Le parsing utilise une FSM à 3 sections identique à la version C, mais exploite HashSet pour la validation d'unicité en O(1) au lieu de parcours O(n).
| Section | Contenu | Format |
|---|---|---|
| 0 | Nombre de fourmis | i32 positif (line.parse::<i32>()) |
| 1 | Salles | nom x y (3 tokens, split_whitespace) |
| 2 | Liens | room1-room2 (un seul -) |
Validation
- Noms uniques :
HashSet<String>O(1) (vsis_name()O(n) en C) - Coordonnées uniques :
HashSet<(i32, i32)>O(1) - Recherche de salle par nom :
HashMap<String, RoomRef>O(1) (vsfind_name()O(n) en C) - Liens bidirectionnels avec
Rc::ptr_eqpour éviter les doublons - Erreurs via
Result<ParseResult, String>(vsft_puterr+ retour -1 en C)
ft_lstadd = ajout en tête), les salles et liens sont insérés avec insert(0, ...) pour reproduire l'ordre LIFO. C'est critique car l'ordre des voisins affecte le parcours BFS et donc le résultat final.
🧮 Algorithmes
1. Vue d'ensemble
solve()
│
├── find_start() / find_end()
│
├── BOUCLE de découverte de chemins :
│ ├── bfs_find_path() : BFS itératif VecDeque
│ ├── update_flow() : marquer f_to/f_fm + annuler conflits
│ └── répéter jusqu'à ce qu'aucun chemin ne soit trouvé
│
└── send_ants() : simulation tour par tour
├── extract_paths() : suivre f_to depuis les voisins de start
├── path_config() : optimiser la distribution des fourmis
└── boucle (move_ants + select_paths)
2. BFS — bfs_find_path()
Le BFS est itératif (avec VecDeque) au lieu de récursif en C. Avantage : pas de risque de stack overflow sur grandes maps.
HashSet<usize>pour les visited — O(1) (vsroom_in()O(n) en C qui parcourt toute la file)HashMap<usize, RoomRef>pour les parents — reconstruction du chemin en O(L)- Clés =
Rc::as_ptr(r) as usize(adresse mémoire) — équivalent de comparer les pointeurs en C - Même logique
flow_to+undoque la version C (Edmonds-Karp)
flow_to(from, to) :
- Si
fromest start : arête utilisée sito.f_fm == from - Sinon : arête utilisée si
from.f_to == to
f_fm (reverse edge) quand le noeud courant a un f_to qui ne pointe pas vers son parent.
3. Mise à jour du flux — update_flow()
Backtrack depuis end vers start, en marquant f_to/f_fm et en annulant les conflits (tunnels inverses) :
// Pour chaque paire (prv, r) sur le chemin :
if !r_f_to_is_prv && !prv_f_fm_is_r {
r.f_fm = Some(prv); // marquer backward
prv.f_to = Some(r); // marquer forward
}
// Annuler les conflits :
if r_f_to_is_prv { r.f_to = None; }
if prv_f_fm_is_r { prv.f_fm = None; }
4. Extraction des chemins — extract_paths()
Suit f_to depuis les voisins du start (pas le start lui-même) jusqu'à end. Les chemins sont triés par longueur (sort_by_key) — équivalent à insertio() en C.
5. Optimisation — path_config()
Portage fidèle de condemn_path + populate_path + save_min :
// populate_path : distribution proportionnelle
send_ants[0] = first;
send_ants[i+1] = (send_ants[i] - abs(path_size[i+1] - path_size[i])).max(0);
// condemn_path : tours = population + longest_path_size
// path_config : tester toutes les combinaisons, garder le minimum
🐜 Simulation des fourmis
while end.ant < total_ants {
moved = move_ants(&paths); // avancer toutes les fourmis
moved |= select_paths(&mut paths, farm); // injecter nouvelles fourmis
output.push('\n');
if !moved { break; }
}
push_ants() — Shift forward
Parcourt le chemin et décale les fourmis d'une case (sauvegarde aux_ant, échange avec prev_ant). Si une fourmi arrive à end, end.ant est incrémenté.
select_paths() — Injection
Pour chaque chemin avec min_ants > 0 : place une nouvelle fourmi (ID = total_ants - farm.ants + 1) dans la première salle, décrémente min_ants et farm.ants.
📦 Architecture des modules
models.rs
Coord, Room, Farm, Path, RoomRef. Définit les structures de données et les méthodes find_start, find_end, find_by_name.
parser.rs
FSM 3 sections + validation (HashSet O(1)). Retourne Result<ParseResult, String>.
bfs.rs
BFS itératif VecDeque avec flow_to + undo. HashSet visited O(1). Reconstruction via HashMap parent.
flow.rs
update_flow avec gestion des conflits + extract_paths (suit f_to depuis voisins de start).
path_config.rs
path_config + condemn_path + populate_path. Optimisation de la distribution des fourmis.
simulation.rs
send_ants + push_ants (shift) + select_paths (injection). Boucle tour par tour.
solver.rs
Orchestration : boucle BFS → update_flow → send_ants. Point d'entrée solve().
lib.rs + main.rs
lib.rs : entry WASM (parse_and_solve). main.rs : entry CLI (stdin → stdout).
📋 Format des maps — Règles de conformité
Le visualiseur valide chaque map avant l'exécution. Une map non conforme affiche un message d'erreur détaillé. Les règles suivantes sont issues du sujet officiel Lem-in :
Structure d'une map
nombre_de_fourmis
#commentaire optionnel
##start
nom_salle coord_x coord_y
##end
nom_salle coord_x coord_y
nom_salle coord_x coord_y
...
nom_salle1-nom_salle2
nom_salle3-nom_salle4
...
Règles de validation (10 contrôles)
| N° | Règle | Détail |
|---|---|---|
1 |
Première ligne | Doit être un entier positif (nombre de fourmis) |
2 |
Noms de salles | Ne peuvent pas commencer par L ou #, ni contenir - |
3 |
Unicité des noms | Chaque nom de salle doit être unique |
4 |
Unicité des coordonnées | Deux salles ne peuvent pas partager les mêmes (x, y) |
5 |
##start |
Doit être présent exactement une fois, suivi d'une salle |
6 |
##end |
Doit être présent exactement une fois, suivi d'une salle |
7 |
Au moins une salle | La map doit contenir au minimum une salle (start + end) |
8 |
Au moins un lien | Si >1 salle, au moins un lien doit exister |
9 |
Format des liens | name1-name2 — un seul -, deux salles existantes |
10 |
Pas de lien vers soi-même | Un lien A-A est invalide |
Commentaires autorisés
#commentaire— ignoré par le parseur##start— marque la salle suivante comme départ##end— marque la salle suivante comme arrivée#Here is the number of lines required: N— généré par le générateur officiel
Exemple de map invalide
-5 ❌ nombre négatif
##start
1 0 0
##end
1 0 0 ❌ doublon de nom ET de coordonnées
1-1 ❌ lien vers soi-même
2-3 ❌ salles 2 et 3 inexistantes
Le visualiseur affichera : ⚠ MAP INVALIDE — 5 erreur(s) avec le détail de chaque erreur.
📝 Exemple complet
Input :
4
##start
1 0 0
2 1 0
3 0 1
4 1 1
##end
5 2 0
1-2
1-3
2-4
3-4
4-5
Output (identique à la version C) :
L1-3
L1-4 L2-3
L2-4 L1-5 L3-3
L3-4 L2-5 L4-3
L4-4 L3-5
L4-5
6 tours — les deux versions produisent exactement la même sortie (validé par stress test sur 20 maps aléatoires avec le générateur officiel).
🔧 Compilation & Build WASM
Build native
cargo build --release
./target/release/lem-in < map.txt
Build WebAssembly
# Installer wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Compiler en WASM et exporter directement dans le dossier docs/
wasm-pack build --target web --release --out-dir docs
# (Optionnel) Ou compiler par défaut (dans pkg/) et copier manuellement :
# wasm-pack build --target web --release
# cp -r pkg/* docs/
# Tester localement
python3 -m http.server 8000
CI/CD GitHub Actions
Le projet inclut un workflow GitHub Actions (.github/workflows/deploy.yml) qui compile automatiquement le WASM et déploie sur GitHub Pages à chaque push sur main.
⚡ Comparaison C vs Rust
Cette section compare en détail les deux implémentations : la version C originale et la version Rust.
Vue d'ensemble
| Aspect | Version C | Version Rust |
|---|---|---|
| Lignes de code (cœur) | ~1375 (15 fichiers) | ~600 (8 modules) |
| Dépendances | libft custom (~80 fichiers) | wasm-bindgen, js-sys, console_error_panic_hook |
| Build native | Makefile + gcc | Cargo |
| Build WASM | Emscripten (emcc, ~500MB) | wasm-pack (léger) |
| CI/CD | Aucune | GitHub Actions auto |
| Tests automatisés | Aucun | Stress test 20 maps ✅ |
Structures de données
| Concept | C | Rust |
|---|---|---|
| Salles | t_room * (liste chaînée) |
Vec<RoomRef> (contigu) |
| Voisins | t_list *lnks (liste chaînée) |
Vec<RoomRef> (contigu) |
| Nom | char * + ft_strdup + free |
String (ownership auto) |
| Flux forward/backward | struct s_room *f_to (NULL = absent) |
Option<RoomRef> (impossible d'oublier le check) |
| Start/End | int start, end (0/1) |
bool start, end |
| Double pointeur | t_room ** (fragile, pour backtracking) |
Pas besoin (Rc::ptr_eq + borrow_mut) |
| Recherche par nom | find_name() O(n) parcours liste |
HashMap O(1) |
Gestion mémoire
| Aspect | C | Rust |
|---|---|---|
| Allocation | malloc manuel | Rc (comptage auto) |
| Libération | free manuel + destruct_farm | Automatique (Drop) |
| Fuites possibles | ✗ Oui (oubli free) | ✓ Non (compile-time) |
| Double-free | ✗ Possible | ✓ Impossible (ownership) |
| Null pointer deref | ✗ Possible | ✓ Impossible (Option) |
| Use-after-free | ✗ Possible | ✓ Impossible (borrow checker) |
Algorithmes
| Aspect | C | Rust |
|---|---|---|
| BFS | Récursif (stack overflow possible) | Itératif VecDeque (sûr) |
| Visited check | room_in() O(n) parcours file |
HashSet O(1) |
| Parent tracking | prev dans t_bfs |
HashMap<usize, RoomRef> |
| update_flow | Gestion des conflits ✅ | Gestion des conflits ✅ (portage fidèle) |
| path_config | condemn_path + populate_path ✅ |
Portage fidèle ✅ |
| Simulation | push_ants + select_paths ✅ |
Portage fidèle ✅ |
| Ordre voisins | ft_lstadd = LIFO |
insert(0, ...) = LIFO (matching) |
Gestion d'erreurs
| Aspect | C | Rust |
|---|---|---|
| Style | Retour int (0/-1) + ft_puterr (side effect) | Result<T, String> (valeur de retour) |
| Type d'erreur | String magique via ft_puterr | String (pourrait être enum Error) |
| Séparation logique/erreur | ✗ Mélange | ✓ Séparation propre |
WebAssembly
| Aspect | C (Emscripten) | Rust (wasm-bindgen) |
|---|---|---|
| Toolchain | emcc (~500MB) | wasm-pack (léger) |
| Export | EMSCRIPTEN_KEEPALIVE | #[wasm_bindgen] |
| Entrée/sortie | Buffer global g_output_buffer + strcpy | &str → String direct |
| Glue JS | Manuel (~100 lignes) | Auto-généré par wasm-bindgen |
| Optimisation | -O2 | opt-level = "s" + lto = true |
Validation — Résultats identiques
Les deux versions produisent exactement le même nombre de tours sur toutes les maps testées :
| Test | C (tours) | Rust (tours) | Match |
|---|---|---|---|
| Map simple (3 fourmis, 1 chemin) | 5 | 5 | ✅ |
| Map multi-chemins (10 fourmis) | 9 | 9 | ✅ |
| Map asymétrique (20 fourmis) | 10 | 10 | ✅ |
| Generator --flow-one | 32 | 32 | ✅ |
| Generator --flow-ten | 31 | 31 | ✅ |
| Generator --flow-thousand | 33 | 33 | ✅ |
| Generator --big | 59 | 59 | ✅ |
| Generator --big-superposition | 85 | 85 | ✅ |
| Stress test 20 maps --big | — | — | ✅ 20/20 |
Résumé
Avantages du C
- Compilation sans runtime
- Contrôle fin de la mémoire
- Compatibilité maximale (42 Norme)
- Executable standalone minimal
Avantages du Rust
- Zéro fuite mémoire (ownership)
- HashMap/HashSet O(1) vs listes O(n)
- BFS itératif (pas de stack overflow)
- 3.2× moins de code (600 vs 1375 lignes)
- Option<T> élimine les null deref
- wasm-bindgen auto-génère le glue JS
- CI/CD GitHub Actions intégré