Skip to content

Thad writes blogs

this is a lot more work than i expected

Menu
  • About
  • Pin Posts
Menu

Advent of Code in Unreal Engine Day 22

Posted on December 23, 2022December 23, 2022 by Thad

I hope you like edge cases, because Day 22 is all about the edge cases. Lets wrap around…

Starting with Part 1 I follow a now familiar pattern: Create a new Actor class, and a new Unreal Test Class, both in C++, and write some tests using IMPLEMENT_SIMPLE_AUTOMATION_TEST that closely follow the puzzle example:

bool AoC2022D22_Test::RunTest(const FString& Parameters)
{
	TArray<FString> Example={
		TEXT("        ...#"),
		TEXT("        .#.."),
		TEXT("        #..."),
		TEXT("        ...."),
		TEXT("...#.......#"),
		TEXT("........#..."),
		TEXT("..#....#...."),
		TEXT("..........#."),
		TEXT("        ...#...."),
		TEXT("        .....#.."),
		TEXT("        .#......"),
		TEXT("        ......#."),
		TEXT(""),
		TEXT("10R5L5R10L4R5L5")		
	};

	auto grid = AD22Actor::CreateGrid(Example);
	FD22Cursor current = AD22Actor::GetInitial(grid);
	TestEqual(TEXT("Initial position is on 1st Row"), current.Pos.Y, 0.0);
	TestEqual(TEXT("Initial position is on 9th Column"), current.Pos.X, 8.0);
	TestEqual(TEXT("Initial direction is Right"), current.GetDirChar(), TCHAR('>'));

	grid.Log();
	FD22Cursor result = AD22Actor::Follow(grid,current,Example.Last());
	grid.Log();
	
	int Row = result.Pos.Y+1;
	int Column = result.Pos.X+1;

	TestEqual(TEXT("In the above example, the final row is 6"), Row, 6);
	TestEqual(TEXT("... the final column is 8"), Column, 8);
	TestEqual(TEXT("... the final facing is 0"), result.GetFacing(), 0);
	
	// The final password is the sum of 1000 times the row, 4 times the column, and the facing.
	TestEqual(TEXT("So, the final password is 1000 * 6 + 4 * 8 + 0: 6032."), result.Password(), 6032);
	return true;
}

In writing my Tests I discover I want Cursor and Grid types, to hold the current state of the head of the path. I define them as structs with the Actor:

// in the header file

USTRUCT(BlueprintType)
struct FD22Cursor
{
	GENERATED_BODY()
	FVector2d Pos;
	FVector2d Dir;
	TCHAR GetDirChar()const;
	int GetFacing() const;
	int Password() const;
};

// in the .cpp

TCHAR FD22Cursor::GetDirChar() const
{
	if (Dir.X>0) return TCHAR('>');
	if (Dir.X<0) return TCHAR('<');	
	if (Dir.Y>0) return TCHAR('v');
	if (Dir.Y<0) return TCHAR('^');
	
	return TCHAR('o');
}

int FD22Cursor::GetFacing() const
{
	// Facing is 0 for right (>), 1 for down (v), 2 for left (<), and 3 for up (^).
	if (Dir.X>0) return 0;
	if (Dir.X<0) return 2;	
	if (Dir.Y>0) return 1;
	if (Dir.Y<0) return 3;	

	return -1;
}

int FD22Cursor::Password() const
{
	int Row = Pos.Y+1;
	int Column = Pos.X+1;
	return 1000 * Row + 4 * Column + GetFacing();
}

My Grid uses a single buffer and provides some very basic off-edge behaviors to help keep my path tracing loop light on bounds checking:

USTRUCT(BlueprintType)
struct FD22Grid
{
	GENERATED_BODY()
	
	int Width=0;
	int Height=0;	
	TArray<TCHAR> Data;

	void Set(int x, int y, TCHAR c) { Data[x+y*Width]=c; }
	bool Set(FVector2d Pos, TCHAR c)
	{
		if (Pos.X>=Width || 
			Pos.Y>=Height ||
			Pos.X<0 ||
			Pos.Y<0) return false;
		
		Set(Pos.X,Pos.Y,c);
		return true;
	}
	
	TCHAR Get(int x, int y) const { return Data[x+y*Width]; }
	TCHAR Get(FVector2d Pos) const
	{
		// infinite blank in all directions
		if (Pos.X>=Width || 
			Pos.Y>=Height ||
			Pos.X<0 ||
			Pos.Y<0) return ' ';
		
		return Get(Pos.X,Pos.Y);
	}
	void Log();
};

Creating the Grid and Getting the Initial Cursor position are straightforward input handling:

FD22Grid AD22Actor::CreateGrid(const TArray<FString>& Lines)
{
	int i,j=0;
	for (i=0;i!=Lines.Num();++i)
	{
		if (Lines[i].IsEmpty()) break;
		if (Lines[i].Len()>j) j=Lines[i].Len();
	}
	FD22Grid Result;
	Result.Width=j;
	Result.Height=i;
	Result.Data.Init(TCHAR(' '), i*j);
	for (i=0;i!=Result.Height;++i)
	{
		for (j=0;j!=Lines[i].Len();++j)
		{
			Result.Set(j,i,Lines[i][j]);
		}
	}
	return Result;
}

FD22Cursor AD22Actor::GetInitial(const FD22Grid& Grid)
{
	// You begin the path in the leftmost open tile of the top row of tiles.
	int Row=0;
	int Column=0;
	while (TCHAR(' ')==Grid.Get(Column, Row )) Column++;
	
	FD22Cursor Initial;
	Initial.Pos.X=Column;
	Initial.Pos.Y=Row;
	// Initially, you are facing to the right
	Initial.Dir=FVector2D(1,0);

	return Initial;
}

Turning a 90 rotation for a 2D Vector can be implemented with a X Y swap and negate one, depending on direction:

FVector2d AD22Actor::Turn(const FVector2d& V, TCHAR D)
{
	if (D==TCHAR('R'))
	{
		return FVector2d(-V.Y, V.X);
	}
	else
	{
	    ensure(D==TCHAR('L'));	    
		return FVector2d(V.Y, -V.X);
	}
}

Taking a single move the key detail is to work the Next after wrapping before updating the position, so wall-blocking works around edges:

FD22Cursor AD22Actor::Move(FD22Grid& Grid, const FD22Cursor& Start, int Steps)
{
	FD22Cursor Result = Start;
	while(Steps--)
	{
		FVector2d Next = Result.Pos+Start.Dir;

		// continue to step through the blank spaces with wrap around:
		while(Grid.Get(Next)==TCHAR(' '))
		{
			if (Next.X<0) Next.X+=Grid.Width;
			else if (Next.X>=Grid.Width) Next.X-=Grid.Width;
			else if (Next.Y<0) Next.Y+=Grid.Height;
			else if (Next.Y>=Grid.Height) Next.Y-=Grid.Height;
			else Next+=Start.Dir;
		}
				
		if (Grid.Get(Next)==TCHAR('#')) break;
		
		Grid.Set(Result.Pos, Result.GetDirChar());
		Result.Pos=Next;
	};
	
	return Result;
}

Also note, writing the path back into the grid is for debugging / validation.

The Move method is then used to write the path follow. The ReadNum you may recognize from Day 21. It is modified here to take the i parameter by reference, so we can use it as an iterator for parsing number/turn-letter pairs and advance through the path:

FD22Cursor AD22Actor::Follow(FD22Grid& Grid, const FD22Cursor& Start, const FString& Path)
{
	int i=0;
	FD22Cursor Current = Start;
	while(i<Path.Len())
	{
		int Steps = ReadNum(Path,i);
		Current = Move(Grid,Current,Steps);
		if (i<Path.Len()) Current.Dir = Turn(Current.Dir, Path[i++]);
	}
	return Current;
}

And that is Part 1 complete. Part 2 explains the map is a Cube that has been unwrapped, and the wrap around should be implemented to move the cursor from face to face.

In a move that might be described as sadistic, the example problem is unfolded with a different face positioning to my problem data. I decide to hard-code my solution to the layout I am given:

7 face-links. Each one bi-directional, each one with 2 axis, each one providing different opportunities for off-by-one errors. Working through each one carefully took some time. I made several mistakes, and a mixture of unit tests, assertions, and visually inspecting the paths from the logging helped me identify.

A modified “Move” handles the different ways you can move between faces. This is not elegant code:

FD22Cursor AD22Actor::CubeMove(FD22Grid& Grid, const FD22Cursor& Start, int Steps)
{
	FD22Cursor Result = Start;
	while(Steps--)
	{
		FVector2d NextDir = Result.Dir;
		FVector2d Next = Result.Pos+Result.Dir;
		if (Grid.Get(Next)==TCHAR(' '))
		{			
			if (Next.X==-1)
			{
				ensure (Next.Y>=100);
				if (Next.Y<150)
				{
					// 0,2 -> 1.0
					Next.X = 50;
					Next.Y = 49 - (Next.Y-100);
					NextDir = FVector2d(1,0);					
				}
				else
				{
					//0,3 -> 1,0
					Next.X = 50 + (Next.Y-150);
					Next.Y = 0;					
					NextDir = FVector2d(0,1);
				}
			}
			else if (Next.X==49)
			{
				ensure (Next.Y<100);
				if (Next.Y<50)
				{
					// 1,0 -> 0,2
					Next.X = 0;
					Next.Y = 149 - Next.Y;
					NextDir = FVector2d(1,0);					
				}
				else
				{
					//1,1 -> 0,2
					Next.X = Next.Y-50;
					Next.Y = 100;					
					NextDir = FVector2d(0,1);
				}
			}
			else if (Next.X==50 && Next.Y>149)
			{
				// 0,3 -> 1,2
				Next.X = 50 + (Next.Y - 150);
				Next.Y = 149;
				NextDir = FVector2d(0,-1);				
			}
			else if (Next.X==100 && Result.Dir.X==1)
			{
				if (Next.Y<100)
				{
					// 1,1 -> 2,0
					Next.X = 100 + (Next.Y-50);
					Next.Y = 49;
					NextDir = FVector2d(0,-1);
				}
				else
				{
					// 1,2 -> 2,0
					Next.X = 149;
					Next.Y = 49 - (Next.Y-100);
					NextDir = FVector2d(-1,0);
				}
			}
			else if (Next.X==150)
			{
				// 2,0 -> 1,2
				Next.X = 99;
				Next.Y = 149 - Next.Y;
				NextDir = FVector2d(-1,0);
			}
			else if (Next.Y==-1)
			{
				if (Next.X<100)
				{
					// 1,0 -> 0,2
					Next.Y = 150 + Next.X-50;
					Next.X = 0;
					NextDir = FVector2d(1,0);
				}
				else
				{
					// 2,0 -> 0,3
					Next.Y = 199;
					Next.X = Next.X - 100;
					NextDir = FVector2d(0,-1);					
				}
			}
			else if (Next.Y==50)
			{
				// 2,0 -> 1,1
				Next.Y = 50 + Next.X-100;
				Next.X = 99;				
				NextDir = FVector2d(-1,0);
			}
			else if (Next.Y==99)
			{
				// 0,2 -> 1,1
				Next.Y = 50 + Next.X;
				Next.X = 50;				
				NextDir = FVector2d(1,0);
			}
			else if (Next.Y==150)
			{
				// 1,2 -> 0,3
				Next.Y = 150 + Next.X-50;
				Next.X = 49;				
				NextDir = FVector2d(-1,0);
			}
			else if (Next.Y==200)
			{
				// 0,3 -> 2,0
				Next.Y = 0;
				Next.X = Next.X + 100;
			}
			else
			{
				UE_LOG(LogTemp, Error, TEXT("%i,%i Missing Edge Case Heading %c"),
					Next.X, Next.Y, Result.GetDirChar());	
			}

		}
				
		if (Grid.Get(Next)==TCHAR('#')) break;
		ensure(Grid.Get(Next)!=' ');
		
		if (Grid.Set(Result.Pos, Result.GetDirChar())==false)
		{			
			Grid.Log();
			UE_LOG(LogTemp, Error, TEXT("%i,%i Out of Bounds heading %c"),
				Result.Pos.X, Result.Pos.Y, Result.GetDirChar());
			break;
		}
		
		Result.Pos=Next;
		Result.Dir=NextDir;
	};
	
	return Result;
}

The line “ensure(Grid.Get(Next)!=’ ‘);” was critical to me finding all the bugs, as was testing the travel in a semi procedural test:

	FVector2D a[]={{1,0},{1,0},{2,0},{2,0},{2,0},{1,1},{0,3}};
	FVector2D b[]={{0,3},{0,2},{0,3},{1,1},{1,2},{0,2},{1,2}};
	FVector2D d[]={{0,-1},{-1,0},{0,-1},{0,1},{1,0},{-1,0},{1,0}};
	for (int i=0;i!=sizeof(a)/sizeof(FVector);++i)
	{
		auto test = MakePart2Grid();
		FD22Cursor c;
		c.Dir = d[i];
		c.Pos = a[i] * 50;
		c = AD22Actor::CubeMove(test, c,50);
		TestEqual(*FString::FromInt(i), (int)c.Pos.X/50,(int)b[i].X);
		TestEqual(*FString::FromInt(i), (int)c.Pos.Y/50,(int)b[i].Y);
		test.Log();
	}

Can you think of a better way? Let me know here or on Mastodon.

1 thought on “Advent of Code in Unreal Engine Day 22”

  1. Pingback: Merry Xmas, Day 25 of Advent of Code in Unreal Engine

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Archives

  • October 2024
  • January 2023
  • December 2022
  • November 2022

Categories

  • Food
  • meta
  • Uncategorized
© 2025 Thad writes blogs | Powered by Minimalist Blog WordPress Theme