Semana 10 — Recursión en árbol

Objetivos

  • Reconocer problemas donde cada llamada hace varias llamadas.
  • Recorrer una cuadrícula de forma recursiva sin volver a visitar celdas.
  • Explicar por qué algunas recursiones explotan y cómo la memoización lo evita.

Teoría — Recursión en árbol: flood fill, cuadrículas · ✂️ Memoización

Lectura: Apuntes, semana 10.

Temas:

  • Fibonacci y el árbol de llamadas. Contar llamadas de verdad.
  • Flood fill: pintar una región de una imagen. El estado “visitado” y por qué sin él no termina.
  • Recursión sobre cuadrículas: contar regiones y encontrar un camino en un laberinto.
  • Profundidad de la recursión en una cuadrícula grande, y cuándo usar una pila propia.
  • ✂️ Memoización: guardar resultados ya calculados. Comparar llamadas con y sin ella.

Laboratorio — Buscaminas

Se construye un Buscaminas de terminal sobre código dado, con un tablero de tamaño variable en memoria dinámica.

Sesión 1:

  1. Leer el código dado: el tablero, la impresión y las pruebas.
  2. Escribir flood_fill para las imágenes PGM, y count_regions.
  3. Escribir board_new y board_free, y contar las minas vecinas de cada celda.

Sesión 2: 4. Escribir board_reveal: al abrir una celda sin minas vecinas, se abren de forma recursiva sus vecinas. 5. Detectar victoria y derrota. Las pruebas usan tableros fijos, con una semilla conocida. 6. Probar un tablero de 1000 × 1000 sin minas y explicar qué ocurre.

Preguntas de repaso

  1. Dibujen el árbol de llamadas de fib(5). ¿Cuántas llamadas hay?
  2. ¿Por qué flood_fill sin marca de visitado no termina?
  3. ¿Cuántos marcos puede tener apilados flood_fill en una imagen de N × N, en el peor caso?
  4. ¿Qué tiene que cumplir una función para que se pueda memoizar?

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (9)Errores (2)Pruebas (3)Laboratorio (17)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

count_islands.cDescargar count_islands.c
// Cuenta las islas de un mapa: grupos de # unidos por arriba, abajo, izquierda o derecha.
#include <stdio.h>

#define ROWS 6
#define COLS 14

// Hunde toda la isla que contiene a (row, col): cada # visitado pasa a ser ~.
void sink(char map[][COLS + 1], int row, int col) {
    if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
        return;
    }
    if (map[row][col] != '#') {
        return;
    }
    map[row][col] = '~';
    sink(map, row - 1, col);
    sink(map, row + 1, col);
    sink(map, row, col - 1);
    sink(map, row, col + 1);
}

// Cuidado: destruye el mapa que recibe. Al terminar, ya no queda ningún #.
int count_islands(char map[][COLS + 1]) {
    int islands = 0;
    for (int row = 0; row < ROWS; row++) {
        for (int col = 0; col < COLS; col++) {
            if (map[row][col] == '#') {
                islands++;
                sink(map, row, col);
            }
        }
    }
    return islands;
}

int main(void) {
    char map[ROWS][COLS + 1] = {
        "##......#.....",
        "##.....###..#.",
        "........#...#.",
        "...#........#.",
        "....#...##....",
        ".........##...",
    };

    for (int row = 0; row < ROWS; row++) {
        printf("%s\n", map[row]);
    }
    printf("islas: %d\n", count_islands(map));
    return 0;
}
deep_fill.cDescargar deep_fill.c
// Flood fill recursivo sobre una cuadrícula de N x N vacía: mide cuántos marcos se apilan.
// Uso: deep_fill N
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int size;
    char *cells;
    long long painted;
    long long depth;
    long long max_depth;
} Canvas;

void fill(Canvas *canvas, int row, int col) {
    canvas->depth++;
    if (canvas->depth > canvas->max_depth) {
        canvas->max_depth = canvas->depth;
    }
    int size = canvas->size;
    if (row >= 0 && row < size && col >= 0 && col < size && canvas->cells[row * size + col] == 0) {
        canvas->cells[row * size + col] = 1;
        canvas->painted++;
        fill(canvas, row - 1, col);
        fill(canvas, row + 1, col);
        fill(canvas, row, col - 1);
        fill(canvas, row, col + 1);
    }
    canvas->depth--;
}

int main(int argc, char *argv[]) {
    int size = 0;
    char extra = 0;
    if (argc != 2 || sscanf(argv[1], "%5d%c", &size, &extra) != 1 || size < 1) {
        printf("uso: deep_fill N, con N de 1 a 99999\n");
        return 1;
    }

    Canvas canvas = {size, calloc((size_t) size * size, 1), 0, 0, 0};
    if (canvas.cells == NULL) {
        printf("no hay memoria\n");
        return 1;
    }

    fill(&canvas, 0, 0);
    printf("N = %d: %lld celdas pintadas, %lld marcos apilados como máximo\n",
           size, canvas.painted, canvas.max_depth);
    free(canvas.cells);
    return 0;
}
fib_count.cDescargar fib_count.c
// Fibonacci recursivo que cuenta sus llamadas. El contador llega por puntero: no hay globales.
#include <stdio.h>

long long fib(int n, long long *calls) {
    (*calls)++;
    if (n < 2) {
        return n;
    }
    return fib(n - 1, calls) + fib(n - 2, calls);
}

int main(void) {
    const int sizes[] = {1, 2, 3, 4, 5, 10, 20, 30, 35, 36, 37, 40};
    int count = (int) (sizeof(sizes) / sizeof(sizes[0]));

    printf("%4s %12s %12s\n", "n", "fib(n)", "llamadas");
    for (int i = 0; i < count; i++) {
        long long calls = 0;
        long long value = fib(sizes[i], &calls);
        printf("%4d %12lld %12lld\n", sizes[i], value, calls);
    }
    return 0;
}
fib_memo.cDescargar fib_memo.c
// Fibonacci con memoización: compara las llamadas con y sin la tabla de resultados.
#include <stdio.h>
#include <stdlib.h>

#define NOT_KNOWN (-1)

long long fib(int n, long long *calls) {
    (*calls)++;
    if (n < 2) {
        return n;
    }
    return fib(n - 1, calls) + fib(n - 2, calls);
}

// memo[n] guarda fib(n), o NOT_KNOWN si todavía no se calculó.
long long fib_memo(int n, long long memo[], long long *calls) {
    (*calls)++;
    if (n < 2) {
        return n;
    }
    if (memo[n] != NOT_KNOWN) {
        return memo[n];
    }
    memo[n] = fib_memo(n - 1, memo, calls) + fib_memo(n - 2, memo, calls);
    return memo[n];
}

// Calcula fib(n) con una tabla nueva. Devuelve -1 si no hay memoria.
long long fib_with_table(int n, long long *calls) {
    long long *memo = malloc((n + 1) * sizeof(long long));
    if (memo == NULL) {
        return -1;
    }
    for (int i = 0; i <= n; i++) {
        memo[i] = NOT_KNOWN;
    }
    long long value = fib_memo(n, memo, calls);
    free(memo);
    return value;
}

int main(void) {
    const int sizes[] = {5, 10, 20, 30, 35};
    int count = (int) (sizeof(sizes) / sizeof(sizes[0]));

    printf("%4s %12s %12s %12s\n", "n", "fib(n)", "sin tabla", "con tabla");
    for (int i = 0; i < count; i++) {
        long long plain_calls = 0;
        long long memo_calls = 0;
        long long value = fib(sizes[i], &plain_calls);
        if (fib_with_table(sizes[i], &memo_calls) != value) {
            printf("no hay memoria o los resultados no coinciden\n");
            return 1;
        }
        printf("%4d %12lld %12lld %12lld\n", sizes[i], value, plain_calls, memo_calls);
    }

    long long calls = 0;
    long long value = fib_with_table(90, &calls);
    if (value < 0) {
        printf("no hay memoria\n");
        return 1;
    }
    printf("fib(90) = %lld, con %lld llamadas\n", value, calls);
    return 0;
}
fib_trace.cDescargar fib_trace.c
// Escribe el árbol de llamadas de fib: cada nivel de sangría es un marco más en la pila.
#include <stdio.h>

int fib(int n, int depth) {
    printf("%*sfib(%d)\n", depth * 4, "", n);
    if (n < 2) {
        return n;
    }
    return fib(n - 1, depth + 1) + fib(n - 2, depth + 1);
}

int main(void) {
    int value = fib(4, 0);
    printf("resultado: %d\n", value);
    return 0;
}
fill_stack.cDescargar fill_stack.c
// El mismo flood fill de deep_fill.c, sin recursión: las celdas pendientes van en una pila propia.
// Uso: fill_stack N
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int row;
    int col;
} Cell;

// Pinta (row, col) y la apila, si está dentro y sin pintar. Cada celda se apila una sola vez,
// así que la pila nunca pasa de size * size celdas.
void push_if_free(char *cells, int size, Cell *stack, long long *top, int row, int col) {
    if (row >= 0 && row < size && col >= 0 && col < size && cells[row * size + col] == 0) {
        cells[row * size + col] = 1;
        stack[*top].row = row;
        stack[*top].col = col;
        (*top)++;
    }
}

int main(int argc, char *argv[]) {
    int size = 0;
    char extra = 0;
    if (argc != 2 || sscanf(argv[1], "%5d%c", &size, &extra) != 1 || size < 1) {
        printf("uso: fill_stack N, con N de 1 a 99999\n");
        return 1;
    }

    size_t total = (size_t) size * size;
    char *cells = calloc(total, 1);
    Cell *stack = malloc(total * sizeof(Cell));
    if (cells == NULL || stack == NULL) {
        printf("no hay memoria\n");
        free(cells);
        free(stack);
        return 1;
    }

    long long top = 0;
    long long max_top = 0;
    long long painted = 0;
    push_if_free(cells, size, stack, &top, 0, 0);
    while (top > 0) {
        if (top > max_top) {
            max_top = top;
        }
        top--;
        Cell cell = stack[top];
        painted++;
        push_if_free(cells, size, stack, &top, cell.row - 1, cell.col);
        push_if_free(cells, size, stack, &top, cell.row + 1, cell.col);
        push_if_free(cells, size, stack, &top, cell.row, cell.col - 1);
        push_if_free(cells, size, stack, &top, cell.row, cell.col + 1);
    }

    printf("N = %d: %lld celdas pintadas, %lld pendientes como máximo\n", size, painted, max_top);
    free(cells);
    free(stack);
    return 0;
}
fill_trace.cDescargar fill_trace.c
// El flood fill de una cuadrícula pequeña, con un rastro de cada llamada que pinta.
#include <stdio.h>

#define ROWS 3
#define COLS 3

typedef struct {
    int calls;
    int painted;
} Tally;

