AoC 2022/23 - Unstable diffusion

TL;DR

On with Advent of Code puzzle XX from 2022: one challenge that needed patience.

This was one of those challenges in which the solution - in particular, the solution for part 2 - was in that bitter spot between you need to do better and optimize this thing and you can get a solution in some minutes, just hold on.

I opted for the waiting part, of course.

The full solution is available; let’s just comment a few passages.

Each “planter elf” is represented as an object of class Planter:

class Planter {
   has $.P is built;

   method pos { $!P.join(',') }

   method make-proposal ($team, $direction is copy) {
      state @deltas = [-1, -1], [-1, 0], [-1, 1],
                      [ 0, -1],          [ 0, 1],
                      [ 1, -1], [ 1, 0], [ 1, 1];
      state @tests-for =
         [2, 4, 7], # north
         [0, 3, 5], # south
         [0, 1, 2], # west
         [5, 6, 7]; # east
      state @move-for = [0, 1], [0, -1], [-1, 0], [1, 0];

      my @is-empty = @deltas.map: { $team{($!P «+» $_).join(',')}:!exists };
      return if @is-empty.all;

      for ^4 {
         return ($!P «+» @move-for[$direction])
            if @is-empty[|@tests-for[$direction]].all;
         $direction = ($direction + 1) % 4;
      }

      return;
   }

   method move-to ($Q) { $!P = $Q }
}

The two phases are represented as methods: one to figure out a proposal (make-proposal), another one for actually doing the move. The planter is “controlled” externally, so there is no autonomous, local decision for doing the move.

Moving in part 1 is done according to the rules, including tracking the direction of movements as time goes by:

my $direction = -1;
for ^10 {
   $direction = ($direction + 1) % 4;

   # collect proposals, keep moving one separated
   my (%moving, %freezing);
   for %team.values -> $elf {
      my $proposal = $elf.make-proposal(%team, $direction) or next;
      my $key = $proposal.join(',');
      next if %freezing{$key};
      if %moving{$key} {
         %moving{$key}:delete;
         %freezing{$key} = 1;
      }
      else {
         %moving{$key} = [$elf, $proposal];
      }
   }

   # move
   for %moving.values -> ($elf, $target) {
      %team{$elf.pos}:delete;
      $elf.move-to($target);
      %team{$elf.pos} = $elf;
   }
}

Depending on the propostals, elves/planters are tracked inside either %moving or %freezing. At the end of the proposals resolution phase, the actual movement phase happens, applied only to the elves/planters remained in %moving.

At this point, calculating the result is simple, keeping in mind that we have to remove the elves themselves because we need the empty locations:

my @Ps = %team.values».P;
my $area = [*] (0, 1).map({ my @Vs = @Ps»[$_]; 1 + @Vs.max - @Vs.min });

return $area - %team.elems;

Part 2 is considerably less predictable, at least for me. I ended up extending the loop in part 1 and waiting until no movement happened, with the hope it would not take too much time. I was lucky, it only takes about 15 minutes to calculate the solution, so it’s good for me.

Still, I’ll go looking into the solutions megathread because I can’t imagine that there’s no better way of doing this.

Cheers!


Comments? Octodon, Twitter, GitHub, Reddit, or drop me a line!