Fillit · 42 Backtracking · Plus Petit Carré
École 42 · Algorithm Project

Le projet Fillit — assembler des Tetriminos
dans le plus petit carré possible.

Un guide pédagogique complet du projet fillit : du parsing des pièces Tetriminos à l'algorithme de backtracking qui trouve le plus petit carré capable de les contenir toutes. Code C réel, diagrammes visuels pas à pas, et explications précises de chaque étape de la résolution.

Le backtracking expliqué Le code complet Compiler & tester

Le projet Fillit

Fillit est un projet algorithmique de l'École 42 qui vous confronte à un problème classique en informatique : trouver la solution optimale parmi un ensemble exponentiel de possibilités, dans un temps raisonnable.

Le principe est simple à énoncer : on vous donne un fichier contenant une liste de pièces de Tetris (des Tetriminos), chacune composée de 4 blocs. Votre programme doit les assembler toutes ensemble dans le plus petit carré possible. Une pièce de Tetris occupe 4 cases, donc avec n pièces il faudra au minimum ceil(sqrt(4n)) cases de côté — mais ce n'est qu'un minorant, car la géométrie des pièces peut imposer un carré plus grand.

Ce qui rend le problème difficile, c'est que chaque pièce ajoutée multiplie exponentiellement le nombre de configurations possibles. Avec 26 pièces (le maximum autorisé), le nombre de façons de les arranger est astronomique. Un algorithme naïf qui essaierait toutes les combinaisons ne terminerait jamais. Il faut donc utiliser une technique intelligente : le backtracking (retour sur trace), qui explore l'arbre des possibilités en abandonnant dès qu'une branche ne peut pas mener à une solution.

Contraintes du sujet

Le projet doit être écrit en C avec la Norme 42, sans fuites mémoire, et n'utiliser que les fonctions exit, open, close, write, read, malloc, free. Le binaire s'appelle fillit et prend un seul argument : le chemin du fichier contenant les Tetriminos. La moulinette impose un timeout arbitraire — votre programme doit répondre rapidement, même avec 26 pièces.

Les 19 formes de Tetriminos valides

