Day 22 is about moving around on a map. The part 2 twist maps the map onto the edges of a cube.
I started with a class to handle the map called MonkeyGrove. The only tricky part was handling the “weird” wrapping rules. The parsing apart the movement instructions is also slightly tricky, but just takes a very simple state machine.
final class MonkeyGrove
{
private final int width;
private final int height;
private char[][] map;
private Coordinates playerCoordinates;
private int playerFacingDirection;
public MonkeyGrove(final int width, final int height)
{
this.width = width;
this.height = height;
map = new char[width][height];
playerCoordinates = Coordinates.INVALID_COORDINATES;
}
public void applyMap(final String[] mapData)
{
clearMap();
for (int y=0; y<mapData.length; y++)
{
for (int x=0; x<mapData[y].length(); x++)
{
final char c = mapData[y].charAt(x);
switch(c)
{
case ' ': break;
case '.':
{
map[x][y] = c;
if (playerCoordinates == Coordinates.INVALID_COORDINATES)
{
playerCoordinates = new Coordinates(x,y);
}
}
break;
case '#':
{
map[x][y] = c;
}
break;
default: throw new IllegalStateException("Invalid input map char: " + c);
}
}
}
}
private void clearMap()
{
for (int y=0; y<height; y++)
for (int x=0; x<width; x++)
map[x][y] = ' ';
}
public void applyMovements(final String movementInstructions)
{
boolean doneMoving = false;
int index = 0;
String digits = "";
while (!doneMoving)
{
if (index >= movementInstructions.length())
{
doneMoving = true;
}
final char c;
if (doneMoving)
c = ' ';
else
c = movementInstructions.charAt(index);
switch (c)
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
{
digits = digits + c;
}
break;
case 'L':
{
move(Integer.parseInt(digits));
digits="";
playerFacingDirection = playerFacingDirection - 1;
if (playerFacingDirection < 0) playerFacingDirection = 3;
}
break;
case 'R':
{
move(Integer.parseInt(digits));
digits="";
playerFacingDirection = playerFacingDirection + 1;
if (playerFacingDirection > 3) playerFacingDirection = 0;
}
break;
case ' ':
{
if (!digits.isEmpty())
{
move(Integer.parseInt(digits));
digits="";
}
}
break;
default: throw new IllegalStateException("Got invalid movement char: " + c);
}
index++;
}
}
private void move(final int numberToMove)
{
for (int i=0; i<numberToMove; i++)
{
if (!moveToNextSpace()) break;
}
}
private boolean moveToNextSpace()
{
final Coordinates oldCoordinates = playerCoordinates.clone();
switch (playerFacingDirection)
{
case 0: playerCoordinates = getNextValidCoordinates(1,0); break;
case 1: playerCoordinates = getNextValidCoordinates(0,1); break;
case 2: playerCoordinates = getNextValidCoordinates(-1,0); break;
case 3: playerCoordinates = getNextValidCoordinates(0,-1); break;
default: throw new IllegalStateException("Player is facing in an invalid direction");
}
int x = playerCoordinates.getX();
int y = playerCoordinates.getY();
if (map[x][y] != '.') throw new IllegalStateException("Player ended on an invalid square" + playerCoordinates);
if (oldCoordinates.equals(playerCoordinates)) return false;
return true;
}
private Coordinates getNextValidCoordinates(final int xDelta, final int yDelta)
{
Coordinates newCoordinates = playerCoordinates.clone();
Coordinates prevCoordinates = newCoordinates.clone();
while (true)
{
newCoordinates.add(xDelta, yDelta);
int x = newCoordinates.getX();
int y = newCoordinates.getY();
if (x >= width) newCoordinates = new Coordinates(0, y);
if (x < 0) newCoordinates = new Coordinates(width-1, y);
if (y >= height) newCoordinates = new Coordinates(x, 0);
if (y < 0) newCoordinates = new Coordinates(x, height-1);
x = newCoordinates.getX();
y = newCoordinates.getY();
if (map[x][y] == '#') return prevCoordinates;
if (map[x][y] == '.') return newCoordinates;
}
}
public long getPart1Answer()
{
return (1000 * (playerCoordinates.getY()+1)) + (4 * (playerCoordinates.getX()+1)) + playerFacingDirection;
}
}
The driver is pretty obvious:
final class Day22
{
public static long part1(final String[] day22InputLines, final String movementInstructions, final int width, final int height)
{
final MonkeyGrove mg = new MonkeyGrove(width, height);
mg.applyMap(day22InputLines);
mg.applyMovements(movementInstructions);
return mg.getPart1Answer();
}
For part 2, things get kinda challenging. You need to figure a way to implement the movement changes of the cube wrapping. I copied my MonkeyGrove to MonkeyGrove2 to make the necessary changes, and created a helper class CubeMovementRules.
The input to CubeMovementRules is a description of the flat sides of the cube like so:
A
BCD
EF
and a set of rules explaining how sides connect to sides, based on each side having letters as above and numbers like:
-------
| 3 |
|2 A 0|
| 1 |
-------
A0F0
A1D3
A2C3
A3B3
B0C2
B1E1
B2F1
B3A3
C0D2
C1E2
C2B0
C3A2
D0F3
D1E3
D2C0
D3A1
E0F2
E1B1
E2C1
E3D1
F0A0
F1B2
F2E0
F3D0
final class CubeMovementRules
{
public static final CubeMovementRules INVALID_CUBE_MOVEMENT_RULES = new CubeMovementRules();
record CoordinatesWithDirection(Coordinates coordinates, int direction) {};
private int width;
private Map<String, String> translationRules = new HashMap<>();
private Map<CoordinatesWithDirection, String> sideDeterminer = new HashMap<>();
private Map<String, Coordinates[]> sideCoordinates = new HashMap<>();
private CubeMovementRules() // for INVALID_CUBE_MOVEMENT_RULES
{
width = -1;
}
public CubeMovementRules(final int width)
{
this.width = width;
}
public void defineFlatLayout(final String[] flatLayout)
{
for(int y=0; y<flatLayout.length; y++)
{
for (int x=0; x<flatLayout[y].length(); x++)
{
final char cubeSideLetter = flatLayout[y].charAt(x);
if (cubeSideLetter == ' ') continue;
mapSidesToCoordinates(cubeSideLetter, x, y);
}
}
}
private void mapSidesToCoordinates(final char cubeSideLetter, final int flatX, final int flatY)
{
final Coordinates[] eitherEndOfSide = new Coordinates[2];
int x1=flatX*width;
int x2=x1+width-1;
int y1=flatY*width;
int y2=y1+width-1;
for (int y=y1; y<=y2; y++)
{
final CoordinatesWithDirection coord = new CoordinatesWithDirection(new Coordinates(x2,y), 0);
sideDeterminer.put(coord, cubeSideLetter+"0");
}
eitherEndOfSide[0] = new Coordinates(x2,y1);
eitherEndOfSide[1] = new Coordinates(x2,y2);
sideCoordinates.put(cubeSideLetter+"0", eitherEndOfSide.clone());
for (int x=x1; x<=x2; x++)
{
final CoordinatesWithDirection coord = new CoordinatesWithDirection(new Coordinates(x,y2), 1);
sideDeterminer.put(coord, cubeSideLetter+"1");
}
eitherEndOfSide[0] = new Coordinates(x1,y2);
eitherEndOfSide[1] = new Coordinates(x2,y2);
sideCoordinates.put(cubeSideLetter+"1", eitherEndOfSide.clone());
for (int y=y1; y<=y2; y++)
{
final CoordinatesWithDirection coord = new CoordinatesWithDirection(new Coordinates(x1,y), 2);
sideDeterminer.put(coord, cubeSideLetter+"2");
}
eitherEndOfSide[0] = new Coordinates(x1,y1);
eitherEndOfSide[1] = new Coordinates(x1,y2);
sideCoordinates.put(cubeSideLetter+"2", eitherEndOfSide.clone());
for (int x=x1; x<=x2; x++)
{
final CoordinatesWithDirection coord = new CoordinatesWithDirection(new Coordinates(x,y1), 3);
sideDeterminer.put(coord, cubeSideLetter+"3");
}
eitherEndOfSide[0] = new Coordinates(x1,y1);
eitherEndOfSide[1] = new Coordinates(x2,y1);
sideCoordinates.put(cubeSideLetter+"3", eitherEndOfSide.clone());
}
public void addRules(final String[] traslationRules)
{
for (final String rule: traslationRules)
{
final String part1 = rule.substring(0,2);
final String part2 = rule.substring(2);
translationRules.put(part1, part2);
}
}
public boolean doesDirectionOutOfCoordinatesRequireTranslation(final Coordinates coordinatesToCheck, final int direction)
{
final CoordinatesWithDirection coord = new CoordinatesWithDirection(coordinatesToCheck, direction);
if (sideDeterminer.containsKey(coord))
{
return true;
}
return false;
}
public final CoordinatesWithDirection getTranslatedCoordinates(final Coordinates coordinatesToTranslate, final int direction)
{
final CoordinatesWithDirection coord = new CoordinatesWithDirection(coordinatesToTranslate, direction);
if (!sideDeterminer.containsKey(coord))
throw new IllegalStateException("Coordinates not in translation list: " + coord);
final String sideNumStr = sideDeterminer.get(coord);
final String newSideNumStr = translationRules.get(sideNumStr);
final int dir1 = sideNumStr.charAt(1)-'0';
final int dir2 = newSideNumStr.charAt(1)-'0';
final Coordinates[] coordinates = sideCoordinates.get(sideNumStr);
if ((dir1 == 0) && (dir2 == 0))
{
// right side to right side, y->invert y
final int y = coordinatesToTranslate.getY() - coordinates[0].getY();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, width-y-1), turn180(direction));
}
if ((dir1 == 0) && (dir2 == 1))
{
// right side to bot side, y->x
final int y = coordinatesToTranslate.getY() - coordinates[0].getY();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, y), turnBack90(direction));
}
if ((dir1 == 0) && (dir2 == 2))
{
// right side to left side, y->y no translation
final int y = coordinatesToTranslate.getY() - coordinates[0].getY();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, y), direction);
}
if ((dir1 == 0) && (dir2 == 3))
{
// right side to top y-> inverted x
final int y = coordinatesToTranslate.getY() - coordinates[0].getY();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, width-y-1), turnForward90(direction));
}
if ((dir1 == 1) && (dir2 == 0))
{
// bot to right side, x->y
final int x = coordinatesToTranslate.getX() - coordinates[0].getX();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, x), turnForward90(direction));
}
if ((dir1 == 1) && (dir2 == 1))
{
// bottom to bottom, invert x, 180 rotation
final int x = coordinatesToTranslate.getX() - coordinates[0].getX();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, width-x-1), turn180(direction));
}
// NOTE ((dir1 == 1) && (dir2 == 2)) not covered
if ((dir1 == 1) && (dir2 == 3))
{
// bottom to top, x->x no translation
final int x = coordinatesToTranslate.getX() - coordinates[0].getX();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, x), direction);
}
if ((dir1 == 2) && (dir2 == 0))
{
// left side to right side, y->y no translation
final int y = coordinatesToTranslate.getY() - coordinates[0].getY();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, y), direction);
}
if ((dir1 == 2) && (dir2 == 1))
{
// left side to bottom, y->inverted x
final int y = coordinatesToTranslate.getY() - coordinates[0].getY();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, width-y-1), turnForward90(direction));
}
if ((dir1 == 2) && (dir2 == 2))
{
// left side to left side, y->inverted y
final int y = coordinatesToTranslate.getY() - coordinates[0].getY();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, width-y-1), turn180(direction));
}
if ((dir1 == 2) && (dir2 == 3))
{
// left side to top, y->x
final int y = coordinatesToTranslate.getY() - coordinates[0].getY();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, y), turnBack90(direction));
}
// NOTE ((dir1 == 3) && (dir2 == 0)) not covered
if ((dir1 == 3) && (dir2 == 1))
{
// top to bot side, x->x
final int x = coordinatesToTranslate.getX() - coordinates[0].getX();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, x), direction);
}
if ((dir1 == 3) && (dir2 == 2))
{
// top to left side, x->y
final int x = coordinatesToTranslate.getX() - coordinates[0].getX();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, x), turnForward90(direction));
}
if ((dir1 == 3) && (dir2 == 3))
{
// top to top, 180 rotation, inverted y
final int x = coordinatesToTranslate.getX() - coordinates[0].getX();
return new CoordinatesWithDirection(getCoordinatesOfCubeSide(newSideNumStr, width-x-1), turn180(direction));
}
// Left here in case should there be a case that was missed
// (used many times during development)
System.out.println(dir1);
System.out.println(dir2);
System.out.println(sideNumStr);
System.out.println(newSideNumStr);
throw new IllegalStateException();
}
private int turnBack90(final int direction)
{
int dir = direction - 1;
if (dir < 0) dir = 3;
return dir;
}
private int turnForward90(final int direction)
{
int dir = direction + 1;
if (dir > 3) dir = 0;
return dir;
}
private int turn180(final int direction)
{
final int dir = turnForward90(direction);
return turnForward90(dir);
}
private Coordinates getCoordinatesOfCubeSide(final String sideNumStr, final int offsetVal)
{
final Coordinates[] coordinates = sideCoordinates.get(sideNumStr);
switch (sideNumStr.charAt(1)-'0')
{
case 0: return new Coordinates(coordinates[0].getX(), coordinates[0].getY()+offsetVal);
case 1: return new Coordinates(coordinates[0].getX()+offsetVal, coordinates[0].getY());
case 2: return new Coordinates(coordinates[0].getX(), coordinates[0].getY()+offsetVal);
case 3: return new Coordinates(coordinates[0].getX()+offsetVal, coordinates[0].getY());
default: throw new IllegalStateException();
}
}
}
And the updated MonkeyGrove2:
final class MonkeyGrove2
{
private final int width;
private final int height;
private char[][] map;
private Coordinates playerCoordinates;
private int playerFacingDirection;
final CubeMovementRules cubeMovementRules;
public MonkeyGrove2(final int width, final int height, final CubeMovementRules cubeMovementRules)
{
this.width = width;
this.height = height;
map = new char[width][height];
playerCoordinates = Coordinates.INVALID_COORDINATES;
this.cubeMovementRules = cubeMovementRules;
}
public void applyMap(final String[] mapData)
{
clearMap();
for (int y=0; y<mapData.length; y++)
{
for (int x=0; x<mapData[y].length(); x++)
{
final char c = mapData[y].charAt(x);
switch(c)
{
case ' ': break;
case '.':
{
map[x][y] = c;
if (playerCoordinates == Coordinates.INVALID_COORDINATES)
{
playerCoordinates = new Coordinates(x,y);
}
}
break;
case '#':
{
map[x][y] = c;
}
break;
default: throw new IllegalStateException("Invalid input map char: " + c);
}
}
}
}
private void clearMap()
{
for (int y=0; y<height; y++)
for (int x=0; x<width; x++)
map[x][y] = ' ';
}
public void applyMovements(final String movementInstructions)
{
boolean doneMoving = false;
int index = 0;
String digits = "";
//dumpWithPlayer();
while (!doneMoving)
{
if (index >= movementInstructions.length())
{
doneMoving = true;
}
final char c;
if (doneMoving)
c = ' ';
else
c = movementInstructions.charAt(index);
switch (c)
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
{
digits = digits + c;
}
break;
case 'L':
{
move(Integer.parseInt(digits));
//dumpWithPlayer();
digits="";
playerFacingDirection = playerFacingDirection - 1;
if (playerFacingDirection < 0) playerFacingDirection = 3;
//dumpWithPlayer();
}
break;
case 'R':
{
move(Integer.parseInt(digits));
//dumpWithPlayer();
digits="";
playerFacingDirection = playerFacingDirection + 1;
if (playerFacingDirection > 3) playerFacingDirection = 0;
//dumpWithPlayer();
}
break;
case ' ':
{
if (!digits.isEmpty())
{
move(Integer.parseInt(digits));
digits="";
//dumpWithPlayer();
}
}
break;
default: throw new IllegalStateException("Got invalid movement char: " + c);
}
index++;
}
}
private void move(final int numberToMove)
{
// System.out.println("move " + numberToMove);
for (int i=0; i<numberToMove; i++)
{
if (!moveToNextSpace()) break;
}
}
private boolean moveToNextSpace()
{
final Coordinates oldCoordinates = playerCoordinates.clone();
final CoordinatesWithDirection cwd = getNextValidCoordinates(playerFacingDirection);
playerCoordinates = cwd.coordinates();
playerFacingDirection = cwd.direction();
int x = playerCoordinates.getX();
int y = playerCoordinates.getY();
if (map[x][y] != '.') throw new IllegalStateException("Player ended on an invalid square" + playerCoordinates);
if (oldCoordinates.equals(playerCoordinates)) return false;
return true;
}
private CoordinatesWithDirection getNextValidCoordinates(final int direction)
{
int curDir = direction;
int prevDir = direction;
Coordinates newCoordinates = playerCoordinates.clone();
Coordinates prevCoordinates = newCoordinates.clone();
int xDelta = 0;
int yDelta = 0;
switch (direction)
{
case 0: xDelta = 1; yDelta = 0; break;
case 1: xDelta = 0; yDelta = 1; break;
case 2: xDelta = -1; yDelta = 0; break;
case 3: xDelta = 0; yDelta = -1; break;
default: throw new IllegalStateException("Invalid direction=" + direction);
}
while (true)
{
if (cubeMovementRules.doesDirectionOutOfCoordinatesRequireTranslation(newCoordinates, curDir))
{
final CoordinatesWithDirection cwd = cubeMovementRules.getTranslatedCoordinates(newCoordinates, curDir);
newCoordinates = cwd.coordinates();
curDir = cwd.direction();
}
else
{
newCoordinates.add(xDelta, yDelta);
}
int x = newCoordinates.getX();
int y = newCoordinates.getY();
if (x >= width) newCoordinates = new Coordinates(0, y);
if (x < 0) newCoordinates = new Coordinates(width-1, y);
if (y >= height) newCoordinates = new Coordinates(x, 0);
if (y < 0) newCoordinates = new Coordinates(x, height-1);
x = newCoordinates.getX();
y = newCoordinates.getY();
if (map[x][y] == '#') return new CoordinatesWithDirection(prevCoordinates, prevDir);
if (map[x][y] == '.') return new CoordinatesWithDirection(newCoordinates, curDir);
}
}
public long getPart2Answer()
{
return (1000 * (playerCoordinates.getY()+1)) + (4 * (playerCoordinates.getX()+1)) + playerFacingDirection;
}
void dumpWithPlayer()
{
System.out.println("====");
for (int y=0; y<height; y++)
{
for (int x=0; x<width; x++)
{
char c= map[x][y];
if ((playerCoordinates.getX() == x) && (playerCoordinates.getY() == y))
{
switch(playerFacingDirection)
{
case 0: c = '>'; break;
case 1: c = 'V'; break;
case 2: c = '<'; break;
case 3: c = '^'; break;
}
}
System.out.print(c);
}
System.out.println();
}
}
// For unit test debugging
void setPosition(final Coordinates coordinates, final int direction)
{
playerCoordinates = coordinates.clone();
playerFacingDirection = direction;
dumpWithPlayer();
}
}
And the updated driver:
final class Day22
{
/* a sample square side numbering:
-------
| 3 |
|2 A 0|
| 1 |
-------
*/
static final String[] FLAT_LAYOUT4 =
"""
A
BCD
EF
""".split("\n");
static final String[] MOVEMENT_RULES4 =
"""
A0F0
A1D3
A2C3
A3B3
B0C2
B1E1
B2F1
B3A3
C0D2
C1E2
C2B0
C3A2
D0F3
D1E3
D2C0
D3A1
E0F2
E1B1
E2C1
E3D1
F0A0
F1B2
F2E0
F3D0
""".split("\n");
public static long part1(final String[] day22InputLines, final String movementInstructions, final int width, final int height)
{
final MonkeyGrove mg = new MonkeyGrove(width, height);
mg.applyMap(day22InputLines);
mg.applyMovements(movementInstructions);
return mg.getPart1Answer();
}
public static long part2(final String[] day22InputLines, final String movementInstructions, final int width, final int height, final int cubeSize)
{
final CubeMovementRules cmr;
if (cubeSize == 4)
{
// Sample input
cmr = new CubeMovementRules(cubeSize);
cmr.defineFlatLayout(FLAT_LAYOUT4);
cmr.addRules(MOVEMENT_RULES4);
}
else if (cubeSize == 50)
{
// Day input
cmr = new CubeMovementRules(cubeSize);
cmr.defineFlatLayout(AdventOfCode2022Day22Main.FLAT_LAYOUT50);
cmr.addRules(AdventOfCode2022Day22Main.MOVEMENT_RULES50);
}
else
{
cmr = CubeMovementRules.INVALID_CUBE_MOVEMENT_RULES;
}
final MonkeyGrove2 mg = new MonkeyGrove2(width, height, cmr);
mg.applyMap(day22InputLines);
mg.applyMovements(movementInstructions);
return mg.getPart2Answer();
}
}
I included the necessary FLAT_LAYOUT50 and MOVEMENT_RULES50 in my main because they were specific to my input, but they looked very similar to the ones in the driver for the example.