Advent of code 2023/14
Ajax Direct

Answer 874ms

Part 1 : 108840 Part 2 : 103445
function move($grid) {
    return preg_replace_callback("/([^#\n^]+)/", fn ($m) =>
        Cache::memoized(function ($string) {
            $s = str_split($string);
            sort($s);
            return implode("", array_reverse($s));
        }, [$m[0]])
    , $grid);
}

function rotate($grid) {
    $grid = array_map("array_reverse", array_map(null, ...array_map("str_split", explode("\n", $grid))));
    $grid = implode("\n", array_map("implode", $grid));
    return $grid;
}

function weight($grid) {
    $width = strpos($grid, "\n") + 1;
    preg_match_all("/O/", $grid, $m, PREG_OFFSET_CAPTURE);

    return array_sum(
        array_map(function ($m) use ($width) {
            return $width - 1 - $m[1] % $width;
        }, $m[0])
    );
}

// ==================================================
// > PART 1
// ==================================================
$input = rotate(rotate(rotate($input)));
$solution_1 = weight(move($input));

// ==================================================
// > PART 2
// ==================================================
function pattern($grid) {
    for ($l = 0;; $l++) {
        for ($i = 0; $i < 4; $i++) $grid = rotate(move($grid));
        $key = str_replace("\n", "", $grid);
        if (isset($seen[$key])) break;
        $seen[$key] = $l;
        $cycles[$l] = weight($grid);
    }
    return [$seen[$key], $l - $seen[$key], $cycles];
}

[$start, $length, $cycles] = pattern($input);
$solution_2 = $cycles[$start + ((1000000000 - $start - 1) % $length)];