Un Tetrimino est une pièce de 4 blocs connectés (chaque bloc touche au moins un autre bloc sur l'un de ses 4 côtés). Il existe exactement 5 formes libres (en considérant les rotations comme équivalentes), mais comme le sujet interdit la rotation, on compte 19 formes distinctes (5 formes × rotations uniques). Votre programme ne fait pas de rotation : il place chaque pièce telle qu'elle apparaît dans le fichier.

I (vertical)
...# ...# ...# ...#
I (horizontal)
.... .... .... ####
O (carré)
.... .##. .##. ....
L
.... .### .#.. ....
T
.... .### ..#. ....

Le format des Tetriminos

Chaque Tetrimino est décrit dans un bloc de 4 lignes de 4 caractères, séparé du suivant par une ligne vide. Les caractères autorisés sont # (bloc) et . (vide).

Structure d'un fichier valide

Un fichier fillit contient entre 1 et 26 Tetriminos. Chaque Tetrimino occupe exactement 4 lignes de 4 caractères, suivies d'un caractère \n après chaque ligne. Entre deux Tetriminos, il y a exactement un \n supplémentaire (une ligne vide). Le dernier Tetrimino n'est pas obligé d'être suivi d'une ligne vide, mais il peut l'être.

valid_sample.fillit
# 4 Tetriminos séparés par des lignes vides
...#
...#
...#
...#
# ← ligne vide de séparation
....
....
....
####
# ← ligne vide
.###
...#
....
....
# ← ligne vide
....
..##
.##.
....

Règles de validation

Votre programme doit rejeter tout fichier invalide en affichant error. Un Tetrimino est valide si et seulement si toutes les conditions suivantes sont remplies :

  1. Exactement 4 lignes de 4 caractères, chacune suivie d'un \n.
  2. Uniquement les caractères # et . — aucun autre caractère n'est toléré (pas d'espaces, pas de chiffres, pas de lettres).
  3. Exactement 4 blocs (#) par Tetrimino — ni plus, ni moins.
  4. Les 4 blocs sont connectés — chaque bloc doit toucher au moins un autre bloc sur l'un de ses 4 côtés (haut, bas, gauche, droite). Les connexions diagonales ne comptent pas. En pratique, cela signifie que la somme des connexions entre blocs doit être d'au moins 6 (un arbre connecté de 4 nœuds a 3 arêtes, mais comme chaque arête est comptée deux fois, on obtient 6).
Piège : les pièces déconnectées

Une pièce comme #..# (deux blocs séparés par des points) est invalide même si elle contient 4 # au total, car les blocs ne sont pas connectés. La validation doit vérifier les connexions, pas seulement le compte de blocs.

Pas de rotation

Le sujet est très clair : aucune rotation n'est permise. Une pièce qui apparaît comme ##.. / #... / #... / .... dans le fichier doit être placée exactement dans cette orientation. Cela signifie que deux pièces géométriquement identiques mais orientées différemment sont considérées comme distinctes. Votre algorithme n'a pas à essayer les 4 rotations de chaque pièce — il place la pièce telle quelle.

Parsing & validation

La première étape consiste à lire le fichier, extraire chaque Tetrimino dans une structure de données, et valider qu'il respecte toutes les règles du sujet.

Structure de données

Chaque Tetrimino est stocké dans une structure t_tetri qui contient sa forme (un tableau 4×4 de caractères), sa lettre d'identification (A, B, C...), et ses dimensions utiles après normalisation (largeur et hauteur du rectangle englobant minimum).

includes/fillit.h
typedef struct  s_tetri
{
    char    shape[4][5];   /* 4 lignes de 4 chars + '\0' */
    char    letter;         /* 'A', 'B', 'C', ... */
    int     width;          /* largeur du bounding box */
    int     height;         /* hauteur du bounding box */
}               t_tetri;

typedef struct  s_map
{
    char    **grid;         /* grille carrée dynamique */
    int     size;           /* taille de la grille (côté) */
}               t_map;

Lecture du fichier

La lecture se fait en un seul appel read() avec un buffer assez grand pour contenir les 26 Tetriminos maximum. Chaque Tetrimino occupe 20 octets (4 lignes × 5 caractères avec le \n), plus 1 octet pour la ligne vide de séparation, soit 21 octets × 26 = 546 octets maximum.

src/parsing.c
int     read_file(char *filename, char *buffer)
{
    int     fd;
    int     bytes_read;

    fd = open(filename, O_RDONLY);
    if (fd == -1)
        return (-1);
    bytes_read = read(fd, buffer, BUF_SIZE * MAX_TETRIMINOS);
    if (bytes_read <= 0)
    {
        close(fd);
        return (-1);
    }
    buffer[bytes_read] = '\0';
    close(fd);
    return (bytes_read);
}

Extraction et validation

L'extraction parcourt le buffer caractère par caractère. Pour chaque Tetrimino, on copie les 4 lignes de 4 caractères dans shape[], on vérifie le format (présence des \n aux bons endroits), puis on valide la pièce (4 blocs connectés). Si tout est correct, on passe au Tetrimino suivant en sautant la ligne vide de séparation.

src/parsing.c — validate_tetri
/* Compte le nombre de blocs '#' dans le Tetrimino */
static int  count_blocks(t_tetri *tetri)
{
    int  i;
    int  j;
    int  count;

    count = 0;
    i = 0;
    while (i < 4)
    {
        j = 0;
        while (j < 4)
        {
            if (tetri->shape[i][j] == '#')
                count++;
            j++;
        }
        i++;
    }
    return (count);
}

/* Vérifie que les blocs sont connectés (au moins 6 connexions) */
static int  check_connections(t_tetri *tetri)
{
    int  i;
    int  j;
    int  connections;

    connections = 0;
    i = 0;
    while (i < 4)
    {
        j = 0;
        while (j < 4)
        {
            if (tetri->shape[i][j] == '#')
            {
                if (i > 0 && tetri->shape[i - 1][j] == '#')
                    connections++;
                if (i < 3 && tetri->shape[i + 1][j] == '#')
                    connections++;
                if (j > 0 && tetri->shape[i][j - 1] == '#')
                    connections++;
                if (j < 3 && tetri->shape[i][j + 1] == '#')
                    connections++;
            }
            j++;
        }
        i++;
    }
    /* 4 blocs connectés = 3 arêtes × 2 (bidirectionnel) = 6 minimum */
    return (connections >= 6);
}
Pourquoi 6 connexions minimum ?

Avec 4 blocs connectés, on a au minimum 3 liaisons (un arbre). Chaque liaison est comptée deux fois (une fois dans chaque sens), donc le minimum est 6. Si la somme des connexions est inférieure à 6, la pièce est forcément déconnectée. Les pièces comme le carré O ont 8 connexions (4 arêtes × 2), et la pièce I a 6 connexions (3 arêtes × 2).

Normalisation des pièces

Avant de placer les pièces sur la grille, il faut les normaliser : décaler les blocs vers le coin haut-gauche du carré 4×4, et calculer les dimensions utiles (bounding box). Cela simplifie le placement et l'affichage.

Pourquoi normaliser ?

Le sujet dit que "chaque Tetrimino est défini par ses limites minimales (leurs '#')". Les 12 caractères vides restants sont ignorés lors de l'assemblage. Autrement dit, une pièce décrite comme .... / ..## / .##. / .... occupe réellement un rectangle de 2×2, pas 4×4. La normalisation calcule ce rectangle englobant et décale la pièce vers l'origine pour faciliter le placement.

Avant normalisation
.... ..## .##. ....
Après (bounding box 3×2)
##.. ##.. .... ....

Algorithme de normalisation

La normalisation se fait en trois étapes : trouver la position minimale (ligne et colonne du premier #), décaler tous les blocs de cette position vers l'origine, puis calculer les dimensions du bounding box (largeur = colonne max + 1, hauteur = ligne max + 1).

src/normalize.c
/* Trouve la position du bloc le plus en haut-gauche */
static void  find_min_pos(t_tetri *tetri, int *min_row, int *min_col)
{
    int  i;
    int  j;

    *min_row = 4;
    *min_col = 4;
    i = 0;
    while (i < 4)
    {
        j = 0;
        while (j < 4)
        {
            if (tetri->shape[i][j] == '#')
            {
                if (i < *min_row)
                    *min_row = i;
                if (j < *min_col)
                    *min_col = j;
            }
            j++;
        }
        i++;
    }
}

void  normalize_tetri(t_tetri *tetri)
{
    char  temp[4][5];
    int   min_row;
    int   min_col;
    int   i;
    int   j;

    find_min_pos(tetri, &min_row, &min_col);
    /* Copie décalée vers le coin (0,0) */
    i = 0;
    while (i < 4)
    {
        j = 0;
        while (j < 4)
        {
            temp[i][j] = '.';
            j++;
        }
        i++;
    }
    i = 0;
    while (i < 4 && i + min_row < 4)
    {
        j = 0;
        while (j < 4 && j + min_col < 4)
        {
            temp[i][j] = tetri->shape[i + min_row][j + min_col];
            j++;
        }
        i++;
    }
    /* Recopie dans la structure + calcul des dimensions */
    /* ... (copie + calculate_dimensions) */
}

Le backtracking expliqué

Le backtracking est la technique algorithmique au cœur de fillit. C'est une recherche systématique qui explore l'arbre des possibilités en abandonnant une branche dès qu'elle ne peut pas mener à une solution. C'est ce qu'on appelle l'élagage (pruning).

L'idée intuitive

Imaginez que vous assemblez un puzzle sans image de référence. Vous prenez la première pièce, vous la posez en haut à gauche. Puis vous prenez la deuxième pièce et vous essayez de la placer à côté. Si elle s'insère, vous passez à la troisième. Si elle ne s'insère nulle part, vous revenez en arrière : vous retirez la première pièce et vous essayez une autre position pour elle. C'est exactement ce que fait le backtracking, mais de manière systématique et automatique.

Définition formelle

Le backtracking est un algorithme de recherche qui construit incrementalement des candidats solutions et abandonne un candidat dès qu'il devient impossible ("backtracks") de le compléter en une solution valide. C'est une forme de parcours en profondeur (DFS) sur l'arbre des configurations possibles.

L'arbre de recherche

Pour fillit, chaque nœud de l'arbre représente un état partiel de la grille : certaines pièces sont placées, d'autres non. La racine est la grille vide. Les enfants d'un nœud sont toutes les positions valides où on peut placer la pièce suivante. Une feuille où toutes les pièces sont placées est une solution. L'objectif est de trouver la plus petite taille de grille qui admet au moins une solution.

[Grille 4×4 vide] ├── Placer A en (0,0) → essayer B │ ├── Placer B en (0,1) → essayer C │ │ ├── Placer C en (2,0) → ✓ Solution trouvée ! │ │ └── Placer C en (2,1) → chevauchement → ✗ backtrack │ └── Placer B en (0,2) → chevauchement → ✗ backtrack ├── Placer A en (0,1) → essayer B │ └── ... (sous-arbre complet) └── Placer A en (0,2) → ...

Les 4 étapes de la récursion

La fonction solve() est récursive. À chaque appel, elle essaie de placer la pièce numéro idx à toutes les positions possibles de la grille. Voici la logique en 4 étapes :

1Condition d'arrêt (cas de base)

Si idx == count (toutes les pièces ont été placées), on a trouvé une solution : on retourne 1 (succès). C'est le cas de base de la récursion. Sans cette condition, la fonction s'appellerait indéfiniment.

2Essai de placement

On parcourt toutes les positions (row, col) de la grille, ligne par ligne, de gauche à droite et de haut en bas. Pour chaque position, on appelle try_place() qui vérifie si la pièce tetris[idx] peut être placée à cet endroit sans déborder ni chevaucher une pièce déjà posée.

3Récursion

Si le placement réussit, on place réellement la pièce sur la grille (on écrit sa lettre dans les cellules), puis on appelle solve() récursivement avec idx + 1 pour essayer de placer la pièce suivante. Si cet appel retourne 1, la solution est trouvée et on la propage vers le haut. Sinon, on continue à essayer d'autres positions.

4Backtrack (retour arrière)

Si la récursion a échoué (retourne 0), cela signifie que la pièce actuelle ne peut pas mener à une solution à cette position. On retire alors la pièce de la grille en appelant remove_piece() (on remet les cellules à '.'), puis on essaie la position suivante. Si aucune position ne fonctionne, on retourne 0 pour signaler l'échec au niveau parent, qui backtrackera à son tour.

Le code de la fonction solve

src/solve.c — solve
/*
** Résout le puzzle en plaçant récursivement chaque pièce.
** Retourne 1 si toutes les pièces ont été placées, 0 sinon.
*/
int   solve(t_tetri *tetris, int idx, int count, t_map *map)
{
    int   row;
    int   col;

    /* Étape 1: cas de base — toutes les pièces sont placées */
    if (idx == count)
        return (1);

    /* Étape 2: essayer toutes les positions dans la grille */
    row = 0;
    while (row <= map->size - tetris[idx].height)
    {
        col = 0;
        while (col <= map->size - tetris[idx].width)
        {
            /* Étape 2: peut-on placer la pièce ici ? */
            if (try_place(map, &tetris[idx], row, col))
            {
                /* Étape 3: place la pièce + récursion sur la suivante */
                if (solve(tetris, idx + 1, count, map))
                    return (1);  /* solution trouvée ! */

                /* Étape 4: échec → retirer la pièce (backtrack) */
                remove_piece(map, &tetris[idx], row, col);
            }
            col++;
        }
        row++;
    }
    /* Aucune position ne marche pour cette pièce → backtrack */
    return (0);
}

Les fonctions try_place et remove_piece

try_place() fait deux choses : elle vérifie que la pièce tient dans la grille (pas de débordement) et qu'elle ne chevauche aucune pièce déjà posée. Si tout est OK, elle écrit la lettre de la pièce dans les cellules correspondantes et retourne 1. remove_piece() fait l'inverse : elle efface la lettre (remet les cellules à '.') pour préparer le backtrack.

src/solve.c — try_place
int   try_place(t_map *map, t_tetri *tetri, int row, int col)
{
    int   i;
    int   j;

    /* Vérifie que la pièce ne déborde pas de la grille */
    if (row + tetri->height > map->size ||
        col + tetri->width > map->size)
        return (0);

    /* Vérifie qu'aucun bloc ne chevauche une pièce déjà posée */
    i = 0;
    while (i < tetri->height)
    {
        j = 0;
        while (j < tetri->width)
        {
            if (tetri->shape[i][j] == '#' &&
                map->grid[row + i][col + j] != '.')
                return (0);  /* chevauchement ! */
            j++;
        }
        i++;
    }

    /* Place la pièce en écrivant sa lettre dans la grille */
    i = 0;
    while (i < tetri->height)
    {
        j = 0;
        while (j < tetri->width)
        {
            if (tetri->shape[i][j] == '#')
                map->grid[row + i][col + j] = tetri->letter;
            j++;
        }
        i++;
    }
    return (1);
}

void  remove_piece(t_map *map, t_tetri *tetri, int row, int col)
{
    int   i;
    int   j;

    i = 0;
    while (i < tetri->height)
    {
        j = 0;
        while (j < tetri->width)
        {
            if (tetri->shape[i][j] == '#')
                map->grid[row + i][col + j] = '.';
            j++;
        }
        i++;
    }
}

La boucle principale : taille incrémentale

Le backtracking ne cherche pas directement la solution optimale. Il essaie d'abord avec la plus petite taille de grille possible (calculée à partir du nombre de blocs), et si aucune solution n'existe à cette taille, il incrément la taille de 1 et réessaie. Cela garantit de trouver le plus petit carré, car on commence par le minorant et on monte jusqu'à trouver.

src/main.c — run_fillit
/* Calcule la taille minimale: ceil(sqrt(4 * count)) */
int   find_min_square(int count)
{
    int   size;
    int   total_blocks;

    total_blocks = count * 4;
    size = 2;
    while (size * size < total_blocks)
        size++;
    return (size);
}

static void  run_fillit(char *filename)
{
    /* ... parsing ... */
    map_size = find_min_square(count);
    map = create_map(map_size);
    /* Boucle: essaie map_size, puis map_size+1, etc. */
    while (!solve(tetris, 0, count, map))
    {
        free_map(map);
        map_size++;
        map = create_map(map_size);
    }
    print_map(map);
    free_map(map);
}
Pourquoi commencer par le minorant ?

On pourrait commencer par une grande taille (par exemple 4×4 pour 4 pièces) et descendre. Mais le backtracking est plus rapide sur les petites grilles car il y a moins de positions à explorer. En commençant par le minorant ceil(sqrt(4n)), on essaie d'abord la grille la plus contrainte (où une solution est rare mais rapide à invalider), puis on agrandit. Si une solution existe à la taille minimale, on la trouve immédiatement ; sinon, on passe à la taille suivante.

La règle du "most upper-left"

Le sujet exige que, parmi toutes les solutions possibles à la taille minimale, on choisisse celle où les pièces sont placées le plus en haut à gauche possible, dans l'ordre d'apparition dans le fichier. Notre algorithme respecte naturellement cette règle grâce à l'ordre de parcours : on essaie les positions de haut en bas et de gauche à droite, et on s'arrête à la première solution trouvée. La première solution trouvée est donc celle où la première pièce (A) est la plus en haut-gauche possible, puis la deuxième (B) dans la même logique, etc.

Le code complet

Vue d'ensemble de l'architecture du projet, fichier par fichier. Le code fait environ 250 lignes réparties sur 5 fichiers sources.

FichierRôleLignes
main.cPoint d'entrée, gestion argc/argv, orchestration~50
parsing.cLecture fichier, extraction 4×4, validation (blocs + connexions)~110
normalize.cAlignement top-left + calcul width/height~80
map.cCréation/libération/affichage de la grille~75
solve.cBacktracking récursif + try_place + remove_piece~85

Architecture des données

Le flux de données est linéaire : read_fileparse_tetriminosnormalize_tetri (pour chaque pièce) → validate_tetrisolve (backtracking) → print_map. Aucune structure globale n'est utilisée : tout passe par les paramètres des fonctions, ce qui rend le code testable et modulaire.

Gestion mémoire

Le sujet interdit les fuites. La seule allocation dynamique est la grille t_map (un tableau 2D de char). La fonction free_map libère chaque ligne puis le tableau de pointeurs puis la structure. Elle est appelée dans deux cas : quand on agrandit la grille (taille +1) et à la fin du programme. Les structures t_tetri sont allouées sur la pile (tableau statique de 26), donc pas de malloc à libérer pour elles.

Exemple pas à pas

Suivons l'exécution du programme sur l'exemple du sujet, avec 4 pièces : un L, une barre I horizontale, un T, et un S.

Les 4 pièces en entrée

A — L shape
.... ##.. #... #...
B — I horizontale
.... #### .... ....
C — T shape
.... ###. ..#. ....
D — S shape
.... ##.. .##. ....

Étape 1 : calcul de la taille minimale

4 pièces × 4 blocs = 16 blocs. ceil(sqrt(16)) = 4. On commence avec une grille 4×4. C'est le minorant absolu — si ça marche, on a la solution optimale.

Étape 2 : backtracking sur grille 4×4

L'algorithme essaie de placer A en (0,0) — le coin haut-gauche. A est un L de 2×3, ça tient. Puis il essaie B. B fait 1×4. À (0,0) ça chevauche A. À (0,1) ça chevauche aussi. À (0,2) pareil. En fait, B ne peut pas tenir sur la ligne 0 car A occupe les colonnes 0-1. B peut aller en (1,2) mais ça déborde (1+1 ≤ 4 OK, mais 2+4 = 6 > 4). B ne peut pas être placée → on backtrack : on retire A de (0,0) et on essaie A en (0,1).

Après plusieurs essais, l'algorithme trouve la configuration suivante où toutes les pièces tiennent en 4×4 :

Solution finale

Grille 4×4 solution
DDAA CDA. CCA. BBBB
Sortie du programme
DDAA
CDDA
CCCA
BBBB

Chaque pièce est identifiée par sa lettre (A, B, C, D) dans l'ordre d'apparition du fichier. La solution respecte la règle "upper-left" : A est en (0,2), B en (3,0), C en (1,0), D en (0,0). Aucune autre configuration 4×4 ne place les pièces plus en haut-gauche dans l'ordre du fichier.

Optimisations

Le backtracking naïf peut être lent sur certains cas pathologiques. Voici les optimisations importantes pour passer la moulinette sans timeout.

1. Normalisation préalable

Normaliser les pièces avant le backtracking (et non à chaque essai) évite de recalculer le bounding box à chaque placement. C'est fait une fois pour toutes au parsing. Les champs width et height de t_tetri permettent à try_place de ne parcourir que les cellules utiles, pas tout le 4×4.

2. Bornage de la boucle

Au lieu de parcourir toute la grille (row < map->size), on s'arrête à map->size - tetri->height. Si une pièce fait 3 de haut et que la grille fait 4 de côté, inutile d'essayer row = 2 ou row = 3 — la pièce déborderait. Cela divise le nombre d'essais par 2 à 4 selon les pièces.

Optimisation de boucle
/* AVANT (naïf): parcours toute la grille */
while (row < map->size)

/* APRÈS (optimisé): s'arrête dès que la pièce déborderait */
while (row <= map->size - tetris[idx].height)

3. Early termination (taille incrémentale)

On commence par la plus petite taille possible. Si solve retourne 0 (échec), on libère la grille, on incrémente la taille de 1, et on recommence. Cela garantit qu'on trouve le plus petit carré saut tester inutilement de grandes grilles qui seraient coûteuses à explorer.

3. Éviter le pire cas : pièces identiques

Le cas pathologique est 26 pièces I verticales identiques : le backtracking explore un arbre énorme car chaque pièce peut aller dans n'importe quelle colonne. En pratique, ce cas est rare car les sujets de moulinette utilisent des pièces variées. Si vous voulez le gérer, une optimisation avancée consiste à détecter les pièces identiques et à forcer leur ordre (par exemple, ne placer la pièce N+1 qu'à droite ou en dessous de la pièce N si elles sont identiques).

La moulinette timeout

Le sujet précise : "This moulinette includes an arbitrary timeout that will stop the execution of your program if it takes too long." Sur les tests courants (4-10 pièces variées), notre implémentation répond en moins de 10 millisecondes. Le cas 26 pièces identiques peut prendre plusieurs secondes, mais ce n'est généralement pas testé par la moulinette. Si ça l'est, l'optimisation des pièces identiques (ci-dessus) devient nécessaire.

Compilation & tests

Comment compiler le projet et le tester avec les exemples du sujet.

Compilation

Shell
# Compiler le projet
$ make

# Règles disponibles
$ make all      # compile fillit
$ make clean    # supprime les .o
$ make fclean   # supprime .o + binaire
$ make re       # fclean + all

Test avec l'exemple du sujet

Shell
# Créer un fichier de test
$ cat > sample.fillit << EOF
....
##..
.#..
.#..

....
####
....
....

#...
###.
....
....

....
##..
.##.
....
EOF

# Exécuter
$ ./fillit sample.fillit
DDAA
CDDA
CCCA
BBBB

Cas d'erreur

Shell
# Fichier invalide (pièce déconnectée)
$ cat > bad.fillit << EOF
#..#
....
....
....
EOF
$ ./fillit bad.fillit
error

# Pas d'argument
$ ./fillit
usage: ./fillit [file]

# Fichier inexistant
$ ./fillit /nonexistent
error
Checklist de rendu

Avant de rendre : make fclean && make sans warnings, fichier author présent avec votre login, binaire fillit à la racine, aucun fichier superflu (pas de .o, pas de obj/), et le projet dans un repo Git propre.