// ==================================================
// > PARSING : Index all tiles and edges
// ==================================================
// List all possible tiles orientation
$tiles = $input->split("\n\n")->mapAssoc(function ($i, $tile) {
$id = $tile->numbers()[0];
$grid = grid($tile->lines->slice(1)->join("\n"));
$conf = [$grid, (clone $grid)->reverseX()];
for ($r = 0; $r < 3; $r++) {
$conf[] = (clone $conf[$r * 2])->rotate();
$conf[] = (clone $conf[$r * 2 + 1])->rotate();
}
return [$id => $conf];
});
// Keep only the edges as numbers
function edgenum($edge) {
return bindec(str_replace([".", "#"], [0, 1], $edge->join("")));
}
$edges = $tiles->map(fn ($conf) =>
set($conf)->map(fn ($c) => [
edgenum($c->rows()[0]), edgenum($c->columns()[9]),
edgenum($c->rows()[9]), edgenum($c->columns()[0])
]
));
// Index left and top edges to find matches quicker
foreach ($edges as $id=>$tile) foreach ($tile as $c=>$conf) foreach ($conf as $e=>$edge) {
if ($e == 3) $left_edges[$edge][] = "$id:$c";
if ($e == 0) $top_edges[$edge][] = "$id:$c";
}
// ==================================================
// > PART 1 : Solve the puzzle
// ==================================================
$size = sqrt(count($edges));
$graph = (new Graph(function ($graph) use ($edges, $left_edges, $top_edges, $size) {
// Start : return every possible tile
if ($graph->current == "@") {
foreach ($edges as $id=>$tile) foreach ($tile as $c=>$conf) {
$next[] = $id . ":" . $c;
}
return array_fill_keys($next, 1);
}
// Select possible next tiles
$order = array_map(fn ($t) => explode(":", $t), explode("|", $graph->current));
$place = count($order);
// Check top and left tiles/edges
$lt = $place % $size ? $order[count($order) - 1] : false;
$tt = $place >= $size ? $order[$place - $size] : false;
$le = $lt ? $edges[$lt[0]][$lt[1]][1] : false;
$te = $tt ? $edges[$tt[0]][$tt[1]][2] : false;
$ple = $le ? $left_edges[$le] : false;
$pte = $te ? $top_edges[$te] : false;
$p = $ple && $pte ? array_intersect($ple, $pte) : ($ple ?: $pte);
// Remove all already used
$used = array_column($order, 0);
$p = array_filter($p, fn ($id) => !in_array(substr($id, 0, 4), $used));
$next = [];
foreach ($p as $s) $next[$graph->current . "|" . $s] = 1;
return $next;
}, "highest"))
// End when all tiles are placed
->defineEnd(function ($graph) use ($edges) {
return $graph->values[$graph->current] == count($edges);
})
// On end : return the solved puzzle's tiles [id, orientation]
->onEnd(function ($graph) use ($size) {
return set(explode("|", $graph->current))->map(fn ($c) => explode(":", $c));
});
$puzzle = $graph->explore("@");
$corners = set([$puzzle[0], $puzzle[$size - 1], $puzzle[$size * ($size - 1)], $puzzle[$size * $size - 1]]);
$solution_1 = $corners->column(0)->multiply();
// ==================================================
// > PART 2
// ==================================================
// Assemble the puzzle
$grid = grid();
foreach ($puzzle as $i=>$conf) {
$tile = grid();
$tile->rows = $tiles[$conf[0]][$conf[1]]->rows->slice(1, -1)->map(fn ($r) => $r->slice(1, -1));
$grid->insert(
[($i % $size) * 8, floor($i / $size) * 8],
$tile
);
}
// The monster tiles' positions
$monster = grid(implode("\n", [
" # ",
"# ## ## ###",
" # # # # # # "]))
->filter()->keys();
// Find sea monsters
for ($i = 0; $i < 8; $i++) {
$monsters = $grid->keys()->map(function ($xy) use ($monster, $grid) {
$parts = [];
foreach ($monster as [$mx, $my]) {
$pos = [$xy[0] + $mx, $xy[1] + $my];
if ($grid->get($pos) != "#") return [];
$parts[] = set($pos);
}
return $parts;
});
if (($monsters = $monsters->filter())->count()) {
$solution_2 = $grid->cells()->keep("#")->count() - $monsters->merge()->unique()->count();
break;
}
if ($i == 3) {
$grid->rotate()->reverseX();
} else {
$grid->rotate();
}
}