Logical Enum-ification of edges of a 2x2 square

I’m having trouble coming up with a logical way to represent the edges (the rainbow blotches) as integers. I don’t want to just do some clockwise/counter clockwise representation because I think that’s not very thoughtful and I’m also willing to skip numbers. They can be negative (although I think thats nasty too), should have some relation with the cartesian coordinates of the edge (however that will be represented), and be memorable. I think it would be cleanest if they were represented with the combination of only 1s and 0s too

I guess they aren’t really enums

I’d like to avoid counting the non external edges of the 1x1 squares that make up the big square too

If you like just using 1’s and 0’s, then what you have here is perfect because there’s 8 total things to represent

How about this
First digit = horizontal or vertical
Second digit = Left/Right orientation
Third digit = Up/Down orientation

so:

2 Likes

I was hoping to do 2 digits with base 2 (fail xd)
Kind of a bonus question but how would you do it base 3 2 digits?

It doesn’t really make sense to use base 3 with the dyadic structure of that square, the way I see it. (Obviously you’d end up with an extra code, for instance.) You could just replace the binary representation with its ternary equivalent, but it wouldn’t be as natural, I suppose.

1 Like

I don’t see an easy way, but you could have first digit (being a 0, 1, or 2) represent if the edge was on the top, one of the sides, or the bottom. The problem we run into is that there are two possible edge locations on top, two and bottom, and four in the middle when we can only represent 3 possible locations with another digit. I’d have the top left be 00, top right be 01, and then let the right side upper edge be 02. On the bottom, 20 would be the bottom right, 21 the bottom left, and 22 actually be the left side lower edge. In the middle 10 is the left upper edge, 11 isn’t assigned (it’s the middle) and 12 is the right side lower edge.

Now all of that being said, if I was actually using this in a game, I would have all the edges in a circular linked list with the unary operator metamethod returning the edge on the opposite side. If it would be helpful in your particular use case, you could also just have 4 edges with clockwise and counterclockwise sub-edges.

1 Like

Do you mean something like base 4 first digit and base 2 second digit?

Also what if the base 3 restriction were removed (so can have up to base 8any base) but maximum of 2 digits is still retained?

What I was suggesting in the second half of my response is not having to restrict ourselves to defining an enumeration as integer values, but rather as distinct tables, each with varying properties describing the edge in relation to the others. Like so:

local Invertible = {}
setmetatable(Invertible, Invertible)

function Invertible.new(t)
	return setmetatable(t, invertible)
end

function Invertible:__unm()
	return self.ACROSS
end

local EDGES = {
	TOP = Invertible.new {
		SUB_CLOCKWISE = {};
		SUB_COUNTERCLOCKWISE = {};
	};
	RIGHT = Invertible.new {
		SUB_CLOCKWISE = {};
		SUB_COUNTERCLOCKWISE = {};
	};
	BOTTOM = Invertible.new {
		SUB_CLOCKWISE = {};
		SUB_COUNTERCLOCKWISE = {};
	};
	LEFT = Invertible.new {
		SUB_CLOCKWISE = {};
		SUB_COUNTERCLOCKWISE = {};
	};
}

EDGES.TOP.LEFT = EDGES.LEFT
EDGES.TOP.RIGHT = EDGES.RIGHT
EDGES.TOP.ACROSS = EDGES.BOTTOM

EDGES.RIGHT.LEFT = EDGES.TOP
EDGES.RIGHT.RIGHT = EDGES.BOTTOM
EDGES.RIGHT.ACROSS = EDGES.LEFT

EDGES.BOTTOM.LEFT = EDGES.RIGHT
EDGES.BOTTOM.RIGHT = EDGES.LEFT
EDGES.BOTTOM.ACROSS = EDGES.TOP

EDGES.LEFT.LEFT = EDGES.BOTTOM
EDGES.LEFT.RIGHT = EDGES.TOP
EDGES.LEFT.ACROSS = EDGES.RIGHT

