Advent of code 2021/12
Ajax Direct

Answer

Part 1 :
Part 2 :
// Parsing : direct links between caves
$caves = $input->lines->reduce(function ($carry, $line) {
    [$from, $to] = $line->split("-");
    $carry[(string) $from][] = (string) $to;
    $carry[(string) $to][] = (string) $from;
    return $carry;
}, set())->instanciate("Set");

function solve($caves, $part2 = false) {
    return (new Graph(function ($graph) use ($caves, $part2) {
        $path = set(explode(",", $graph->current));
        $pos = $path->last();
        if ($pos == "end") return [];

        $forbidden = $path->filter(fn ($c) => $c[0] >= "a" && $c[0] <= "z");

        if ($part2 && !$forbidden->occurrences()->remove([1])->count()) {
            $forbidden = ["start"];
        }

        $next = [];
        foreach ($caves[$pos]->remove($forbidden) as $n) {
            $next[$graph->current . "," . $n] = 1;
        }
        return $next;
    }, "highest"))

    // Number of path that end with "end"
    ->onCompletion(function ($graph) {
        return set($graph->values)
            ->filter(fn ($v, $k) => set(explode(",", $k))->last() == "end", true)
            ->count();
    })
    // Gogogo
    ->explore("start");
}

// ==================================================
// > SOLVE
// ==================================================
$solution_1 = solve($caves);
$solution_2 = solve($caves, true);