Outside Context Problem
Monkey Map

Another one where part 1 one was simple enough and then part 2 turns it into a doozy.

I faffed around a lot in this one going between using a 2D char[,] or 3D char[,,] representation.

I settled on using the 2D representation in the end.

The next bit, and this is where I disappointed myself, was not coming up with a generic solution to the edge translations. I'm sure I could with time, but speed was of the essence.

So, I created a smaller net with the same layout as my puzzle input...

     ..........
     ..........
     ..........
     ..........
     ..........
     .....
     .....
     .....
     .....
     .....
..........
..........
..........
..........
..........
.....
.....
.....
.....
.....

Then I created a path that would ensure the player would exit each edge and enter every other possible edge.

I did this as I knew that debugging the real map would just be nigh on impossible.

Anyway, with some patience, I came up with a way to map the edge translations.

private (Point Position, char NewDirection) Wrap3D(Point newSegment, Point segmentPosition)
{
    return (newSegment.X, newSegment.Y, _direction) switch
    {
        (0, 0, 'L') => (GetPositionInNewSegment(0, 2, 'L', segmentPosition.Y), 'R'), // ✓
        (1, -1, 'U') => (GetPositionInNewSegment(0, 3, 'L', segmentPosition.X, false), 'R'), // ✓
        (2, -1, 'U') => (GetPositionInNewSegment(0, 3, 'D', segmentPosition.X, false), 'U'), // ✓
        (3, 0, 'R') => (GetPositionInNewSegment(1, 2, 'R', segmentPosition.Y), 'L'), // ✓
        (2, 1, 'D') => (GetPositionInNewSegment(1, 1, 'R', segmentPosition.X, false), 'L'), // ✓
        (2, 1, 'R') => (GetPositionInNewSegment(2, 0, 'D', segmentPosition.Y, false), 'U'), // ✓
        (2, 2, 'R') => (GetPositionInNewSegment(2, 0, 'R', segmentPosition.Y), 'L'), // ✓
        (1, 3, 'D') => (GetPositionInNewSegment(0, 3, 'R', segmentPosition.X, false), 'L'), // ✓
        (1, 3, 'R') => (GetPositionInNewSegment(1, 2, 'D', segmentPosition.Y, false), 'U'), // ✓
        (0, 4, 'D') => (GetPositionInNewSegment(2, 0, 'U', segmentPosition.X, false), 'D'), // ✓
        (-1, 3, 'L') => (GetPositionInNewSegment(1, 0, 'U', segmentPosition.Y, false), 'D'), // ✓
        (-1, 2, 'L') => (GetPositionInNewSegment(1, 0, 'L', segmentPosition.Y), 'R'), // ✓
        (0, 1, 'U') => (GetPositionInNewSegment(1, 1, 'L', segmentPosition.X, false), 'R'), // ✓
        (0, 1, 'L') => (GetPositionInNewSegment(0, 2, 'U', segmentPosition.Y, false), 'D'), // ✓
        _ => throw new PuzzleException("Cannot 3D teleport.")
    };
}

As you can see, I added comments after verifying each one with my small map.

Picking a convenient line, the switch statement works as follows...

(0, 1, 'L') => (GetPositionInNewSegment(0, 2, 'U', segmentPosition.Y, false), 'D'), // ✓

The first two numbers are the coordinates of the segment being entered. The character is the direction of entry. So, for the net above we are entering segment 0, 1 heading left (i.e. coming from 1, 1).

     ..........
     ..........
 0,0 .1,0..2,0.
     ..........
     ..........
     .....
     .....
 0,1 .1,1. 2,1
     .....
     .....
..........
..........
.0,2..1,2. 2,2
..........
..........
.....
.....
.0,3. 1,3  2,3
.....
.....

The next part gets the entry position into what will be the destination segment. In this case 0, 2, on its upper edge.

(0, 1, 'L') => (GetPositionInNewSegment(0, 2, 'U', segmentPosition.Y, false), 'D'), // ✓

With the following determining which coordinate will determine the offset for entry into the new edge.

(0, 1, 'L') => (GetPositionInNewSegment(0, 2, 'U', segmentPosition.Y, false), 'D'), // ✓

We use 𝑦 in this instance because as 𝑦 increases in the original segment, it maps to an increasing 𝑥 in the new segment.

    |.....
    y.....
    |.....
    v.....
-x-> .....
..........
..........
..........
..........
..........

Finally, the last part of the statement indicates the new direction upon entry into the new segment. Which, as we can see, will be down.

(0, 1, 'L') => (GetPositionInNewSegment(0, 2, 'U', segmentPosition.Y), 'D', false), // ✓

GetPositionInNewSegment also takes an optional boolean parameter which defaults to true.

(0, 1, 'L') => (GetPositionInNewSegment(0, 2, 'U', segmentPosition.Y), 'D', false), // ✓

If this were omitted or set to true in the above example, it would reverse the entry offset coordinate. If we consider moving from 1, 3 to 0, 3 where the right edge maps to the bottom edge, we need to reverse the direction of the initial location.

..........|
..........y
......1,2.|
..........v
..........
.....
.....
.0,3.
.....
.....
 <-x-

This approach worked in the end, but I really would have liked to have automated the edge mappings.

Hopefully, I'll find the time to get back to it.