Now I’m not sure what your use case is (something like this could easily be overkill) but I’ve found that breaking things down like this helps me. For example in one game I’m working on right now, I have to determine what direction (up, down, left, right, forward, backward) a thruster block is placed on a ship so that I know which thrusters to use when the player flies it and presses w (forward). I also need to be able to easily convert this into local x-y-z vectors, and need to frequently find the inverse vector/direction when applying inertia dampening using the opposite thrusters. Sometimes when a cockpit block is taken off and put on in a different direction, I needed to apply clockwise rotations around the different axis’s to match the new orientation. What I ended up with is this code (feel free to use in your personal projects):

local abs = math.abs
local Iterable = shared.Iterable
local negatable = {
	__unm = function(self)
		return self.neg
	end
}

local FORWARD = setmetatable({
	dir = 'Z';
	sign = 1;
	vector = Vector3.new(0, 0, 1);
}, negatable)
local BACKWARD = setmetatable({
	dir = FORWARD.dir;
	sign = -FORWARD.sign;
	vector = -FORWARD.vector;
}, negatable)
local UP = setmetatable({
	dir = 'Y';
	sign = 1;
	vector = Vector3.new(0, 1, 0);
}, negatable)
local DOWN = setmetatable({
	dir = UP.dir;
	sign = -UP.sign;
	vector = -UP.vector;
}, negatable)
local LEFT = setmetatable({
	dir = 'X';
	sign = 1;
	vector = Vector3.new(1, 0, 0);
}, negatable)
local RIGHT = setmetatable({
	dir = LEFT.dir;
	sign = -LEFT.sign;
	vector = -LEFT.vector;
}, negatable)

FORWARD[FORWARD] = FORWARD;
FORWARD[BACKWARD] = FORWARD;
FORWARD[UP] = RIGHT;
FORWARD[DOWN] = -FORWARD[UP];
FORWARD[LEFT] = UP;
FORWARD[RIGHT] = -FORWARD[LEFT];

BACKWARD[FORWARD] = BACKWARD;
BACKWARD[BACKWARD] = BACKWARD;
BACKWARD[UP] = FORWARD[DOWN];
BACKWARD[DOWN] = FORWARD[UP];
BACKWARD[LEFT] = FORWARD[RIGHT];
BACKWARD[RIGHT] = FORWARD[LEFT];

UP[FORWARD] = LEFT;
UP[BACKWARD] = -UP[FORWARD];
UP[UP] = UP;
UP[DOWN] = UP;
UP[LEFT] = BACKWARD;
UP[RIGHT] = -UP[LEFT];

DOWN[FORWARD] = UP[BACKWARD];
DOWN[BACKWARD] = UP[FORWARD];
DOWN[UP] = DOWN;
DOWN[DOWN] = DOWN;
DOWN[LEFT] = UP[RIGHT];
DOWN[RIGHT] = UP[LEFT];

LEFT[FORWARD] = DOWN;
LEFT[BACKWARD] = -LEFT[FORWARD];
LEFT[UP] = FORWARD;
LEFT[DOWN] = -LEFT[UP];
LEFT[LEFT] = LEFT;
LEFT[RIGHT] = LEFT;

RIGHT[FORWARD] = LEFT[BACKWARD];
RIGHT[BACKWARD] = LEFT[FORWARD];
RIGHT[UP] = LEFT[DOWN];
RIGHT[DOWN] = LEFT[UP];
RIGHT[LEFT] = RIGHT;
RIGHT[RIGHT] = RIGHT;

FORWARD.neg = BACKWARD
BACKWARD.neg = FORWARD
UP.neg = DOWN
DOWN.neg = UP
LEFT.neg = RIGHT
RIGHT.neg = LEFT

local Operations = {}
Operations.__index = Operations
Operations.__call = next

function Operations:__unm()
	local ops = setmetatable({}, Operations)
	local len = #self
	for i, op in self do
		ops[len - i + 1] = -op
	end
	return ops
end

local Direction = {
	VALUES = Iterable {
		FORWARD = FORWARD;
		BACKWARD = BACKWARD;
		UP = UP;
		DOWN = DOWN;
		LEFT = LEFT;
		RIGHT = RIGHT;
	};
}
Direction.__index = Direction

