0
\$\begingroup\$

I made a simple Snake game designed for trying Snake bots. I thought the base map functions work, but now I found there is an error. What I need is to properly translate any coordinates outside the actual map to coordinates within it.

For example, if you're at the right edge of a 10x10 map and you go to (zero-indexed) [10, 5], you'd expect to pop out at [0, 5]. That works in my implementation, but not the negative overflow. I tried a lot of formulas in the dev console and just can't figure it out. Consider this code:

    /**
     * Normalizes overflow of a point so that it is within the map
     * @param {number|Vector2} x
     * @param {number} y
     * @returns {Vector2}
     */
    normalizePoint(x, y) {
        if (x instanceof Vector2) {
            y = x.y;
            x = x.x;
        }

        // auto overflow
        if (x >= this.size) {
            x = (x % this.size);
        }
        else if (x < 0) {
            // ???
        }
        if (y >= this.size) {
            y = (y % this.size);
        }
        else if (y < 0) {
            // one of the formulae I tried, does not work for all values
            y = this.size + (y-1) % this.size;
        }
        return new Vector2(x, y);
    }

Vector2 is a simple object with properties x and y. this.size represents the map size. For negative coordinates, I am not getting the correct results. I want overflowing X and Y coords to resolve to the same numbers. For example for a 5x5 map, all rows should be equal X coords after normalizing:

Real map coord X: 0 1 2 3 4
Negative alternatives: -5 -4 -3 -2 -1
Negative alternatives: -10 -9 -8 -7 -6

I would prefer it to be a O(1) formula, rather than some weird loop. With a loop, I solved it like this:

if (x < 0) {
            while (x < 0) {
                x += this.size;
            }
        }

```
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

If you need to wrap just a few steps out of bounds (up to negative size), then you can do this like so:

(size + coordinate) % size

When coordinate = -1 then this gives you (size - 1) % size = size - 1.

And when coordinate = size then this gives you (size + size) % size = 0.

For any value in between (0 to size - 1), this behaves the same as the identity function, giving you coordinate back unchanged.

So you can safely use this one formula for all cases, with no if branching required to change behaviour depending on whether you're overflowing, underflowing, or in-range.


If you need to wrap further than negative size, you can increase the multiple of size you add inside the parentheses:

(3 * size + coordinate) % size

This will wrap correctly from -3 * size up to the maximum representable integer, and you can keep growing that coefficient if you need.


If you have no bound on how far your coordinates can go in the negative direction, then you can do it with a double modulo:

((coordinate % size) + size) % size

The inner modulo brings us into the range of -(size-1) to (size -1) and then we apply the original formula around it to bring us to 0 to size - 1.


Just to note, this is not circular wrapping, but toroidal. Your map area remains a rectangle, not a circle.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks! This is awesome, now I also do not have to copy the point object to avoid messy code since I can normalize the coordinates with this one liner. \$\endgroup\$ Apr 27, 2021 at 22:46
  • \$\begingroup\$ I found out that this solution does not work properly. For size 10 and coordinate -11, you get -1, instead of 9. \$\endgroup\$ Apr 30, 2021 at 9:51
  • 1
    \$\begingroup\$ It works exactly properly in the range I described. If you need to go further into the negatives, you modify it as shown above. \$\endgroup\$
    – DMGregory
    Apr 30, 2021 at 12:27

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .