Serpent

Un petit exo d’itérateurs tout en zig-zag.

Une reprise d’un petit exo de première année à l’Ensimag. Il est sympa parce que pas si simple à écrire, y-compris en python.

On cherche à dessiner en svg une grille numérotée comme celle-ci :

Ce qui rend les choses compliquées est le parcours en boustrophédon.

Pire que ça, pour corser un peu le tout, je vous propose de compléter le code ci-dessous et et de résoudre le problème à base d’un itérateur sur les coordonnées de chaque carré dans leur ordre de numérotation.

use std::io::Write;

// on boucle sur toutes les coordonnees de carres
fn coordonnees_carres(
    largeur: u32,
    hauteur: u32,
    taille_carre: u32,
) -> impl Iterator<Item = (u32, u32)> {
    unimplemented!()
}

fn serpent<P: AsRef<std::path::Path>>(
    fichier: P,
    largeur: u32,
    hauteur: u32,
    taille_carre: u32,
) -> std::io::Result<()> {
    let mut svg = std::fs::File::create(fichier)?;
    writeln!(&mut svg, "<svg width='{}' height='{}'>", largeur, hauteur)?;
    coordonnees_carres(largeur, hauteur, taille_carre)
        .enumerate()
        .try_for_each(|(i, (x, y))| {
            writeln!(
                &mut svg,
                "<rect x='{}' y='{}' width='{}' height='{}' stroke='blue' fill='white'/>",
                x + taille_carre / 20,
                y + taille_carre / 20,
                taille_carre * 9 / 10,
                taille_carre * 9 / 10
            )?;
            writeln!(
                &mut svg,
                "<text x='{}' y='{}' text-anchor='middle'>{}</text>",
                x + taille_carre / 2,
                y + taille_carre / 2,
                i
            )
        })?;
    write!(&mut svg, "</svg>")
}

fn main() -> std::io::Result<()> {
    serpent("gros.svg", 900, 600, 80)?;
    serpent("petit.svg", 90, 600, 80)?;
    serpent("minuscule.svg", 90, 90, 80)?;
    serpent("vide.svg", 90, 90, 100)?;
    Ok(())
}

L’archive à compléter est disponible ici.

Solution

On va découper le problème en deux parties. Avant de s’intéresser aux coordonnées en elles-même nous allons tout d’abord écrire un itérateur sur tous les mouvements à réaliser pour passer d’une case l’autre.

On commence par créer un petit type énuméré pour stocker les différents mouvements possibles.

#[derive(Copy, Clone)]
enum Direction {
    Bas,
    Gauche,
    Droite,
}

Bon, maintenant l’itérateur sur les mouvements. On cherche à faire :

  • une ligne vers la droite
  • on descend
  • une ligne vers la gauche
  • on descend
  • une ligne vers la droite ….

Et ce jusqu’à ce que l’image soit remplie. Chaque ligne est ici composée de plusieurs déplacements latéraux. Il y a sans doute plein de manières de faire ça, voici ma solution :

use itertools::Itertools;
use std::iter::repeat;

// on prend une largeur et hauteur en nombre de carres
// et on boucle sur tous les deplacements a realiser
fn deplacements(largeur: usize, hauteur: usize) -> impl Iterator<Item = Direction> {
    let ligne_vers_droite = repeat(Direction::Droite).take(largeur.saturating_sub(1)); // n carres = n-1 deplacements
    let ligne_vers_gauche = repeat(Direction::Gauche).take(largeur.saturating_sub(1));
    let lignes = repeat(ligne_vers_droite)
        .interleave(repeat(ligne_vers_gauche))
        .take(hauteur);
    let bas = repeat(repeat(Direction::Bas).take(1));
    lignes.interleave_shortest(bas).flatten()
}

La méthode interleave d’Itertools nous permet un algo assez clair. Il y a néanmoins quelques subtilités. Une ligne, qu’elle soit vers la droite ou vers la gauche est un itérateur sur des déplacements. Son type exact est Take<Repeat<Direction>>. La variable lignes, elle, est un itérateur sur toutes les lignes, soit un itérateur sur des éléments de type Take<Repeat<Direction>>. On souhaite entrelacer les lignes avec les déplacements vers le bas. On utiliser donc encore une fois la méthode interleave mais ce n’est possible que si l’itérateur bas itère sur des éléments de même type que l’itérateur lignes. Il n’est donc pas possible d’utiliser par exemple repeat(std::iter::once(Direction::Bas)) du moins sans passer par un fat pointer.

Le flatten final nous permet d’un itérateur sur des itérateurs à un itérateur sur des déplacements.

Il ne reste plus qu’à transformer ça en itérateur sur des coordonnées :

// on boucle sur toutes les coordonnees de carres
fn coordonnees_carres(
    largeur: u32,
    hauteur: u32,
    taille_carre: u32,
) -> impl Iterator<Item = (u32, u32)> {
    let carres_en_largeur = largeur / taille_carre;
    let carres_en_hauteur = hauteur / taille_carre;
    let x = (largeur - carres_en_largeur * taille_carre) / 2;
    let y = (hauteur - carres_en_hauteur * taille_carre) / 2;
    deplacements(carres_en_largeur as usize, carres_en_hauteur as usize).scan(
        (x, y),
        move |coord, deplacement| {
            let old_coord = *coord;
            match deplacement {
                Direction::Bas => coord.1 += taille_carre,
                Direction::Gauche => coord.0 -= taille_carre,
                Direction::Droite => coord.0 += taille_carre,
            }
            Some(old_coord)
        },
    )
}

Il nous faut une position courante, mise à jour à chaque nouveau déplacement. Un genre de fold sauf que l’on doit produire un itérateur et non une valeur finale. C’est donc un scan que l’on utilise ici.

Le truc sympa avec une approche par itérateurs c’est que l’on est moins sensible aux cas spéciaux que dans un code plus “manuel”.