function Direction.new()
	return setmetatable({
		primaryAxis = FORWARD;
		secondaryAxis = UP;
		tertiaryAXis = LEFT;
	},Direction)
end

function Direction.getDir(vector)
	local v1 = FORWARD:Dot(vector)
	local v2 = UP:Dot(vector)
	local v3 = LEFT:Dot(vector)
	if abs(v1) > abs(v2) then
		if abs(v1) > abs(v3) then
			if v1 > 0 then
				return FORWARD
			else
				return BACKWARD
			end
		elseif v3 > 0 then
			return LEFT
		else
			return RIGHT
		end
	elseif abs(v2) > abs(v3) then
		if v2 > 0 then
			return UP
		else
			return DOWN
		end
	elseif v3 > 0 then
		return LEFT
	else
		return RIGHT
	end
end

function Direction.newFromCFrame(cframe)
	return setmetatable({
		primaryAxis = Direction.getDir(cframe:vectorToWorldSpace(FORWARD.vector));
		secondaryAxis = Direction.getDir(cframe:vectorToWorldSpace(UP.vector));
		tertiaryAXis = Direction.getDir(cframe:vectorToWorldSpace(LEFT.vector));
	},Direction)
end

function Direction:getOperations(fromDirection)
	local operations = {}
	
	local from1 = fromDirection.primaryAxis
	local from2 = fromDirection.secondaryAxis
	local from3 = fromDirection.tertiaryAxis
	
	local to1 = self.primaryAxis
	if from1 == to1 then
	elseif from1[from2] == to1 then
		operations[1] = from2
	elseif from1[-from2] == to1 then
		operations[1] = -from2
	elseif from1[from3] == to1 then
		operations[1] = from3
	elseif from1[-from3] == to1 then
		operations[1] = from3
	else
		operations[1] = from2
		operations[2] = from2
	end
	
	local to2 = self.secondaryAxis
	if from2 == to2 then
	elseif from2[from1] == to2 then
		operations[#operations + 1] = from1
	elseif from2[-from1] == to2 then
		operations[#operations + 1] = -from1
	elseif from2[from3] == to2 then
		operations[#operations + 1] = from3
	elseif from2[-from3] == to2 then
		operations[#operations + 1] = -from3
	else
		operations[#operations + 1] = from1
		operations[#operations + 1] = from1
	end
	
	return operations
end
	
function Direction:applyOperations(operations)
	for i, operation in ipairs(operations) do
		self.primaryAxis = self.primaryAxis[operation]
		self.secondaryAxis = self.secondaryAxis[operation]
		self.tertiaryAxis = self.tertiaryAxis[operation]
	end
end
2 Likes

But if I was going to restrict the enum to 2 digits of any base, I’d say either base 4. It must be a higher base than 2 to represent all those edges, and 8 or higher can always represent all the edges in a single digit. Bases 3, 5, 6, and 7 don’t divide easily. That leaves 4 for two digits or base 8 or higher otherwise with a single digit.

1 Like

How would you structure it if you were going to type out the “Enums” over a hundred times?

I’d create loops that took advantage of geometric patterns to initialize the enumerations. Anything that could be computed at runtime in this case can be precomputed and stored as helpful information in the enumeration.

I’m defining active edges for certain structures so that can’t really be generalized

The initialization doesn’t have to take advantage of patterns or generalizations, although it is nice if it is possible to. Anything that can be computed given the structure and no additional input can be precomputed (even if that means the hard way without helpful patterns) if the structure is known during initialization. I can’t say much more without knowing specific details. “active edges” sounds like the maze generation algorithms and pathfinding algorithms I’ve worked on. You can send me a PM if the details are secret and would like some of my input.

Details aren’t really secret but as of right now I’m not really worrying about what I can optimize in run time, but how to minimize the amount of time I will spend actually inputting the bare minimum of what is necessary (I realize these two are interconnected but emphasis on “bare minimum” because I will be making as many optimizations as possible)

So that’s why I’m looking for a practical method for representing the edges that does not take much effort to type nor think about its meaning

You could just list them in an arbitrary order and memorize the list?

2 Likes