void fill(char grid[][COLS + 1], int row, int col, int depth, Tally *tally) {
    tally->calls++;
    if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
        return;
    }
    if (grid[row][col] != '.') {
        return;
    }
    grid[row][col] = 'o';
    tally->painted++;
    printf("%*spinta (%d, %d)\n", depth * 2, "", row, col);
    fill(grid, row - 1, col, depth + 1, tally);
    fill(grid, row + 1, col, depth + 1, tally);
    fill(grid, row, col - 1, depth + 1, tally);
    fill(grid, row, col + 1, depth + 1, tally);
}

int main(void) {
    char grid[ROWS][COLS + 1] = {
        "..#",
        ".##",
        "..#",
    };
    Tally tally = {0, 0};

    fill(grid, 0, 1, 0, &tally);
    for (int row = 0; row < ROWS; row++) {
        printf("%s\n", grid[row]);
    }
    printf("%d celdas pintadas, %d llamadas\n", tally.painted, tally.calls);
    return 0;
}
grid_fill.cDescargar grid_fill.c
// Flood fill sobre una cuadrícula de caracteres: se pinta la región que contiene una celda.
#include <stdio.h>

#define ROWS 6
#define COLS 12

void print_grid(char grid[][COLS + 1]) {
    for (int row = 0; row < ROWS; row++) {
        printf("%s\n", grid[row]);
    }
}

// Pinta con new_mark la región de celdas old_mark que contiene a (row, col).
// Exige: new_mark != old_mark.
void fill(char grid[][COLS + 1], int row, int col, char old_mark, char new_mark) {
    if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
        return;
    }
    if (grid[row][col] != old_mark) {
        return;
    }
    grid[row][col] = new_mark;
    fill(grid, row - 1, col, old_mark, new_mark);
    fill(grid, row + 1, col, old_mark, new_mark);
    fill(grid, row, col - 1, old_mark, new_mark);
    fill(grid, row, col + 1, old_mark, new_mark);
}

int main(void) {
    char grid[ROWS][COLS + 1] = {
        "....#.......",
        "....#..###..",
        "#####..#.#..",
        "..#....###..",
        "..#.........",
        "..#.........",
    };

    print_grid(grid);
    printf("\n");
    fill(grid, 4, 8, '.', 'o');
    print_grid(grid);
    return 0;
}
maze.cDescargar maze.c
// Busca un camino en un laberinto: # es pared, E es la salida.
// Al terminar, + marca el camino encontrado y x las celdas de donde hubo que volver.
#include <stdio.h>

#define ROWS 7
#define COLS 11

void print_maze(char maze[][COLS + 1]) {
    for (int row = 0; row < ROWS; row++) {
        printf("%s\n", maze[row]);
    }
}

// Devuelve 1 si desde (row, col) se puede llegar a la salida.
int find_path(char maze[][COLS + 1], int row, int col) {
    if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
        return 0;
    }
    if (maze[row][col] == 'E') {
        return 1;
    }
    if (maze[row][col] != ' ') {
        return 0;
    }
    maze[row][col] = '+';
    if (find_path(maze, row - 1, col) || find_path(maze, row + 1, col)
        || find_path(maze, row, col - 1) || find_path(maze, row, col + 1)) {
        return 1;
    }
    maze[row][col] = 'x';
    return 0;
}

int main(void) {
    char open_maze[ROWS][COLS + 1] = {
        "###########",
        "#   #     #",
        "# # # ### #",
        "# #   #   #",
        "# ##### # #",
        "#       #E#",
        "###########",
    };
    char closed_maze[ROWS][COLS + 1] = {
        "###########",
        "#   #     #",
        "# # # ### #",
        "# #   #   #",
        "# #########",
        "#       #E#",
        "###########",
    };

    printf("hay camino: %d\n", find_path(open_maze, 1, 1));
    print_maze(open_maze);
    printf("hay camino: %d\n", find_path(closed_maze, 1, 1));
    print_maze(closed_maze);
    return 0;
}

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

errors/fill_no_mark.cDescargar errors/fill_no_mark.c
// Tiene un error a propósito: pinta la celda después de las llamadas, no antes.
#include <stdio.h>

#define ROWS 3
#define COLS 3

void fill(char grid[][COLS + 1], int row, int col) {
    if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
        return;
    }
    if (grid[row][col] != '.') {
        return;
    }
    fill(grid, row - 1, col);
    fill(grid, row + 1, col);
    fill(grid, row, col - 1);
    fill(grid, row, col + 1);
    grid[row][col] = 'o';
}

int main(void) {
    char grid[ROWS][COLS + 1] = {
        "...",
        ".#.",
        "...",
    };

    fill(grid, 0, 0);
    printf("%s\n%s\n%s\n", grid[0], grid[1], grid[2]);
    return 0;
}
errors/fill_same_mark.cDescargar errors/fill_same_mark.c
// Tiene un error a propósito: pinta del mismo color que ya tenía la región.
#include <stdio.h>

#define ROWS 3
#define COLS 3

void fill(char grid[][COLS + 1], int row, int col, char old_mark, char new_mark) {
    if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
        return;
    }
    if (grid[row][col] != old_mark) {
        return;
    }
    grid[row][col] = new_mark;
    fill(grid, row - 1, col, old_mark, new_mark);
    fill(grid, row + 1, col, old_mark, new_mark);
    fill(grid, row, col - 1, old_mark, new_mark);
    fill(grid, row, col + 1, old_mark, new_mark);
}

int main(void) {
    char grid[ROWS][COLS + 1] = {
        "...",
        ".#.",
        "...",
    };

    fill(grid, 0, 0, grid[0][0], '.');
    printf("%s\n%s\n%s\n", grid[0], grid[1], grid[2]);
    return 0;
}

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

test_examples.pyDescargar test_examples.py
"""Comprueba los programas de la clase y lo que los apuntes dicen de ellos."""
import pathlib
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
SOLUTION = BUILD / "solution"
BASE = ["-Wall", "-Wextra", "-std=c17"]
FLAGS = [*BASE, "-g", "-fsanitize=address,undefined"]
PROGRAMS = ["fib_count", "fib_trace", "fib_memo", "grid_fill", "fill_trace", "count_islands", "maze",
            "deep_fill", "fill_stack"]
# El mismo tamaño de pila que tiene el entorno del curso, para no depender de cómo se abrió la terminal.
STACK_KB = 8192


