Lem-in · Rust WASM · École 42
Une réécriture de Lem-in · Édition Rust · WASM Ready

Le projet Lem-in,
réécrit en RustC memory-safe & WASM.

Mêmes algorithmes Edmonds-Karp & path_config, même nombre de tours que la version C (validé sur 20 maps aléatoires). Mais le code Rust offre 8 modules clairement séparés, une sécurité mémoire garantie par l'ownership, des HashMap/HashSet O(1) au lieu de listes chaînées O(n), et une compilation WebAssembly native pour le visualiseur interactif.

Modules Rust
8
architecture clairement séparée
Sécurité mémoire
100%
ownership + Drop, zéro fuite
WASM
visualiseur web natif
Équivalence C
20/20
maps validées, même nb de tours

📖 À 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)
  • PerformanceHashMap/HashSet O(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() ou createRadialGradient) à 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
}
Différences clés avec le C :
  • String au lieu de char * + ft_strdup — pas de gestion mémoire manuelle
  • Vec<RoomRef> au lieu de t_list *lnks — contigu en mémoire (meilleur cache locality)
  • Option<RoomRef> au lieu de struct s_room *f_to + NULL — le compilateur force le check
  • bool au lieu de int start/end — plus clair
  • Pas de nextFarm.rooms est un Vec
  • Pas de t_room ** (double pointeur) — Rc::ptr_eq + borrow_mut suffisent

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).

SectionContenuFormat
0Nombre de fourmisi32 positif (line.parse::<i32>())
1Sallesnom x y (3 tokens, split_whitespace)
2Liensroom1-room2 (un seul -)

Validation

Ordre LIFO : Comme 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.

flow_to(from, to) :
  • Si from est start : arête utilisée si to.f_fm == from
  • Sinon : arête utilisée si from.f_to == to
Le mode undo autorise uniquement le voisin 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)

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

ConceptCRust
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

AspectCRust
Allocationmalloc manuelRc (comptage auto)
Libérationfree manuel + destruct_farmAutomatique (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

AspectCRust
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

AspectCRust
StyleRetour int (0/-1) + ft_puterr (side effect)Result<T, String> (valeur de retour)
Type d'erreurString magique via ft_puterrString (pourrait être enum Error)
Séparation logique/erreur✗ Mélange✓ Séparation propre

WebAssembly

AspectC (Emscripten)Rust (wasm-bindgen)
Toolchainemcc (~500MB)wasm-pack (léger)
ExportEMSCRIPTEN_KEEPALIVE#[wasm_bindgen]
Entrée/sortieBuffer global g_output_buffer + strcpy&strString direct
Glue JSManuel (~100 lignes)Auto-généré par wasm-bindgen
Optimisation-O2opt-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 :

TestC (tours)Rust (tours)Match
Map simple (3 fourmis, 1 chemin)55
Map multi-chemins (10 fourmis)99
Map asymétrique (20 fourmis)1010
Generator --flow-one3232
Generator --flow-ten3131
Generator --flow-thousand3333
Generator --big5959
Generator --big-superposition8585
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é
Conclusion : Les deux implémentations sont fonctionnellement équivalentes (même nombre de tours sur tous les tests). La version Rust est plus concise, plus sûre, et plus performante sur le parsing/BFS grâce aux HashMap/HashSet. La version C reste l'implémentation de référence à 42 et le choix par défaut. La version Rust est une alternative moderne qui offre des garanties de sécurité et de performance, au prix d'une courbe d'apprentissage plus raide.