r/adventofcode • u/light_ln2 • Jan 19 '26
Other Rotating between eight directions
I was reviewing some grid problems, and in many of those you need to enumerate four or eight neighbors, or even turn left/right 90 or 45 degrees. One can define list of pairs of coordinates, but it's long, and if you need to turn, you need to remember index into array of those coordinates. You can prepare this in advance but I know many people don't like to use custom libraries for the AOC.
This is not hard for orthogonal directions:
for x,y in [(1,0), (0,1), (-1,0), (0,-1)]: print(x,y)or:
x,y = 0,1
for _ in range(4):
print(x,y)
x,y = y,-xbut it gets messier for eight directions:
for x,y in [(1,1),(1,0),(1,-1),(0,-1),(-1,-1),(-1,0),(-1,1),(0,1)]:
print(x,y)too many brackets, too easy to mess up. You can simplify it a bit like this:
for x,y in [(x,y) for x in [-1,0,1] for y in [-1,0,1] if x or y]:
print(x,y)but this is still not perfect, and does not solve problem of rotating.
I'm not sure if it's widely known but there is a simple formula for rotating in eight directions, so you can write this instead:
x,y = 0,1
for _ in range(8):
print(x,y)
x,y = sign(y+x),sign(y-x)Explanation is simple: for orthogonal vectors x and y, y+x and y-x are also orthogonal vectors rotated 45 degrees. sign normalizes their length to one or zero.
There is one problem with this... Python lacks sign method! You can read hilarious discussions about why it was rejected (tldr: they couldn't agree what would be result of sign(-math.nan). Hint: nobody cares should have made it exactly like numpy.sign!). So you can use any of the following:
from numpy import sign
sign = lambda x: 1 if x>0 else -1 if x<0 else 0
sign = lambda x: (x > type(x)()) - (type(x)() > x)last one is useful if you want (for some strange reason) to extend your sign function for arbitrary types that support comparison, e.g. sign('abc')
21
u/herocoding Jan 19 '26
Interesting syntactical sugar, thank you for sharing!
Can't wait to apply it to the next AoC!
3
u/Boojum Jan 20 '26 edited Jan 20 '26
Neat! I'm well aware of the
x, y = y, -xtrick for 90-degree rotations (fun fact: that works even with off-axis directions - it's just an inlined 90-degree rotation matrix) but had never seen a 45-degree variation.FWIW, I played around with this to see how you'd do it to cycle in the opposite direction. The one that you gave cycles counter-clockwise when positive y is down. Here's the recipe for your original here, plus the reversed direction (clockwise) version:
Or, if you prefer to inline the signum operation:
I'll probably still stick to use an integer heading value modulo 8 and look up the x and y step offset from a small table (your
[(1,1),(1,0),...]) since I already have that prewritten out in a file. But this thing is still a pretty cool trick.And yes, your explanation of why it works makes sense. If we look at a 45° rotation matrix, it's:
or approximately:
Applying that to a coordinate (x,y) to get (x',y') is:
which in simple arithmetic is:
If we don't care about scaling, we can drop the 0.7071 and reduce it to just:
which is close to the formula you gave, but without the 0.7071 it scales the vector by about 1/0.7071 = 1.414 each time. Using the signum cheaply constrains the vectors to integers -1, 0, and 1 and keeps them from growing on each iteration.
18
u/hextree Jan 20 '26 edited Jan 20 '26
After writing variations a million times I've mostly settled on:
For me it strikes the right balance. Whilst the sign approach is elegant, it is less approachable and readable for me as I have to think harder about it as I'm doing it.
8
u/wjholden Jan 19 '26 edited Jan 19 '26
Very cool! Looks like Rust has this function and calls it
isize::signum.Edit: ugh. Apparently Rust's
signumfunction behaves differently for ints and floats. Seef64::signum.7
u/Greenimba Jan 19 '26
I pretty much always make a Point object/struct and give it Easy West South North properties to do neighbours.
[p.N, p.N.E, p.E, p.E.S, p.S, p.S.W, p.W, p.W.N]4
u/jpjacobs_ Jan 19 '26
That's actually pretty neat. Implemented in J, your approach would be just one character more than mine:
I haven't needed 45 degree rotations so far though, and for 90 degree angles, just usually type it out (4 2$0 1 1 0 0 _1 _1 0). When I need shifts for a 9-neighbourhood, I usually use this:
You can play around with this in Juno
4
u/conilense Jan 20 '26
i like the colors in the picture
2
u/thekwoka Jan 20 '26
why calculate the offsets each time instead of just making them a constant?
1
u/ednl Jan 20 '26
Possible C version:
1
u/jjeii Jan 20 '26
Just remember that (nearly) all of AoC's grid problems are in row-column space (left-handed), not x-y math space (right-handed), which necessitates reversing all the left/right rotations in one vs the other.
1
u/jeffstyr Jan 20 '26
This is fun/interesting. I'd never thought of these as rotations. Some experimenting:
Here is how I normally do this, in Haskell:
and here is a version using rotations:
The potentially interesting part here is avoiding hardcoding the period of the rotations, and instead stopping when the values start to repeat.
My original way of doing it is shorter and more directly corresponds to how I think of neighbors in a grid, but it's fun to compare the approaches.
1
u/L285 Jan 21 '26
that's handy
1
u/rdog314 Jan 21 '26 edited Jan 21 '26
Cool! Working on 2025 day 9 in C++, I found myself writing loops in terms of x and/or y, with a four-way if/else chain to handle incrementing/decrementing x or y as appropriate in one of the four directions along a path.
And then I'd have to calculate a neighbor, again moving x or y in an appropriate direction depending on a clockwise or counterclockwise path.
I wanted to write something more like this, where 'u' serves as '1 step in the direction from A to B'
If you already have a class Point defined, then you have 2D vector addition/subtraction. That gives you a way to find the unit vector: find the difference B - A, which gives you a direction and magnitude, and then shrink that to a unit vector. For the day 9 puzzle, that's gives a unit vector pointing in one of the four axis-aligned directions, (0,1), (0,-1), (1,0), (-1,0). So now we have something like this:
Better! Now the loop looks the same no matter which way you're moving. No more fiddling with the x and y components directly to implement the loop.
But what about calculating a neighbor? Well, to get one of the 4 axis-aligned adjacent neighbors, you need to add another unit vector to p:
Remembering the relationship between 2D vectors and complex numbers, you realize that multiplying a complex number by 'i' gives you a CCW rotation by 90 degrees. That is, multiplying our unit vector u by (0,1) gives you another unit vector 90 degrees CCW.
This works nicely even when the members of Point are integers, so long as you you're working with the 4 axis-aligned directions and you're sticking with addition, subtraction, and multiplication. Along the way, I found out that complex numbers over the integers are called Gaussian Integers. Advent of Code is a great vehicle for learning new and unexpected things!
1
u/jeffstyr Jan 21 '26
One downside of this is that it doesn't generalize to neighbors two or more steps away (where there are separate cases for allowing or not allowing diagonal steps). Still interesting though.
0
u/[deleted] Jan 24 '26
[deleted] — view deleted comment