def run(program, *args):
    done = subprocess.run(
        [str(program), *args], cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return done.stdout, done.returncode


def run_with_stack(kilobytes, program, *args):
    """Corre el programa con esa pila. Devuelve (salida, código de salida de bash)."""
    command = f"ulimit -s {kilobytes} && exec {program} " + " ".join(args)
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return done.stdout, done.returncode


def can_raise_stack(kilobytes):
    done = subprocess.run(["bash", "-c", f"ulimit -s {kilobytes}"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    return done.returncode == 0


def rows_of(text):
    return [line.split() for line in text.splitlines()[1:]]


class Programs(unittest.TestCase):
    def test_every_program_compiles_without_warnings(self):
        for name in PROGRAMS:
            with self.subTest(program=name):
                subprocess.run(["gcc", *FLAGS, "-Werror", "-c", f"{name}.c", "-o", "/dev/null"], cwd=HERE, check=True,
                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    def test_errors_compile_without_warnings(self):
        # Los errores de esta semana no los ve el compilador: aparecen al correr.
        for name in ("fill_no_mark", "fill_same_mark"):
            with self.subTest(program=name):
                subprocess.run(["gcc", *FLAGS, "-Werror", "-c", f"errors/{name}.c", "-o", "/dev/null"], cwd=HERE,
                               check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    def test_the_lab_compiles_without_warnings(self):
        for name in ("image", "regions", "board", "fill_demo", "minesweeper", "big_board", "regions_tests", "board_tests"):
            with self.subTest(file=name):
                subprocess.run(["gcc", *FLAGS, "-Werror", "-Ilab", "-c", f"lab/{name}.c", "-o", "/dev/null"], cwd=HERE,
                               check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    def test_no_program_leaks(self):
        for name in PROGRAMS:
            args = ("20",) if name in ("deep_fill", "fill_stack") else ()
            with self.subTest(program=name):
                out, code = run(BUILD / name, *args)
                self.assertEqual(code, 0, out)
                self.assertNotIn("LeakSanitizer", out)


class Fibonacci(unittest.TestCase):
    def test_calls_follow_the_formula(self):
        # llamadas(n) = 2 * fib(n + 1) - 1
        fib = [0, 1]
        for _ in range(50):
            fib.append(fib[-1] + fib[-2])
        out, _ = run(BUILD / "fib_count")
        for n, value, calls in rows_of(out):
            with self.subTest(n=n):
                self.assertEqual(int(value), fib[int(n)])
                self.assertEqual(int(calls), 2 * fib[int(n) + 1] - 1)

    def test_fib_5_makes_15_calls(self):
        out, _ = run(BUILD / "fib_count")
        self.assertIn(["5", "5", "15"], rows_of(out))

    def test_trace_has_one_line_per_call(self):
        out, _ = run(BUILD / "fib_trace")
        self.assertEqual(sum(line.strip().startswith("fib(") for line in out.splitlines()), 9)

    def test_memo_calls_are_2n_minus_1(self):
        out, _ = run(BUILD / "fib_memo")
        for row in rows_of(out):
            if len(row) == 4:
                n, _, plain, memo = row
                with self.subTest(n=n):
                    self.assertEqual(int(memo), 2 * int(n) - 1)
                    self.assertGreaterEqual(int(plain), int(memo))
        self.assertIn("fib(90) = 2880067194370816120, con 179 llamadas", out)


class Depth(unittest.TestCase):
    def test_the_depth_is_n_squared_plus_one(self):
        for size in (1, 3, 10, 100):
            with self.subTest(size=size):
                out, code = run(BUILD / "deep_fill", str(size))
                self.assertEqual(code, 0, out)
                self.assertIn(f"{size * size} celdas pintadas, {size * size + 1} marcos", out)

    def test_recursion_breaks_at_1000_with_8_mb(self):
        # Vale en cualquier arquitectura: un millón de marcos no cabe en 8 MB.
        for name in ("deep_fill", "deep_fill_plain", "deep_fill_o2"):
            with self.subTest(program=name):
                out, code = run_with_stack(STACK_KB, BUILD / name, "1000")
                self.assertNotEqual(code, 0, out)
                self.assertNotIn("celdas pintadas", out)

    def test_asan_names_the_stack_overflow(self):
        # En x86-64 emulado, ASan a veces cae sin escribir el informe: eso se acepta como caída.
        out, code = run_with_stack(STACK_KB, BUILD / "deep_fill", "1000")
        self.assertNotEqual(code, 0)
        if code == 1:
            self.assertIn("stack-overflow", out)
            self.assertIn("in fill", out)

    def test_plain_build_dies_with_a_signal(self):
        out, code = run_with_stack(STACK_KB, BUILD / "deep_fill_plain", "1000")
        self.assertEqual(code, -11, out)

    @unittest.skipUnless(can_raise_stack(200000), "este sistema no deja agrandar la pila")
    def test_with_more_stack_it_works(self):
        out, code = run_with_stack(200000, BUILD / "deep_fill_plain", "1000")
        self.assertEqual(code, 0, out)
        self.assertIn("1000000 celdas pintadas, 1000001 marcos", out)

    def test_own_stack_needs_no_big_stack(self):
        out, code = run_with_stack(STACK_KB, BUILD / "fill_stack", "1000")
        self.assertEqual(code, 0, out)
        self.assertIn("1000000 celdas pintadas", out)
        self.assertNotIn("LeakSanitizer", out)


class MarkErrors(unittest.TestCase):
    def crash_of(self, name):
        with tempfile.TemporaryDirectory() as folder:
            binary = pathlib.Path(folder) / name
            subprocess.run(["gcc", *FLAGS, f"errors/{name}.c", "-o", str(binary)], cwd=HERE, check=True)
            return run_with_stack(STACK_KB, binary)

    def test_marking_after_the_calls_never_ends(self):
        out, code = self.crash_of("fill_no_mark")
        self.assertNotEqual(code, 0, out)
        if code == 1:
            self.assertIn("stack-overflow", out)

    def test_painting_with_the_same_mark_never_ends(self):
        out, code = self.crash_of("fill_same_mark")
        self.assertNotEqual(code, 0, out)
        if code == 1:
            self.assertIn("stack-overflow", out)


@unittest.skipUnless((HERE / "solution").exists(), "falta la solución")
class BigBoard(unittest.TestCase):
    def test_small_boards_work(self):
        for name in ("big_board", "big_board_plain", "big_board_o2"):
            with self.subTest(program=name):
                out, code = run_with_stack(STACK_KB, SOLUTION / name, "100", "100")
                self.assertEqual(code, 0, out)
                self.assertIn("10000 celdas abiertas, juego ganado", out)

    def test_1000_by_1000_breaks_the_recursive_reveal(self):
        for name in ("big_board", "big_board_plain", "big_board_o2"):
            with self.subTest(program=name):
                out, code = run_with_stack(STACK_KB, SOLUTION / name, "1000", "1000")
                self.assertNotEqual(code, 0, out)
                self.assertNotIn("celdas abiertas", out)

    @unittest.skipUnless(can_raise_stack(400000), "este sistema no deja agrandar la pila")
    def test_with_more_stack_the_big_board_works(self):
        out, code = run_with_stack(400000, SOLUTION / "big_board_plain", "1000", "1000")
        self.assertEqual(code, 0, out)
        self.assertIn("1000000 celdas abiertas, juego ganado", out)


if __name__ == "__main__":
    unittest.main()
test_lab.pyDescargar test_lab.py
"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.

Cada prueba compila un main propio con regions.c o board.c de esa carpeta, por separado de
las pruebas de ustedes. Corren con ASan, así que una fuga también hace fallar la prueba.
"""
import os
import pathlib
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
LAB = HERE / "lab"
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]


def setUpModule():
    if not (SOURCE / "board.c").exists():
        raise unittest.SkipTest(f"no está {LAB_DIR}/board.c")


def compile_and_run(sources, args=(), stdin="", files=None):
    """Devuelve (salida, código de salida) de un programa, en una carpeta temporal."""
    with tempfile.TemporaryDirectory() as name:
        folder = pathlib.Path(name)
        binary = folder / "program"
        built = subprocess.run(
            ["gcc", *FLAGS, f"-I{LAB}", *map(str, sources), "-o", str(binary)],
            text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        if built.returncode != 0:
            raise AssertionError(f"No compila:\n{built.stdout}")
        for file_name, content in (files or {}).items():
            (folder / file_name).write_text(content, encoding="utf-8")
        done = subprocess.run(
            [str(binary), *args], cwd=folder, input=stdin, text=True, encoding="utf-8",
            errors="replace", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
        )
        return done.stdout, done.returncode


def run_driver(includes, body, sources, files=None):
    with tempfile.TemporaryDirectory() as name:
        driver = pathlib.Path(name) / "driver.c"
        driver.write_text(includes + "int main(void) {\n" + body + "\nreturn 0;\n}\n", encoding="utf-8")
        return compile_and_run([driver, *sources], files=files)


IMAGE_HELPERS = """#include <stdio.h>
#include "image.h"
#include "regions.h"
static Image *from_text(const char *text, int width, int height) {
    Image *img = image_new(width, height, IMAGE_MAX_VALUE, 0);
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            image_set(img, x, y, text[y * width + x] - '0');
        }
    }
    return img;
}
static void print_rows(const Image *img) {
    for (int y = 0; y < image_height(img); y++) {
        for (int x = 0; x < image_width(img); x++) {
            printf("%d", image_get(img, x, y));
        }
        printf(" ");
    }
    printf("\\n");
}
"""

BOARD_HELPERS = """#include <stdio.h>
#include "board.h"
static void show(const Board *b) {
    for (int r = 0; r < board_rows(b); r++) {
        for (int c = 0; c < board_cols(b); c++) {
            printf("%c", board_cell_char(b, r, c));
        }
        printf(" ");
    }
    printf("\\n");
}
"""


def c_rows(rows):
    return "const char *rows[] = {" + ", ".join(f'"{row}"' for row in rows) + "};\n" \
        + f"Board *b = board_from_rows(rows, {len(rows)});\n" \
        + 'if (b == NULL) { printf("NULL\\n"); return 0; }\n'


class LabCase(unittest.TestCase):
    longMessage = False

    def check(self, result, wanted, what):
        out, code = result
        if code != 0:
            self.fail(f"{what}: el programa terminó con error.\n{out}")
        got = "\n".join(line.rstrip() for line in out.strip().splitlines())
        if got != wanted:
            self.fail(f"{what}\n  se esperaba: {wanted}\n  se obtuvo:   {got}")

    def fill(self, text, width, height, x, y, value):
        body = (f'Image *img = from_text("{text}", {width}, {height});\n'
                f"flood_fill(img, {x}, {y}, {value});\nprint_rows(img);\nimage_free(img);")
        return run_driver(IMAGE_HELPERS, body, [SOURCE / "regions.c", LAB / "image.c"])

    def regions(self, text, width, height):
        body = (f'Image *img = from_text("{text}", {width}, {height});\n'
                'printf("%d ", count_regions(img));\nprint_rows(img);\nimage_free(img);')
        return run_driver(IMAGE_HELPERS, body, [SOURCE / "regions.c", LAB / "image.c"])

    def board(self, rows, body):
        return run_driver(BOARD_HELPERS, c_rows(rows) + body + "\nboard_free(b);", [SOURCE / "board.c"])


class FloodFill(LabCase):
    def test_paints_only_the_region(self):
        self.check(self.fill("110" "010" "100", 3, 3, 0, 0, 9), "990 090 100",
                   "flood_fill en (0, 0): la celda de abajo a la izquierda solo toca por una diagonal")

    def test_diagonals_do_not_join(self):
        self.check(self.fill("10" "01", 2, 2, 0, 0, 9), "90 01", "flood_fill no pasa por una diagonal")

    def test_starts_anywhere_in_the_region(self):
        self.check(self.fill("110" "010" "100", 3, 3, 1, 1, 9), "990 090 100",
                   "flood_fill en (1, 1) pinta la misma región que en (0, 0)")

    def test_goes_around_corners(self):
        self.check(self.fill("11111" "00001" "11101" "10001" "11111", 5, 5, 0, 0, 7),
                   "77777 00007 77707 70007 77777", "flood_fill en un camino con curvas")

    def test_whole_image(self):
        self.check(self.fill("000000", 3, 2, 2, 1, 5), "555 555", "flood_fill en una imagen de un solo valor")

    def test_same_value_ends_and_changes_nothing(self):
        self.check(self.fill("110" "010", 3, 2, 0, 0, 1), "110 010",
                   "flood_fill con el valor que la región ya tiene: debe terminar sin cambiar nada")

    def test_large_region(self):
        body = ("Image *img = image_new(100, 100, IMAGE_MAX_VALUE, 3);\n"
                "flood_fill(img, 50, 50, 8);\nint painted = 0;\n"
                "for (int y = 0; y < 100; y++) { for (int x = 0; x < 100; x++) {"
                " painted += image_get(img, x, y) == 8; } }\n"
                'printf("%d\\n", painted);\nimage_free(img);')
        self.check(run_driver(IMAGE_HELPERS, body, [SOURCE / "regions.c", LAB / "image.c"]), "10000",
                   "flood_fill en una imagen de 100 x 100")


class CountRegions(LabCase):
    def test_one_region(self):
        self.check(self.regions("000000", 3, 2), "1 000 000", "count_regions con un solo valor")

    def test_single_pixel(self):
        self.check(self.regions("5", 1, 1), "1 5", "count_regions en una imagen de 1 x 1")

    def test_diagonals_do_not_join(self):
        self.check(self.regions("10" "01", 2, 2), "4 10 01", "count_regions: las diagonales no unen")

    def test_checkerboard(self):
        self.check(self.regions("0101" "1010" "0101" "1010", 4, 4), "16 0101 1010 0101 1010",
                   "count_regions en un tablero de ajedrez")

    def test_same_value_far_apart_counts_twice(self):
        self.check(self.regions("10201", 5, 1), "5 10201",
                   "count_regions: dos regiones con el mismo valor son dos regiones")

    def test_region_with_a_hole(self):
        self.check(self.regions("111" "101" "111", 3, 3), "2 111 101 111", "count_regions con un hueco adentro")

    def test_files_of_the_lab(self):
        for name, wanted in (("sun.pgm", 3), ("islands.pgm", 5), ("tiny.pgm", 6)):
            body = (f'Image *img = image_load("{LAB / "images" / name}");\n'
                    'if (img == NULL) { printf("NULL\\n"); return 0; }\n'
                    'printf("%d\\n", count_regions(img));\nimage_free(img);')
            with self.subTest(file=name):
                self.check(run_driver(IMAGE_HELPERS, body, [SOURCE / "regions.c", LAB / "image.c"]),
                           str(wanted), f"count_regions de {name}")

    def test_called_twice_gives_the_same(self):
        body = ('Image *img = from_text("1100", 2, 2);\n'
                'printf("%d %d\\n", count_regions(img), count_regions(img));\nimage_free(img);')
        self.check(run_driver(IMAGE_HELPERS, body, [SOURCE / "regions.c", LAB / "image.c"]), "2 2",
                   "count_regions dos veces: cada llamada empieza sin marcas")


class BoardNew(LabCase):
    def driver(self, body):
        return run_driver(BOARD_HELPERS, body, [SOURCE / "board.c"])

    def test_new_board(self):
        body = ('Board *b = board_new(2, 3);\nif (b == NULL) { printf("NULL\\n"); return 0; }\n'
                'printf("%d %d %d %d ", board_rows(b), board_cols(b), board_mines(b), board_opened(b));\n'
                "show(b);\nboard_free(b);")
        self.check(self.driver(body), "2 3 0 0 ### ###", "board_new(2, 3)")

    def test_bad_sizes_return_null(self):
        for rows, cols in ((0, 3), (3, 0), (-1, 3), (3, -1), (100000, 100000)):
            body = (f"Board *b = board_new({rows}, {cols});\n"
                    'printf("%s\\n", b == NULL ? "NULL" : "tablero");\nboard_free(b);')
            with self.subTest(rows=rows, cols=cols):
                self.check(self.driver(body), "NULL", f"board_new({rows}, {cols}) debe devolver NULL")

    def test_free_accepts_null(self):
        self.check(self.driver('board_free(NULL);\nprintf("ok\\n");'), "ok", "board_free(NULL)")

    def test_rejects_bad_rows_without_leaks(self):
        body = ('const char *rows[] = {"..", "..."};\nconst char *other[] = {"x"};\n'
                'printf("%d %d\\n", board_from_rows(rows, 2) == NULL, board_from_rows(other, 1) == NULL);')
        self.check(self.driver(body), "1 1", "board_from_rows con datos malos")


class NeighborCounts(LabCase):
    def around(self, rows):
        body = ('for (int r = 0; r < board_rows(b); r++) {\n'
                '    for (int c = 0; c < board_cols(b); c++) { printf("%d", board_mines_around(b, r, c)); }\n'
                '    printf(" ");\n}\nprintf("\\n");')
        return self.board(rows, body)

    def test_mine_in_the_center_has_eight_neighbors(self):
        self.check(self.around(["...", ".*.", "..."]), "111 101 111",
                   "minas vecinas alrededor del centro: la mina no se cuenta a sí misma")

    def test_corners_and_borders(self):
        self.check(self.around(["*..", "...", "..*"]), "010 121 010", "minas vecinas con minas en las esquinas")

    def test_full_board(self):
        self.check(self.around(["***", "***", "***"]), "353 585 353", "minas vecinas en un tablero lleno")

    def test_not_square(self):
        self.check(self.around(["*...", "...*"]), "0111 1110",
                   "minas vecinas en un tablero de 2 x 4")


class Reveal(LabCase):
    def test_number_opens_only_itself(self):
        self.check(self.board(["*..", "...", "..."], 'board_reveal(b, 0, 1);\nshow(b);\nprintf("%d\\n", board_opened(b));'),
                   "#1# ### ###\n1", "abrir una celda con número solo abre esa celda")

    def test_zero_opens_the_region_and_its_border(self):
        rows = ["*.......", "..*.....", "........", "......*.", "........"]
        self.check(self.board(rows, "board_reveal(b, 2, 0);\nshow(b);"),
                   "###1.... 12#1.... .111.111 .....1## .....1##",
                   "abrir una celda sin minas vecinas abre su zona y el borde con números")

    def test_opens_through_diagonals(self):
        # Las dos zonas sin minas solo se tocan por una diagonal: (1, 1) y (2, 2).
        rows = ["...*.", ".....", ".....", "*....", "....."]
        self.check(self.board(rows, "board_reveal(b, 0, 0);\nshow(b);"), "..1## ..111 11... #1... #1...",
                   "la apertura sigue a las ocho vecinas, también por las diagonales")

    def test_a_number_stops_the_opening(self):
        rows = ["......", "..*...", "......", "....*.", "......"]
        body = 'board_reveal(b, 0, 5);\nprintf("%d %d\\n", board_opened(b), board_state(b) == GAME_LOST);'
        self.check(self.board(rows, body), "9 0", "la apertura se detiene en los números y no abre ninguna mina")

    def test_flag_is_not_opened(self):
        rows = ["...", "...", "..*"]
        body = 'board_toggle_flag(b, 0, 2);\nboard_reveal(b, 0, 0);\nshow(b);\nboard_reveal(b, 0, 2);\nshow(b);'
        self.check(self.board(rows, body), "..F .11 .1#\n..F .11 .1#",
                   "una celda con bandera no se abre, ni directa ni de forma recursiva")

    def test_flag_can_be_removed(self):
        rows = ["*.", ".."]
        body = 'board_toggle_flag(b, 0, 1);\nboard_toggle_flag(b, 0, 1);\nboard_reveal(b, 0, 1);\nshow(b);'
        self.check(self.board(rows, body), "#1 ##", "una bandera quitada ya no protege la celda")

    def test_outside_is_ignored(self):
        rows = ["..", ".*"]
        body = ("board_reveal(b, -1, 0);\nboard_reveal(b, 0, -1);\nboard_reveal(b, 2, 0);\nboard_reveal(b, 0, 2);\n"
                'show(b);\nprintf("%d\\n", board_opened(b));')
        self.check(self.board(rows, body), "## ##\n0", "board_reveal fuera del tablero no hace nada")

    def test_open_twice_counts_once(self):
        rows = ["*.", ".."]
        body = 'board_reveal(b, 0, 1);\nboard_reveal(b, 0, 1);\nprintf("%d\\n", board_opened(b));'
        self.check(self.board(rows, body), "1", "abrir dos veces la misma celda cuenta una vez")

    def test_big_enough_board_with_recursion(self):
        body = ('Board *b = board_new(60, 60);\nif (b == NULL) { printf("NULL\\n"); return 0; }\n'
                'board_reveal(b, 30, 30);\nprintf("%d\\n", board_opened(b));\nboard_free(b);')
        self.check(run_driver(BOARD_HELPERS, body, [SOURCE / "board.c"]), "3600", "abrir un tablero de 60 x 60 sin minas")


class WinAndLose(LabCase):
    def test_starts_playing(self):
        self.check(self.board(["*."], 'printf("%d\\n", board_state(b) == GAME_PLAYING);'), "1", "al inicio se está jugando")

    def test_opening_a_mine_loses(self):
        rows = ["*.", ".*"]
        body = 'board_reveal(b, 0, 0);\nprintf("%d %d ", board_state(b) == GAME_LOST, board_opened(b));\nshow(b);'
        self.check(self.board(rows, body), "1 0 *# #*", "abrir una mina pierde y muestra todas las minas")

    def test_all_safe_cells_win(self):
        rows = ["*.", ".."]
        body = ('board_reveal(b, 0, 1);\nprintf("%d ", board_state(b) == GAME_PLAYING);\nboard_reveal(b, 1, 0);\n'
                'printf("%d ", board_state(b) == GAME_PLAYING);\nboard_reveal(b, 1, 1);\n'
                'printf("%d\\n", board_state(b) == GAME_WON);')
        self.check(self.board(rows, body), "1 1 1", "se gana solo al abrir la última celda sin mina")

    def test_win_without_flagging_the_mines(self):
        rows = ["...", "...", "..*"]
        self.check(self.board(rows, 'board_reveal(b, 0, 0);\nprintf("%d\\n", board_state(b) == GAME_WON);'), "1",
                   "no hace falta poner banderas para ganar")

    def test_flag_on_safe_cell_blocks_the_win(self):
        rows = ["*.", ".."]
        body = ('board_toggle_flag(b, 1, 1);\nboard_reveal(b, 0, 1);\nboard_reveal(b, 1, 0);\n'
                'printf("%d\\n", board_state(b) == GAME_PLAYING);')
        self.check(self.board(rows, body), "1", "una celda sin mina con bandera sigue sin abrir")

    def test_nothing_changes_after_losing(self):
        rows = ["*.", ".."]
        body = 'board_reveal(b, 0, 0);\nboard_reveal(b, 1, 1);\nprintf("%d %d\\n", board_opened(b), board_state(b) == GAME_LOST);'
        self.check(self.board(rows, body), "0 1", "después de perder, board_reveal no hace nada")

    def test_nothing_changes_after_winning(self):
        rows = ["*.", ".."]
        body = ('board_reveal(b, 0, 1);\nboard_reveal(b, 1, 0);\nboard_reveal(b, 1, 1);\nboard_reveal(b, 0, 0);\n'
                'printf("%d %d\\n", board_opened(b), board_state(b) == GAME_WON);')
        self.check(self.board(rows, body), "3 1", "después de ganar, board_reveal no hace nada")


class RandomBoards(LabCase):
    def driver(self, body):
        return run_driver(BOARD_HELPERS, body, [SOURCE / "board.c"])

    LAYOUT = """
    static void layout(const Board *b) {
        for (int r = 0; r < board_rows(b); r++) {
            for (int c = 0; c < board_cols(b); c++) { printf("%d", board_mines_around(b, r, c)); }
            printf(" ");
        }
        printf("\\n");
    }
    """

    def test_same_seed_same_board(self):
        body = ('Board *a = board_random(6, 6, 8, 42);\nBoard *b = board_random(6, 6, 8, 42);\n'
                'if (a == NULL || b == NULL) { printf("NULL\\n"); board_free(a); board_free(b); return 0; }\n'
                "layout(a);\nlayout(b);\nboard_free(a);\nboard_free(b);")
        out, code = run_driver(BOARD_HELPERS + self.LAYOUT, body, [SOURCE / "board.c"])
        self.assertEqual(code, 0, out)
        lines = [line.rstrip() for line in out.strip().splitlines()]
        self.assertEqual(len(lines), 2, out)
        self.assertEqual(lines[0], lines[1], "la misma semilla debe dar el mismo tablero")
        self.assertNotEqual(set(lines[0]) - {" "}, {"0"}, "con 8 minas, algunas celdas deben tener minas vecinas")

    def test_exact_mine_count(self):
        for mines in (0, 1, 8, 36):
            body = (f'Board *b = board_random(6, 6, {mines}, 7);\nif (b == NULL) {{ printf("NULL\\n"); return 0; }}\n'
                    'printf("%d\\n", board_mines(b));\nboard_free(b);')
            with self.subTest(mines=mines):
                self.check(self.driver(body), str(mines), f"board_random con {mines} minas")

    def test_impossible_counts(self):
        for mines in (-1, 37):
            body = (f"Board *b = board_random(6, 6, {mines}, 7);\n"
                    'printf("%s\\n", b == NULL ? "NULL" : "tablero");\nboard_free(b);')
            with self.subTest(mines=mines):
                self.check(self.driver(body), "NULL", f"board_random con {mines} minas no se puede")

    def test_game_with_a_known_seed(self):
        out, code = compile_and_run([LAB / "minesweeper.c", SOURCE / "board.c"], args=("5", "5", "3", "7"),
                                    stdin=(LAB / "game_seed7_input.txt").read_text(encoding="utf-8"))
        self.assertEqual(code, 0, out)
        wanted = (LAB / "game_seed7_output.txt").read_text(encoding="utf-8")
        if out != wanted:
            self.fail("La partida de la semilla 7 no coincide con lab/game_seed7_output.txt.\n"
                      f"Se obtuvo:\n{out}")


class TheirOwnTests(LabCase):
    def test_regions_tests_pass(self):
        out, code = compile_and_run([SOURCE / "regions.c", LAB / "image.c", LAB / "regions_tests.c"])
        self.assertEqual(code, 0, out)

    def test_board_tests_pass(self):
        out, code = compile_and_run([SOURCE / "board.c", LAB / "board_tests.c"])
        self.assertEqual(code, 0, out)

    def test_no_warnings(self):
        for name in ("regions.c", "board.c"):
            with self.subTest(file=name):
                done = subprocess.run(
                    ["gcc", "-Wall", "-Wextra", "-std=c17", f"-I{LAB}", "-c", str(SOURCE / name), "-o", os.devnull],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.stdout, "", f"{name} da advertencias:\n{done.stdout}")


if __name__ == "__main__":
    unittest.main()
test_notes.pyDescargar test_notes.py
"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-10.md"
SLIDES = ROOT / "slides" / "week-10.tex"
# Antes de un bloque cuya salida cambia con el sistema (informes de una caída, direcciones). No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un bloque que muestra lo que escribe el laboratorio terminado: se corre con la solución.
SOLUTION_OUTPUT = "<!-- salida con la solución -->"


def normalize(text):
    # Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
    return text.replace("‘", "'").replace("’", "'")


def run_shell(command):
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return normalize(done.stdout)


def clean_lines(text):
    return [line.rstrip() for line in text.splitlines()]


def split_commands(lines):
    """Parte un bloque de terminal en pares (comando, líneas de salida)."""
    pairs = []
    for line in lines:
        if line.startswith("$ "):
            pairs.append((line[2:], []))
        elif pairs:
            pairs[-1][1].append(normalize(line.rstrip()))
    return pairs


def note_blocks(language):
    """Devuelve (líneas, marca) de cada bloque de los apuntes."""
    blocks, current, mark, previous = [], None, "", ""
    for raw in NOTES.read_text(encoding="utf-8").splitlines():
        line = re.sub(r"^> ?", "", raw)
        if current is None:
            if line.strip() == "```" + language:
                current, mark = [], previous.strip()
            elif line.strip():
                previous = line
        elif line.strip() == "```":
            blocks.append((current, mark))
            current = None
        else:
            current.append(line)
    return blocks


def slide_blocks(environments):
    text = SLIDES.read_text(encoding="utf-8")
    pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
    return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]


def known_code_lines():
    lines = set()
    sources = list(HERE.glob("*.c")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.c"))
    sources += list((HERE / "lab").glob("*.c")) + list((HERE / "lab").glob("*.h"))
    for source in sources:
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


class NotesAreTrue(unittest.TestCase):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if not lines or not lines[0].startswith("$ ") or mark in (SYSTEM_DEPENDENT, SOLUTION_OUTPUT):
                continue
            for command, expected in split_commands(lines):
                checked += 1
                with self.subTest(command=command):
                    self.assertEqual(clean_lines(run_shell(command)), expected)
        self.assertGreater(checked, 8)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_code_blocks_in_notes_exist(self):
        known = known_code_lines()
        for lines, _ in note_blocks("c"):
            for line in lines:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)

    @unittest.skipUnless(NOTES.exists() and (HERE / "solution").exists(), "falta la solución")
    def test_solution_output_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if mark != SOLUTION_OUTPUT:
                continue
            for command, expected in split_commands(lines):
                # Los apuntes muestran el comando de ustedes; aquí se corre con la solución.
                actual = clean_lines(run_shell(command.replace("build/lab/", "build/solution/")))
                checked += 1
                with self.subTest(command=command):
                    self.assertEqual(actual, expected)
        self.assertGreater(checked, 1)


class SlidesAreTrue(unittest.TestCase):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        # Los informes de una caída van en otro entorno, informe, que no se compara.
        for block in slide_blocks("terminal|salida|salidalarga"):
            for command, expected in split_commands(block):
                actual = clean_lines(run_shell(command))
                position = 0
                # Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
                for line in expected:
                    line = line.strip()
                    if not line:
                        continue
                    with self.subTest(command=command, line=line):
                        found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
                        self.assertTrue(found, f"no aparece en la salida real: {line}")
                        if found:
                            position = found[0] + 1

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_code_blocks_in_slides_exist(self):
        known = known_code_lines()
        for block in slide_blocks("codigo|codigochico|codigomini"):
            for line in block:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)


if __name__ == "__main__":
    unittest.main()

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/big_board.cDescargar lab/big_board.c
// Abre una esquina de un tablero grande y sin minas. Uso: big_board filas columnas
#include <stdio.h>

#include "board.h"

int main(int argc, char *argv[]) {
    int rows = 0;
    int cols = 0;
    if (argc != 3 || sscanf(argv[1], "%5d", &rows) != 1 || sscanf(argv[2], "%5d", &cols) != 1) {
        printf("uso: big_board filas columnas\n");
        return 1;
    }

    Board *board = board_new(rows, cols);
    if (board == NULL) {
        printf("no se pudo crear el tablero\n");
        return 1;
    }

    board_reveal(board, 0, 0);
    printf("%d x %d: %d celdas abiertas, ", rows, cols, board_opened(board));
    printf("%s\n", board_state(board) == GAME_WON ? "juego ganado" : "juego sin terminar");
    board_free(board);
    return 0;
}
lab/board_tests.cDescargar lab/board_tests.c
// Pruebas de board.c. Agreguen las suyas, con estas como modelo.
#include <assert.h>
#include <stdio.h>

#include "board.h"

// Sesión 1
static void test_new_board_is_hidden_and_has_no_mines(void) {
    Board *board = board_new(2, 3);
    assert(board != NULL);
    assert(board_rows(board) == 2);
    assert(board_cols(board) == 3);
    assert(board_mines(board) == 0);
    assert(board_opened(board) == 0);
    assert(board_cell_char(board, 1, 2) == '#');
    board_free(board);
}

// Sesión 1
static void test_counts_around_a_mine(void) {
    const char *rows[] = {
        "*..",
        "...",
        "..*",
    };
    Board *board = board_from_rows(rows, 3);
    assert(board != NULL);
    assert(board_mines(board) == 2);
    assert(board_mines_around(board, 0, 1) == 1);
    assert(board_mines_around(board, 1, 1) == 2);
    assert(board_mines_around(board, 2, 0) == 0);
    board_free(board);
}

// Sesión 2
static void test_reveal_opens_the_zero_region(void) {
    const char *rows[] = {
        "....",
        "....",
        "...*",
    };
    Board *board = board_from_rows(rows, 3);
    assert(board != NULL);

    board_reveal(board, 0, 0);

    assert(board_cell_char(board, 0, 0) == '.');
    assert(board_cell_char(board, 1, 3) == '1');
    assert(board_cell_char(board, 2, 3) == '#');
    assert(board_opened(board) == 11);
    assert(board_state(board) == GAME_WON);
    board_free(board);
}

int main(void) {
    test_new_board_is_hidden_and_has_no_mines();
    printf("board_new: ok\n");
    test_counts_around_a_mine();
    printf("minas vecinas: ok\n");
    test_reveal_opens_the_zero_region();
    printf("board_reveal: ok\n");
    return 0;
}
lab/board.cDescargar lab/board.c
// Implementación del tablero. Ver board.h.
#include "board.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef enum {
    CELL_HIDDEN, // vale 0: es lo que queda en un bloque de calloc
    CELL_OPEN,
    CELL_FLAGGED
} CellView;

typedef struct {
    int is_mine;
    int around; // minas en las ocho vecinas
    CellView view;
} Cell;

// Las celdas van fila por fila, igual que los píxeles de una imagen.
struct board {
    int rows;
    int cols;
    int mines;
    int opened;   // celdas sin mina ya abiertas
    int exploded; // 1 si ya se abrió una mina
    Cell *cells;
};

static int in_bounds(const Board *board, int row, int col) {
    return row >= 0 && row < board->rows && col >= 0 && col < board->cols;
}

static Cell *cell_at(const Board *board, int row, int col) {
    return &board->cells[row * board->cols + col];
}

// ---------- Sesión 1: escriban estas tres funciones ----------

Board *board_new(int rows, int cols) {
    // Escriban aquí su código. Son dos bloques: el struct y las celdas.
    (void) rows;
    (void) cols;
    return NULL;
}

void board_free(Board *board) {
    // Escriban aquí su código.
    (void) board;
}

// Llena el campo around de todas las celdas. board_from_rows y board_random la llaman cuando
// terminan de poner las minas.
static void count_neighbors(Board *board) {
    // Escriban aquí su código.
    (void) board;
}

// ---------- Sesión 2: escriban estas dos funciones ----------

void board_reveal(Board *board, int row, int col) {
    // Escriban aquí su código. Necesitarán una función auxiliar recursiva, declarada static.
    (void) board;
    (void) row;
    (void) col;
}

GameState board_state(const Board *board) {
    // Escriban aquí su código.
    (void) board;
    return GAME_PLAYING;
}

// ---------- De aquí para abajo, el código ya está hecho ----------

Board *board_from_rows(const char *rows[], int n) {
    if (n < 1) {
        return NULL;
    }
    int cols = (int) strlen(rows[0]);
    Board *board = board_new(n, cols);
    if (board == NULL) {
        return NULL;
    }

    for (int row = 0; row < n; row++) {
        if ((int) strlen(rows[row]) != cols) {
            board_free(board);
            return NULL;
        }
        for (int col = 0; col < cols; col++) {
            if (rows[row][col] == '*') {
                cell_at(board, row, col)->is_mine = 1;
                board->mines++;
            } else if (rows[row][col] != '.') {
                board_free(board);
                return NULL;
            }
        }
    }
    count_neighbors(board);
    return board;
}

// Generador de números seudoaleatorios: cada número sale del anterior. Lleva su estado afuera, en
// *state, para que la misma semilla dé lo mismo en todas las computadoras.
static unsigned int next_random(unsigned int *state) {
    *state = *state * 1664525u + 1013904223u;
    return *state >> 8; // los bits de abajo de este generador se repiten demasiado
}

Board *board_random(int rows, int cols, int mines, unsigned int seed) {
    Board *board = board_new(rows, cols);
    if (board == NULL) {
        return NULL;
    }
    int total = rows * cols;
    if (mines < 0 || mines > total) {
        board_free(board);
        return NULL;
    }

    unsigned int state = seed;
    while (board->mines < mines) {
        Cell *cell = &board->cells[next_random(&state) % (unsigned int) total];
        if (!cell->is_mine) {
            cell->is_mine = 1;
            board->mines++;
        }
    }
    count_neighbors(board);
    return board;
}

void board_toggle_flag(Board *board, int row, int col) {
    if (board_state(board) != GAME_PLAYING || !in_bounds(board, row, col)) {
        return;
    }
    Cell *cell = cell_at(board, row, col);
    if (cell->view == CELL_HIDDEN) {
        cell->view = CELL_FLAGGED;
    } else if (cell->view == CELL_FLAGGED) {
        cell->view = CELL_HIDDEN;
    }
}

int board_rows(const Board *board) {
    return board->rows;
}

int board_cols(const Board *board) {
    return board->cols;
}

int board_mines(const Board *board) {
    return board->mines;
}

int board_opened(const Board *board) {
    return board->opened;
}

int board_mines_around(const Board *board, int row, int col) {
    return cell_at(board, row, col)->around;
}

char board_cell_char(const Board *board, int row, int col) {
    const Cell *cell = cell_at(board, row, col);
    if (cell->is_mine && (board->exploded || cell->view == CELL_OPEN)) {
        return '*';
    }
    if (cell->view == CELL_HIDDEN) {
        return '#';
    }
    if (cell->view == CELL_FLAGGED) {
        return 'F';
    }
    return cell->around == 0 ? '.' : (char) ('0' + cell->around);
}

void board_print(const Board *board) {
    printf("   ");
    for (int col = 0; col < board->cols; col++) {
        printf("%2d", col % 100);
    }
    printf("\n");
    for (int row = 0; row < board->rows; row++) {
        printf("%2d ", row % 100);
        for (int col = 0; col < board->cols; col++) {
            printf(" %c", board_cell_char(board, row, col));
        }
        printf("\n");
    }
}
lab/board.hDescargar lab/board.h
// Tablero de Buscaminas. Es un tipo opaco: quien lo usa solo tiene punteros y solo llama a estas
// funciones. Las filas y las columnas se cuentan desde 0.
#ifndef BOARD_H
#define BOARD_H

typedef struct board Board;

typedef enum {
    GAME_PLAYING,
    GAME_WON,
    GAME_LOST
} GameState;

// Crea un tablero sin minas, con todas sus celdas tapadas. Devuelve NULL si rows o cols son menores
// que 1, si rows * cols no cabe en un int o si no hay memoria.
// Owner: quien llama, que debe liberarlo con board_free.
Board *board_new(int rows, int cols);

// Libera el tablero. Acepta NULL.
void board_free(Board *board);

// Crea un tablero fijo desde n cadenas de igual largo: '*' es una mina y '.' es una celda sin mina.
// Devuelve NULL si los datos están mal o si no hay memoria. Owner: quien llama.
Board *board_from_rows(const char *rows[], int n);

// Crea un tablero con mines minas en lugares elegidos a partir de seed: con la misma semilla salen
// siempre los mismos lugares. Devuelve NULL si mines no cabe o si no hay memoria. Owner: quien llama.
Board *board_random(int rows, int cols, int mines, unsigned int seed);

// Abre la celda. Una celda sin minas vecinas abre también a sus ocho vecinas, y así sucesivamente.
// No hace nada si la celda está fuera, abierta o con bandera, o si el juego ya terminó.
void board_reveal(Board *board, int row, int col);

// Pone o quita la bandera de una celda tapada. En otro caso no hace nada.
void board_toggle_flag(Board *board, int row, int col);

// Dice cómo va el juego: GAME_LOST si se abrió una mina, GAME_WON si se abrieron todas las celdas sin
// mina, y GAME_PLAYING en otro caso.
GameState board_state(const Board *board);

int board_rows(const Board *board);
int board_cols(const Board *board);
int board_mines(const Board *board);

// Devuelve cuántas celdas sin mina están abiertas.
int board_opened(const Board *board);

// Devuelve cuántas minas tiene alrededor la celda, sin contarse a sí misma.
// Exige: (row, col) dentro del tablero.
int board_mines_around(const Board *board, int row, int col);

// Devuelve el carácter que el jugador ve en la celda: '#' tapada, 'F' con bandera, '.' abierta sin
// minas vecinas, de '1' a '8' abierta con minas vecinas, '*' mina abierta. Si ya se perdió, muestra
// '*' en todas las minas. Exige: (row, col) dentro del tablero.
char board_cell_char(const Board *board, int row, int col);

// Escribe el tablero en la terminal, con los números de fila y de columna.
void board_print(const Board *board);

#endif
lab/fill_demo.cDescargar lab/fill_demo.c
// Muestra una imagen, cuenta sus regiones, pinta una y la muestra otra vez.
// Uso: fill_demo imagen.pgm x y valor
#include <stdio.h>

#include "image.h"
#include "regions.h"

int main(int argc, char *argv[]) {
    int x = 0;
    int y = 0;
    int value = 0;
    if (argc != 5 || sscanf(argv[2], "%6d", &x) != 1 || sscanf(argv[3], "%6d", &y) != 1
        || sscanf(argv[4], "%6d", &value) != 1) {
        printf("uso: fill_demo imagen.pgm x y valor\n");
        return 1;
    }

    Image *img = image_load(argv[1]);
    if (img == NULL) {
        printf("no se pudo leer %s\n", argv[1]);
        return 1;
    }
    if (x < 0 || x >= image_width(img) || y < 0 || y >= image_height(img)
        || value < 0 || value > image_max_value(img)) {
        printf("(%d, %d) o el valor %d están fuera de la imagen\n", x, y, value);
        image_free(img);
        return 1;
    }

    image_show(img);
    printf("regiones: %d\n\n", count_regions(img));
    flood_fill(img, x, y, value);
    image_show(img);
    printf("regiones: %d\n", count_regions(img));
    image_free(img);
    return 0;
}
lab/game_seed7_input.txtDescargar lab/game_seed7_input.txt
a 0 0
b 0 2
b 0 2
x
a 4 4
a 0 2
lab/game_seed7_output.txtDescargar lab/game_seed7_output.txt
    0 1 2 3 4
 0  # # # # #
 1  # # # # #
 2  # # # # #
 3  # # # # #
 4  # # # # #
jugada:     0 1 2 3 4
 0  . 1 # # #
 1  . 1 1 2 1
 2  . . . . .
 3  . . 1 1 1
 4  . . 1 # #
jugada:     0 1 2 3 4
 0  . 1 F # #
 1  . 1 1 2 1
 2  . . . . .
 3  . . 1 1 1
 4  . . 1 # #
jugada:     0 1 2 3 4
 0  . 1 # # #
 1  . 1 1 2 1
 2  . . . . .
 3  . . 1 1 1
 4  . . 1 # #
jugada: comandos: a fila columna, b fila columna, s
    0 1 2 3 4
 0  . 1 # # #
 1  . 1 1 2 1
 2  . . . . .
 3  . . 1 1 1
 4  . . 1 # #
jugada:     0 1 2 3 4
 0  . 1 # # #
 1  . 1 1 2 1
 2  . . . . .
 3  . . 1 1 1
 4  . . 1 # 1
jugada:     0 1 2 3 4
 0  . 1 * # *
 1  . 1 1 2 1
 2  . . . . .
 3  . . 1 1 1
 4  . . 1 * 1
Perdieron: abrieron una mina.
lab/image.cDescargar lab/image.c
// Implementación del módulo de imágenes. Ver image.h.
#include "image.h"

#include <assert.h>
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

// Ningún dato válido de un PGM de este curso pasa de aquí.
#define NUMBER_LIMIT 100000

// En cada línea del archivo: el formato pide no pasar de 70 caracteres.
#define VALUES_PER_LINE 16

// Los píxeles van fila por fila: el píxel (x, y) es pixels[y * width + x].
struct image {
    int width;
    int height;
    int max_value;
    int *pixels;
};

static int valid_size(int width, int height, int max_value) {
    return width >= 1 && height >= 1 && width <= INT_MAX / height
        && max_value >= 1 && max_value <= IMAGE_MAX_VALUE;
}

Image *image_new(int width, int height, int max_value, int value) {
    if (!valid_size(width, height, max_value) || value < 0 || value > max_value) {
        return NULL;
    }

    Image *img = malloc(sizeof(*img));
    if (img == NULL) {
        return NULL;
    }

    int count = width * height;
    img->pixels = malloc(count * sizeof(int));
    if (img->pixels == NULL) {
        free(img);
        return NULL;
    }

    img->width = width;
    img->height = height;
    img->max_value = max_value;
    for (int i = 0; i < count; i++) {
        img->pixels[i] = value;
    }
    return img;
}

void image_free(Image *img) {
    if (img != NULL) {
        free(img->pixels);
        free(img);
    }
}

// Devuelve el siguiente número del archivo, o -1 si lo que sigue no es un número.
// Se salta los espacios y los comentarios, que van de # hasta el fin de la línea.
static int read_number(FILE *file) {
    int c = fgetc(file);
    while (c == '#' || isspace(c)) {
        if (c == '#') {
            while (c != '\n' && c != EOF) {
                c = fgetc(file);
            }
        } else {
            c = fgetc(file);
        }
    }

    if (!isdigit(c)) {
        return -1;
    }

    int value = 0;
    while (isdigit(c)) {
        value = value * 10 + (c - '0');
        if (value > NUMBER_LIMIT) {
            return -1;
        }
        c = fgetc(file);
    }
    // Lo que no era parte del número le toca a la siguiente lectura.
    if (c != EOF) {
        ungetc(c, file);
    }
    return value;
}

// Lee la imagen de un archivo ya abierto. Lo que venga después del último píxel se ignora.
static Image *read_image(FILE *file) {
    if (fgetc(file) != 'P' || fgetc(file) != '2') {
        return NULL;
    }

    int width = read_number(file);
    int height = read_number(file);
    int max_value = read_number(file);
    Image *img = image_new(width, height, max_value, 0);
    if (img == NULL) {
        return NULL;
    }

    int count = width * height;
    for (int i = 0; i < count; i++) {
        int value = read_number(file);
        if (value < 0 || value > max_value) {
            image_free(img);
            return NULL;
        }
        img->pixels[i] = value;
    }
    return img;
}

Image *image_load(const char *name) {
    FILE *file = fopen(name, "r");
    if (file == NULL) {
        return NULL;
    }

    Image *img = read_image(file);
    fclose(file);
    return img;
}

int image_save(const Image *img, const char *name) {
    FILE *file = fopen(name, "w");
    if (file == NULL) {
        return 0;
    }

    int written = fprintf(file, "P2\n%d %d\n%d\n", img->width, img->height, img->max_value);
    for (int y = 0; y < img->height && written >= 0; y++) {
        for (int x = 0; x < img->width && written >= 0; x++) {
            int last_in_line = x == img->width - 1 || x % VALUES_PER_LINE == VALUES_PER_LINE - 1;
            written = fprintf(file, "%d%c", img->pixels[y * img->width + x],
                              last_in_line ? '\n' : ' ');
        }
    }

    // Al cerrar se escribe lo que quedaba pendiente, y eso también puede fallar.
    if (fclose(file) != 0 || written < 0) {
        return 0;
    }
    return 1;
}

int image_width(const Image *img) {
    return img->width;
}

int image_height(const Image *img) {
    return img->height;
}

int image_max_value(const Image *img) {
    return img->max_value;
}

int image_get(const Image *img, int x, int y) {
    assert(x >= 0 && x < img->width && y >= 0 && y < img->height);
    return img->pixels[y * img->width + x];
}

void image_set(Image *img, int x, int y, int value) {
    assert(x >= 0 && x < img->width && y >= 0 && y < img->height);
    assert(value >= 0 && value <= img->max_value);
    img->pixels[y * img->width + x] = value;
}

void image_print(const Image *img) {
    printf("%d x %d, max %d\n", img->width, img->height, img->max_value);
    for (int y = 0; y < img->height; y++) {
        for (int x = 0; x < img->width; x++) {
            printf("%4d", img->pixels[y * img->width + x]);
        }
        printf("\n");
    }
}

void image_show(const Image *img) {
    const char shades[] = " .:-=+*#%@";
    int last_shade = (int) sizeof(shades) - 2;

    for (int y = 0; y < img->height; y++) {
        for (int x = 0; x < img->width; x++) {
            putchar(shades[img->pixels[y * img->width + x] * last_shade / img->max_value]);
        }
        putchar('\n');
    }
}
lab/image.hDescargar lab/image.h
// Módulo de imágenes en escala de grises, en formato PGM de texto (P2).
// Es un tipo opaco: quien lo usa solo tiene punteros y solo llama a estas funciones.
// Cada píxel vale de 0 (negro) a max_value (blanco).
#ifndef IMAGE_H
#define IMAGE_H

#define IMAGE_MAX_VALUE 255

typedef struct image Image;

// Crea una imagen con todos sus píxeles en value. Devuelve NULL si algún dato no está en su
// rango o si no hay memoria. Owner: quien llama, que debe liberarla con image_free.
Image *image_new(int width, int height, int max_value, int value);

// Lee un archivo PGM de texto. Devuelve NULL si no se puede abrir, si no es P2, si sus datos
// están mal o si no hay memoria. Owner: quien llama, que debe liberarla con image_free.
Image *image_load(const char *name);

// Libera la imagen. Acepta NULL.
void image_free(Image *img);

// Escribe la imagen en un archivo PGM de texto. Devuelve 1 si pudo y 0 si no.
int image_save(const Image *img, const char *name);

// Devuelven el ancho, el alto y el valor que significa blanco.
int image_width(const Image *img);
int image_height(const Image *img);
int image_max_value(const Image *img);

// Devuelve el píxel de la columna x y la fila y.
// Exige: 0 <= x < ancho y 0 <= y < alto.
int image_get(const Image *img, int x, int y);

// Cambia el píxel de la columna x y la fila y.
// Exige: 0 <= x < ancho, 0 <= y < alto y 0 <= value <= max_value.
void image_set(Image *img, int x, int y, int value);

// Escribe el tamaño y los números de cada píxel, para imágenes pequeñas.
void image_print(const Image *img);

// Dibuja la imagen en la terminal con letras: más claro, más lleno.
void image_show(const Image *img);

#endif
lab/minesweeper.cDescargar lab/minesweeper.c
// Buscaminas de terminal. Uso: minesweeper filas columnas minas semilla
// Comandos: a fila columna (abrir), b fila columna (bandera), s (salir).
#include <stdio.h>

#include "board.h"

#define LINE_SIZE 80

// Hace un turno: devuelve 0 si hay que salir o se acabó la entrada, y 1 si el juego sigue.
static int play_turn(Board *board) {
    char line[LINE_SIZE];
    char command = 0;
    int row = 0;
    int col = 0;

    printf("jugada: ");
    if (fgets(line, LINE_SIZE, stdin) == NULL) {
        return 0;
    }
    int fields = sscanf(line, " %c %6d %6d", &command, &row, &col);
    if (fields == 1 && command == 's') {
        return 0;
    }
    if (fields == 3 && command == 'a') {
        board_reveal(board, row, col);
    } else if (fields == 3 && command == 'b') {
        board_toggle_flag(board, row, col);
    } else {
        printf("comandos: a fila columna, b fila columna, s\n");
    }
    return 1;
}

int main(int argc, char *argv[]) {
    int rows = 0;
    int cols = 0;
    int mines = 0;
    unsigned int seed = 0;
    if (argc != 5 || sscanf(argv[1], "%4d", &rows) != 1 || sscanf(argv[2], "%4d", &cols) != 1
        || sscanf(argv[3], "%6d", &mines) != 1 || sscanf(argv[4], "%9u", &seed) != 1) {
        printf("uso: minesweeper filas columnas minas semilla\n");
        return 1;
    }

    Board *board = board_random(rows, cols, mines, seed);
    if (board == NULL) {
        printf("no se pudo crear un tablero de %d x %d con %d minas\n", rows, cols, mines);
        return 1;
    }

    board_print(board);
    while (board_state(board) == GAME_PLAYING && play_turn(board)) {
        board_print(board);
    }

    if (board_state(board) == GAME_WON) {
        printf("Ganaron: abrieron todas las celdas sin mina.\n");
    } else if (board_state(board) == GAME_LOST) {
        printf("Perdieron: abrieron una mina.\n");
    }
    board_free(board);
    return 0;
}
lab/README.mdDescargar lab/README.md
# Laboratorio de la semana 10: Buscaminas

Dos sesiones de 2 horas, cada una pensada para unos 90 minutos de trabajo. Los comandos se corren
desde `code/week-10`. Las ideas están en los apuntes, sobre todo en "El laboratorio, paso a paso".

| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `image.h`, `image.c` | El módulo de imágenes de la semana 6, terminado | No |
| `regions.h` | La interfaz de `flood_fill` y `count_regions` | No |
| `regions.c` | Las dos funciones, por escribir | **Sí** |
| `regions_tests.c` | Pruebas con `assert` de las regiones | **Sí**, agreguen |
| `fill_demo.c` | Muestra una imagen, cuenta sus regiones y pinta una | No |
| `board.h` | La interfaz del tablero, con un tipo opaco | No |
| `board.c` | Lo marcado arriba, por escribir; lo demás está hecho | **Sí** |
| `board_tests.c` | Pruebas con `assert` del tablero | **Sí**, agreguen |
| `minesweeper.c` | El juego de terminal | No |
| `big_board.c` | Abre una esquina de un tablero grande sin minas | No |
| `images/` | `islands.pgm`, `sun.pgm` y `tiny.pgm` | No |
| `game_seed7_input.txt`, `game_seed7_output.txt` | Una partida fija y lo que debe escribir | No |

```sh
make lab                                     # compila todo en build/lab/
build/lab/regions_tests                      # sus pruebas de regiones
build/lab/board_tests                        # sus pruebas del tablero
build/lab/fill_demo lab/images/islands.pgm 2 2 255
build/lab/minesweeper 8 8 10 42              # filas, columnas, minas y semilla
make test-lab                                # comprueba todo el laboratorio; falla hasta que terminen
```

Todo se compila con ASan: si un bloque no se libera, el programa termina con un informe y un código de
error, y la prueba falla. `board_tests` se detiene en el primer `assert` que no se cumple: es normal que
falle la parte de la sesión 2 mientras están en la sesión 1. Las líneas como `(void) img;` solo evitan una
advertencia mientras un parámetro no se usa: bórrenlas cuando escriban la función.

# Sesión 1

## Parte 1: leer (unos 20 minutos)

Lean `image.h`, `regions.h` y `board.h` completos. En `board.c`, lean el `struct`, `in_bounds`, `cell_at`,
`board_from_rows`, `board_random`, `board_cell_char` y `board_print`. Lean también `board_tests.c`.
Respondan por escrito, sin ejecutar nada.

1. `regions.c` no puede escribir `img->pixels`. ¿Por qué? ¿Qué funciones tiene que usar en su lugar?
2. Con `rows = 2` y `cols = 3`, ¿en qué posición de `cells` está la celda `(1, 2)`?
3. `count_neighbors` es `static`, y quien usa el módulo nunca la llama. ¿Cómo sabe entonces quien usa
   `board.h` que los conteos están listos?
4. `count_regions` recibe un `const Image *`. ¿Por qué eso le impide usar lo pintado como marca de visitado?
5. ¿Qué hace `board_random` para que la misma semilla dé el mismo tablero en dos computadoras distintas?

## Parte 2: `flood_fill` (unos 15 minutos)

Escriban `flood_fill` en `regions.c`, con una función auxiliar `static` recursiva. Es lo mismo que `fill`
en `grid_fill.c`, pero con píxeles y a través de `image_get`, `image_set`, `image_width` e `image_height`.
Recuerden el caso del valor igual: `flood_fill` debe terminar también si la región ya vale `new_value`.

## Parte 3: `count_regions` (unos 25 minutos)

Escriban `count_regions`. A diferencia de `count_islands.c`, cuenta las regiones de **todos** los valores, y
no puede cambiar la imagen. Necesita:

1. Un bloque `visited` de `ancho * alto` marcas, pedido con `calloc` y liberado al final. Esta función es
   su owner.
2. Una función auxiliar `static` que marque una región, con el valor de esa región como parámetro.
3. Dos ciclos anidados: cada píxel sin marcar empieza una región nueva.

Agreguen a `regions_tests.c` al menos una prueba: una imagen con un hueco, por ejemplo.

```sh
build/lab/regions_tests
build/lab/fill_demo lab/images/islands.pgm 2 2 255
build/lab/fill_demo lab/images/sun.pgm 0 0 230
```

Antes de correr `fill_demo` con `islands.pgm`, cuenten a mano las regiones de la imagen. ¿Por qué baja en uno
el conteo después de pintar?

## Parte 4: el tablero en memoria dinámica (unos 30 minutos)

En `board.c`, escriban:

1. `board_new`: dos bloques, el `struct` y las celdas. Revisen los tamaños como lo hace `valid_size` en
   `image.c`, y si falla el segundo `malloc`, liberen el primero. Dejen las celdas en cero: por eso
   `CELL_HIDDEN` vale 0.
2. `board_free`, que acepte `NULL`.
3. `count_neighbors`: para cada celda, recorran las ocho vecinas con dos ciclos de -1 a 1. Salten la celda
   misma y las que caen fuera, con `in_bounds`. Una mina también lleva su conteo.

Corran `build/lab/board_tests`: deben pasar las dos primeras pruebas, y la tercera sigue fallando. Agreguen
una prueba de `count_neighbors` con un tablero de una sola fila.

**Si no alcanza el tiempo:** lo que no terminen lo terminan antes de la sesión 2. La sesión 2 necesita que
`board_new`, `board_free` y `count_neighbors` funcionen.

# Sesión 2

## Parte 5: `board_reveal` (unos 35 minutos)

Escriban `board_reveal` y su función auxiliar recursiva. El pseudocódigo está en los apuntes. Decidan en qué
orden van estas revisiones, y escríbanlo en un comentario:

- La celda está fuera del tablero.
- La celda ya está abierta o tiene bandera.
- La celda es una mina.
- La celda tiene minas vecinas.

Si el juego ya terminó, `board_reveal` no hace nada. Y cada celda sin mina que se abre suma uno a `opened`.

## Parte 6: victoria y derrota (unos 20 minutos)

Escriban `board_state` con los campos `exploded` y `opened`. Corran `build/lab/board_tests` y comparen con
una partida fija:

```sh
build/lab/minesweeper 5 5 3 7 < lab/game_seed7_input.txt
```

Lo que escribe debe ser igual a `lab/game_seed7_output.txt`. Después jueguen una partida de verdad, y escriban
una prueba de una derrota en `board_tests.c`.

## Parte 7: un tablero de 1000 × 1000 (unos 30 minutos)

Corran estos comandos y anoten qué pasa en cada uno. `big_board` es el programa compilado con ASan, y
`big_board_plain` es el mismo, sin sanitizers.

```sh
build/lab/big_board 100 100
build/lab/big_board 1000 1000
build/lab/big_board_plain 100 100
build/lab/big_board_plain 1000 1000
ulimit -s
```

Respondan por escrito:

1. ¿Qué pasó, y en qué función? ¿`board_new` tuvo algún problema con las celdas? ¿Cómo lo saben?
2. Sin minas, ¿cuántos marcos de su función recursiva pueden quedar apilados como máximo en un tablero de
   N × N? Justifíquenlo con el orden en que su función recorre las vecinas.
3. Busquen, probando tamaños, el mayor N que funciona con `big_board` y con `big_board_plain`. Con ese N y lo que
   dice `ulimit -s`, estimen cuántos bytes ocupa un marco en cada caso. ¿Por qué pueden salir distintos?
4. Corran `(ulimit -s 400000; build/lab/big_board_plain 1000 1000)`. ¿Qué cambió? ¿Por qué no es esta la
   solución del problema?
5. ¿Qué cambiarían en `board_reveal` para que un tablero de 1000 × 1000 funcione con la pila de siempre?

Al final, `make test-lab`.

**Si no alcanza el tiempo:** las preguntas 3 a 5 se pueden terminar antes de la próxima clase.

## Si sobra tiempo

1. Reescriban `board_reveal` con una pila propia, como `fill_stack.c`, y corran `big_board 1000 1000`.
2. Hagan que la primera celda abierta nunca sea una mina: si lo es, muevan esa mina a otra celda. ¿Qué conteos
   hay que recalcular?
3. Escriban `count_regions_diagonal`, que también una los píxeles por las diagonales. ¿Cuánto cambia el conteo de
   `islands.pgm`?
lab/regions_tests.cDescargar lab/regions_tests.c
// Pruebas de regions.c. Agreguen las suyas, con estas como modelo.
#include <assert.h>
#include <stdio.h>

#include "image.h"
#include "regions.h"

// Crea una imagen desde números: rows tiene width * height valores, fila por fila.
static Image *image_from(int width, int height, const int rows[]) {
    Image *img = image_new(width, height, IMAGE_MAX_VALUE, 0);
    assert(img != NULL);
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            image_set(img, x, y, rows[y * width + x]);
        }
    }
    return img;
}

static void test_fill_stops_at_the_border_of_the_region(void) {
    const int rows[] = {
        1, 1, 0,
        0, 1, 0,
        1, 0, 0,
    };
    Image *img = image_from(3, 3, rows);

    flood_fill(img, 0, 0, 9);

    assert(image_get(img, 0, 0) == 9);
    assert(image_get(img, 1, 0) == 9);
    assert(image_get(img, 1, 1) == 9);
    assert(image_get(img, 2, 0) == 0);
    assert(image_get(img, 0, 2) == 1); // solo toca a la región por una diagonal
    image_free(img);
}

static void test_count_regions_sees_diagonals_as_separate(void) {
    const int rows[] = {
        1, 0,
        0, 1,
    };
    Image *img = image_from(2, 2, rows);

    assert(count_regions(img) == 4);
    image_free(img);
}

int main(void) {
    test_fill_stops_at_the_border_of_the_region();
    test_count_regions_sees_diagonals_as_separate();
    printf("pruebas de regiones: ok\n");
    return 0;
}
lab/regions.cDescargar lab/regions.c
// Implementación de las regiones. Ver regions.h.
// Aquí no se ve struct image: todo pasa por las funciones de image.h.
#include "regions.h"

#include <stdlib.h>

void flood_fill(Image *img, int x, int y, int new_value) {
    // Escriban aquí su código. Necesitarán una función auxiliar recursiva, declarada static.
    (void) img;
    (void) x;
    (void) y;
    (void) new_value;
}

int count_regions(const Image *img) {
    // Escriban aquí su código. Recibe una imagen const: las marcas de visitado van en un bloque
    // propio de memoria dinámica, que esta función pide y libera.
    (void) img;
    return 0;
}
lab/regions.hDescargar lab/regions.h
// Regiones de una imagen. Una región es un grupo de píxeles del mismo valor, unidos por arriba,
// abajo, izquierda o derecha: las diagonales no cuentan. Usa imágenes solo por su interfaz.
#ifndef REGIONS_H
#define REGIONS_H

#include "image.h"

// Pinta con new_value toda la región que contiene al píxel (x, y). Los demás píxeles no cambian.
// Si la región ya vale new_value, no hace nada.
// Exige: (x, y) dentro de la imagen y 0 <= new_value <= image_max_value(img).
void flood_fill(Image *img, int x, int y, int new_value);

// Devuelve cuántas regiones tiene la imagen, sin cambiarla. Devuelve -1 si no hay memoria.
int count_regions(const Image *img);

#endif
lab/images/islands.pgmDescargar lab/images/islands.pgm
P2
# Un mar oscuro con tres islas claras: una de ellas tiene una laguna oscura adentro.
16 8
255
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 255 255 255 255 0 0 0 0 0 200 200 0 0 0 0
0 255 0 0 255 0 0 0 0 0 200 200 200 0 0 0
0 255 0 0 255 0 0 0 0 0 0 200 200 0 0 0
0 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 120 120 120 0 0 0 0 0 0
0 0 0 0 0 0 0 120 120 120 120 120 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
lab/images/sun.pgmDescargar lab/images/sun.pgm
P2
# Un sol a la izquierda y un cerro que sube hacia la derecha.
32 16
255
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 255 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 255 255 255 255 255 255 255 255 255 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 255 255 255 255 255 255 255 255 255 255 255 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 255 255 255 255 255 255 255 255 255 255 255 255 255 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 255 255 255 255 255 255 255 255 255 255 255 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 255 255 255 255 255 255 255 255 255 40 40 40
40 40 40 40 40 40 40 40 40 40 40 40 40 40 130 130
40 40 40 40 40 40 40 40 255 40 40 40 40 40 40 40
40 40 40 40 40 40 40 40 40 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40
40 40 40 40 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 40 40 40 40 40 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
40 40 40 40 40 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
130 130 130 130 130 130 130 130 130 130 130 130 130 130 130 130
lab/images/tiny.pgmDescargar lab/images/tiny.pgm
P2
# Tres columnas y dos filas, con un comentario.
3 2
255
0 10 20
30 40 50

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 10: recursión en árbol. Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make lab       compila el laboratorio en build/lab/
#   make test      corre las pruebas de los ejemplos y de los apuntes
#   make test-lab  prueba su laboratorio (lab/regions.c y lab/board.c); falla hasta que lo terminen
#   make clean     borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.

CC = gcc
BASE = -Wall -Wextra -std=c17
CFLAGS = $(BASE) -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = fib_count fib_trace fib_memo grid_fill fill_trace count_islands maze deep_fill fill_stack
LAB_HEADERS = lab/image.h lab/regions.h lab/board.h

.PHONY: all lab test test-lab clean

all: $(PROGRAMS:%=$(BUILD)/%) $(BUILD)/deep_fill_plain $(BUILD)/deep_fill_o2

$(BUILD)/%: %.c | $(BUILD)
	$(CC) $(CFLAGS) $< -o $@

# El mismo deep_fill.c sin sanitizers: con -O0, y con -O2.
$(BUILD)/deep_fill_plain: deep_fill.c | $(BUILD)
	$(CC) $(BASE) -g -O0 $< -o $@

$(BUILD)/deep_fill_o2: deep_fill.c | $(BUILD)
	$(CC) $(BASE) -g -O2 $< -o $@

# Cada .c se compila por separado y luego se enlaza, como en la semana 6.
$(BUILD)/lab/%.o: lab/%.c $(LAB_HEADERS) | $(BUILD)/lab
	$(CC) $(CFLAGS) -c $< -o $@

$(BUILD)/solution/%.o: solution/%.c $(LAB_HEADERS) | $(BUILD)/solution
	$(CC) $(CFLAGS) -Ilab -c $< -o $@

$(BUILD)/solution/%.o: lab/%.c $(LAB_HEADERS) | $(BUILD)/solution
	$(CC) $(CFLAGS) -Ilab -c $< -o $@

# Los programas del laboratorio, en cada carpeta: $(1) es lab o solution.
define LAB_PROGRAMS
$(BUILD)/$(1)/regions_tests: $(BUILD)/$(1)/regions_tests.o $(BUILD)/$(1)/regions.o $(BUILD)/$(1)/image.o
	$(CC) $(CFLAGS) $$^ -o $$@

$(BUILD)/$(1)/fill_demo: $(BUILD)/$(1)/fill_demo.o $(BUILD)/$(1)/regions.o $(BUILD)/$(1)/image.o
	$(CC) $(CFLAGS) $$^ -o $$@

$(BUILD)/$(1)/board_tests: $(BUILD)/$(1)/board_tests.o $(BUILD)/$(1)/board.o
	$(CC) $(CFLAGS) $$^ -o $$@

$(BUILD)/$(1)/minesweeper: $(BUILD)/$(1)/minesweeper.o $(BUILD)/$(1)/board.o
	$(CC) $(CFLAGS) $$^ -o $$@

$(BUILD)/$(1)/big_board: $(BUILD)/$(1)/big_board.o $(BUILD)/$(1)/board.o
	$(CC) $(CFLAGS) $$^ -o $$@

$(BUILD)/$(1)/big_board_plain: lab/big_board.c $(1)/board.c lab/board.h | $(BUILD)/$(1)
	$(CC) $(BASE) -g -O0 -Ilab lab/big_board.c $(1)/board.c -o $$@

$(BUILD)/$(1)/big_board_o2: lab/big_board.c $(1)/board.c lab/board.h | $(BUILD)/$(1)
	$(CC) $(BASE) -g -O2 -Ilab lab/big_board.c $(1)/board.c -o $$@
endef
LAB_TARGETS = regions_tests fill_demo board_tests minesweeper big_board big_board_plain big_board_o2

$(eval $(call LAB_PROGRAMS,lab))
$(eval $(call LAB_PROGRAMS,solution))

$(BUILD) $(BUILD)/lab $(BUILD)/solution:
	mkdir -p $@

lab: $(LAB_TARGETS:%=$(BUILD)/lab/%)

# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
	@if [ -d solution ]; then $(MAKE) --no-print-directory $(LAB_TARGETS:%=$(BUILD)/solution/%); fi
	python3 -m unittest discover -p 'test_*.py'

test-lab: all lab
	LAB_DIR=lab python3 -m unittest -v test_lab

clean:
	rm -rf $(BUILD) __pycache__