Day 16 is simulating beams as they bounce and split inside an “arena” filled with “contraptions”. This one is a big of work simply because you have to figure out how you’re going to handle the beams splitting and make sure you have all of your beam manipulations correct. You need to have cases for beams coming in from each direction for each contraption. You will also need to figure out how to detect and eliminate beams that get into a loop. While doing all that, you also need to keep track of which spots the beam passes through. There’s plenty going on… and plenty of chances to make a mistake, including one that isn’t detected by the sample input but occurs in your actual input. (Ask me how I know
I wasted a good 30 minutes trying to find the problem that caused my first attempt at part 2 to produce a very low answer like 144. )
EDIT: Oh I almost forgot one other tricky thing… your input will have \ in it and that is an escape character in many languages (C/C++ and Java for sure, but probably others too.) You’ll need to load your input into a text editor and search and replace all the \ for \\ if you want to use it as “hard coded” Strings. Of course, you could always read it from a file if that’s easier, I guess.
I created a helper class named LavaBeamContraption. It manages the “arena” and all the beams. I used a couple of enums for BeamDirection and Contraption and a couple of records for Coordinates and Beams.
final class LavaBeamContraption
{
public enum BeamDirection
{
DEFUNCT,
NORTH,
EAST,
SOUTH,
WEST;
}
public record Coordinates(int x, int y) {}
private static final Coordinates INVALID_COORDINATES = new Coordinates(-1, -1);
public record Beam(Coordinates coordinates, BeamDirection direction) {}
private static final Beam DEFUNCT_BEAM = new Beam(INVALID_COORDINATES, BeamDirection.DEFUNCT);
private enum Contraption
{
FORWARD_SLASH('/'),
BACKWARD_SLASH('\\'),
PIPE('|'),
DASH('-');
private final char contraptionChar;
private Contraption(final char contraptionChar)
{
this.contraptionChar = contraptionChar;
}
public char getContraptionChar()
{
return contraptionChar;
}
}
private final int height;
private final int width;
private final Map<Coordinates,Contraption> contraptionCoordinates = new HashMap<>();
private final List<Beam> beams = new ArrayList<>();
private Set<Coordinates> energizedCoordinates = new HashSet<>();
private Set<Beam> loopDetection = new HashSet<>();
public LavaBeamContraption(int height, int width)
{
this.height = height;
this.width = width;
}
public void loadInContraption(final String[] day16InputLines)
{
for (int y=0; y<height; y++)
{
for (int x=0; x<width; x++)
{
final char contraptionChar = day16InputLines[y].charAt(x);
final Coordinates coordinates = new Coordinates(x, y);
switch (contraptionChar)
{
case '.':
{
// empty space, do nothing
}
break;
case '/':
{
contraptionCoordinates.put(coordinates, Contraption.FORWARD_SLASH);
}
break;
case '\\':
{
contraptionCoordinates.put(coordinates, Contraption.BACKWARD_SLASH);
}
break;
case '|':
{
contraptionCoordinates.put(coordinates, Contraption.PIPE);
}
break;
case '-':
{
contraptionCoordinates.put(coordinates, Contraption.DASH);
}
break;
default:
throw new IllegalStateException("Unexpected char in input: " + contraptionChar);
}
}
}
}
public void addBeam(final Beam beam)
{
beams.add(beam);
}
public void releaseBeams()
{
//final Scanner scanner = new Scanner(System.in);
while (!beams.isEmpty())
{
final List<Beam> newBeamsToAddAtEndOfCycle = new ArrayList<>();
for (int i=0; i<beams.size(); i++)
{
final Beam beam = beams.get(i);
if (loopDetection.contains(beam))
{
// this beam is looping, so kill it now
beams.set(i, DEFUNCT_BEAM);
continue;
}
if (beam == DEFUNCT_BEAM) continue;
loopDetection.add(beam);
final Coordinates coordinates = beam.coordinates;
energizedCoordinates.add(coordinates);
if (!contraptionCoordinates.containsKey(coordinates))
{
final Coordinates newBeamCoordinates = moveBeam(beam);
if (!areCoordinatesInArea(newBeamCoordinates))
{
beams.set(i, DEFUNCT_BEAM);
}
else
{
beams.set(i, new Beam(newBeamCoordinates, beam.direction));
}
}
else
{
final Contraption contraption = contraptionCoordinates.get(coordinates);
final Beam[] newBeams = moveBeamThroughContraption(beam, contraption);
if (newBeams.length == 1)
{
final Coordinates newBeamCoordinates = newBeams[0].coordinates;
if (!areCoordinatesInArea(newBeamCoordinates))
{
beams.set(i, DEFUNCT_BEAM);
}
else
{
beams.set(i, newBeams[0]);
}
}
else
{
beams.set(i, DEFUNCT_BEAM);
for (final Beam newBeam : newBeams)
{
if (areCoordinatesInArea(newBeam.coordinates))
{
newBeamsToAddAtEndOfCycle.add(newBeam);
}
}
}
}
}
// Clean up all defunct beams (so the isEmpty() will work)
// Walk backward to avoid problems with indexes shifting as they're deleted
for (int i=beams.size()-1; i>=0; i--)
{
if (beams.get(i) == DEFUNCT_BEAM) beams.remove(i);
}
// Add any new beams generated in cycle (can't add in real time
// or they'll end up processed in same cycle added)
beams.addAll(newBeamsToAddAtEndOfCycle);
}
}
private Beam[] moveBeamThroughContraption(final Beam beam, final Contraption contraption)
{
final List<Beam> newBeams = new ArrayList<>();
final Coordinates coordinates = beam.coordinates;
switch (contraption)
{
case BACKWARD_SLASH:
{
switch (beam.direction)
{
case NORTH:
newBeams.add(new Beam(new Coordinates(coordinates.x-1, coordinates.y), BeamDirection.WEST));
break;
case EAST:
newBeams.add(new Beam(new Coordinates(coordinates.x, coordinates.y+1), BeamDirection.SOUTH));
break;
case SOUTH:
newBeams.add(new Beam(new Coordinates(coordinates.x+1, coordinates.y), BeamDirection.EAST));
break;
case WEST:
newBeams.add(new Beam(new Coordinates(coordinates.x, coordinates.y-1), BeamDirection.NORTH));
break;
default:
throw new IllegalStateException("BACKWARD_SLASH beam.direction was " + beam.direction);
}
}
break;
case FORWARD_SLASH:
{
switch (beam.direction)
{
case NORTH:
newBeams.add(new Beam(new Coordinates(coordinates.x+1, coordinates.y), BeamDirection.EAST));
break;
case EAST:
newBeams.add(new Beam(new Coordinates(coordinates.x, coordinates.y-1), BeamDirection.NORTH));
break;
case SOUTH:
newBeams.add(new Beam(new Coordinates(coordinates.x-1, coordinates.y), BeamDirection.WEST));
break;
case WEST:
newBeams.add(new Beam(new Coordinates(coordinates.x, coordinates.y+1), BeamDirection.SOUTH));
break;
default:
throw new IllegalStateException("FORWARD_SLASH beam.direction was " + beam.direction);
}
}
break;
case DASH:
{
switch (beam.direction)
{
case NORTH:
case SOUTH:
newBeams.add(new Beam(new Coordinates(coordinates.x+1, coordinates.y), BeamDirection.EAST));
newBeams.add(new Beam(new Coordinates(coordinates.x-1, coordinates.y), BeamDirection.WEST));
break;
case EAST:
newBeams.add(new Beam(new Coordinates(coordinates.x+1, coordinates.y), BeamDirection.EAST));
break;
case WEST:
newBeams.add(new Beam(new Coordinates(coordinates.x-1, coordinates.y), BeamDirection.WEST));
break;
default:
throw new IllegalStateException("DASH beam.direction was " + beam.direction);
}
}
break;
case PIPE:
{
switch (beam.direction)
{
case NORTH:
newBeams.add(new Beam(new Coordinates(coordinates.x, coordinates.y-1), BeamDirection.NORTH));
break;
case SOUTH:
newBeams.add(new Beam(new Coordinates(coordinates.x, coordinates.y+1), BeamDirection.SOUTH));
break;
case EAST:
case WEST:
newBeams.add(new Beam(new Coordinates(coordinates.x, coordinates.y-1), BeamDirection.NORTH));
newBeams.add(new Beam(new Coordinates(coordinates.x, coordinates.y+1), BeamDirection.SOUTH));
break;
default:
throw new IllegalStateException("DASH beam.direction was " + beam.direction);
}
}
break;
default:
throw new IllegalStateException("Unexpected Contration: " + contraption);
}
return newBeams.toArray(new Beam[0]);
}
private boolean areCoordinatesInArea(final Coordinates coordinates)
{
if (coordinates.x < 0) return false;
if (coordinates.y < 0) return false;
if (coordinates.x >= width) return false;
if (coordinates.y >= height) return false;
return true;
}
private Coordinates moveBeam(final Beam beam)
{
switch (beam.direction)
{
case NORTH:
return new Coordinates (beam.coordinates.x, beam.coordinates.y-1);
case EAST:
return new Coordinates (beam.coordinates.x+1, beam.coordinates.y);
case SOUTH:
return new Coordinates (beam.coordinates.x, beam.coordinates.y+1);
case WEST:
return new Coordinates (beam.coordinates.x-1, beam.coordinates.y);
default:
throw new IllegalStateException("beam.direction was " + beam.direction);
}
}
public long getEnergizedCount()
{
return energizedCoordinates.size();
}
}
For the part 1 method, I eventually refactored out most of it for part 2, but it basically loads up the arena and defines the beam and then lets it loose, finally asking how many spots got energized.
public static Long part1(final String[] day16InputLines)
{
return getEnergizedCountForABeam(day16InputLines, new Beam(new Coordinates(0,0), BeamDirection.EAST));
}
private static long getEnergizedCountForABeam(final String[] day16InputLines, final Beam beam)
{
final LavaBeamContraption lbc = new LavaBeamContraption(day16InputLines.length, day16InputLines[0].length());
lbc.loadInContraption(day16InputLines);
lbc.addBeam(beam);
lbc.releaseBeams();
return lbc.getEnergizedCount();
}
I had everything I needed already in the helper class for the part 2 method, so it just handles finding the requested maximum.
public static Long part2(final String[] day16InputLines)
{
final int height = day16InputLines.length;
final int width = day16InputLines[0].length();
long bestResult = -1;
for (int x=0; x<width; x++)
{
final long res1 = getEnergizedCountForABeam(day16InputLines, new Beam(new Coordinates(x, 0), BeamDirection.SOUTH));
if (res1 > bestResult) bestResult = res1;
final long res2 = getEnergizedCountForABeam(day16InputLines, new Beam(new Coordinates(x, height-1), BeamDirection.NORTH));
if (res2 > bestResult) bestResult = res2;
}
for (int y=0; y<height; y++)
{
final long res1 = getEnergizedCountForABeam(day16InputLines, new Beam(new Coordinates(0, y), BeamDirection.EAST));
if (res1 > bestResult) bestResult = res1;
final long res2 = getEnergizedCountForABeam(day16InputLines, new Beam(new Coordinates(width-1,y), BeamDirection.WEST));
if (res2 > bestResult) bestResult = res2;
}
return bestResult;
}