I was reading the following article about “Games on Strange Boards”. It describes various locally two-dimensional array topologies such as:
In the above diagrams, sides with the same arrows are glued together in a way that they arrows match up. Hence, if the arrows point in the same direction then they are glued normally. However, if they point in different directions then they are glued after twisting.
For example, moving off the top right edge of a cylinder will wrap you back to the top left edge. However, moving off the top right edge of a Möbius strip will wrap you back to the bottom left edge.
Now, creating cylindrical and toroidal arrays is easy. You use the modulo operation to make the rows and columns wrap around. Consider the code for calculating the coordinates of a toroidal array with m
rows and n
columns:
const mod = (x, y) => (x % y + y) % y; // floored division modulo operation
const coords = (m, n) => (i, j) => [mod(i, m), mod(j, n)]; // toroidal array
How would you calculate the coordinates of a Möbius strip, Klein bottle or projective plane? Are there any special cases to handle considering that these are non-orientable surfaces?
Consider a cylinder made from a rectangular sheet of paper. The sheet has two sides, front and back. When we glue the sheet into a cylinder, we can't reach the back side (inside of the cylinder) from the front side (outside of the cylinder). However, if we glue the sheet of paper into a Möbius strip then we can. Here's what a grid on a Möbius strip would look like if we separated the two sides and flattened it:
┌────┬────┬────┬────┰────┬────┬────┬────┐
│ a4 │ b4 │ c4 │ d4 ┃ A1 │ B1 │ C1 │ D1 │
├────┼────┼────┼────╂────┼────┼────┼────┤
│ a3 │ b3 │ c3 │ d3 ┃ A2 │ B2 │ C2 │ D2 │
├────┼────┼────┼────╂────┼────┼────┼────┤
│ a2 │ b2 │ c2 │ d2 ┃ A3 │ B3 │ C3 │ D3 │
├────┼────┼────┼────╂────┼────┼────┼────┤
│ a1 │ b1 │ c1 │ d1 ┃ A4 │ B4 │ C4 │ D4 │
└────┴────┴────┴────┸────┴────┴────┴────┘
Note that the squares on the left (i.e. the ones in lowercase) are on the front whereas the square on the right (i.e. the ones in uppercase) are on the back. Squares with only a case difference are the same square, only on opposite sides of the Möbius strip. One thing to notice is that this flattened Möbius strip is a lot like a cylinder, except that the left and right sides coincide.
Here's what the code for a Möbius strip would look like:
const mod = (x, y) => (x % y + y) % y;
const coords = (m, n) => (i, j) => {
j = mod(j, 2 * n); // wrapping around like a cylinder
if (j < n) return [i, j]; // front side
return [m - i - 1, j - n]; // back side, translated to front side
};
A Klein bottle exactly the same as a Möbius strip, except that it behaves like a torus instead of a cylinder. Here's what the code for a Klein bottle would look like:
const mod = (x, y) => (x % y + y) % y;
const coords = (m, n) => (i, j) => {
i = mod(i, m); // wrapping around
j = mod(j, 2 * n); // like a torus
if (j < n) return [i, j]; // front side
return [m - i - 1, j - n]; // back side, translated to front side
};
A projective plane also behaves like a torus. However, each of its sides can have two orientations, regular and rotated by 180°. Here's what a flattened projective plane would look like:
┌────┬────┬────┬────┰────┬────┬────┬────┐
│ a4 │ b4 │ c4 │ d4 ┃ A1 │ B1 │ C1 │ D1 │
├────┼────┼────┼────╂────┼────┼────┼────┤
│ a3 │ b3 │ c3 │ d3 ┃ A2 │ B2 │ C2 │ D2 │
├────┼────┼────┼────╂────┼────┼────┼────┤
│ a2 │ b2 │ c2 │ d2 ┃ A3 │ B3 │ C3 │ D3 │
├────┼────┼────┼────╂────┼────┼────┼────┤
│ a1 │ b1 │ c1 │ d1 ┃ A4 │ B4 │ C4 │ D4 │
┝━━━━┿━━━━┿━━━━┿━━━━╋━━━━┿━━━━┿━━━━┿━━━━┥
│ D4 │ C4 │ B4 │ A4 ┃ d1 │ c1 │ b1 │ a1 │
├────┼────┼────┼────╂────┼────┼────┼────┤
│ D3 │ C3 │ B3 │ A3 ┃ d2 │ c2 │ b2 │ a2 │
├────┼────┼────┼────╂────┼────┼────┼────┤
│ D2 │ C2 │ B2 │ A2 ┃ d3 │ c3 │ b3 │ a3 │
├────┼────┼────┼────╂────┼────┼────┼────┤
│ D1 │ C1 │ B1 │ A1 ┃ d4 │ c4 │ b4 │ a4 │
└────┴────┴────┴────┸────┴────┴────┴────┘
So, here's what the code for a projective plane would look like:
const mod = (x, y) => (x % y + y) % y;
const coords = (m, n) => (i, j) => {
i = mod(i, 2 * m); // wrapping around
j = mod(j, 2 * n); // like a torus
if (i >= m) { // collapse to Klein bottle topology
i -= m;
j = mod(n - j - 1, 2 * n);
}
if (j < n) return [i, j]; // front side
return [m - i - 1, j - n]; // back side, translated to front side
};
Hope that helps.