Oh boy day 17 was rough. I spent too many hours getting his weird Tetris rules to apply properly, so many weird bugs or corner cases. For quite some time I couldn’t get the first “rock” to land where he said it should. Then when I finally had something that made the first rock work right, the second one would be off by one. It turned out my wind checking wasn’t quite right… among other things.
Another of my big bugs was not properly unit testing my culling. It wasn’t actually scrolling down like it should have been because I donno what kind of drugs I must have (or maybe should have been) on when I coded it.
According to my personal stats, it was 03:56:06 (so almost 4am) before I had part one properly debugged at working, and I was already feeling exhausted. I will say, when I drew out a picture of the first 10 rocks and it exactly matched the example, there was much celebration in my head (and I earned myself a drink.) Here’s a relevant picture:
| ##### |
| # |
| # |
| # |
| # |
| ## # |
| ## # |
| ### |
| # |
| ### |
| # |
| #### |
| ##|
| ##|
| #|
| #|
| ####|
| ### |
| # |
|# ####|
|# # |
|# # |
|# ## |
|## ## |
|###### |
| ### |
| # |
| #### |
| ## |
| ## |
| # |
| # # |
| # # |
|##### |
| ### |
| # |
| #### |
-------
Another problem I had was that my playfield size wasn’t big enough because there is the possibility of a really deep hole. In the example input, the deepest hole is 32, and in my provided input the deepest hole was 59. This means that I needed to make sure my culled playfield had at least enough room for 60 lines, which it didn’t have at first, and that was causing the answer to be off.
For part 2, I tried different things, and read some of Reddit for hints. I couldn’t get it to work right initially. Someone on Reddit did give me a cryptic hint, and eventually I worked out that there must be a cycle I needed to find. Here’s a picture of how I eventually thought about it.
I eventually got desperate to know the correct answer to help with my debugging/math, and I tried running a half dozen other peoples listed code: 4 Python and 2 Java. None of the 4 Python either worked or gave the correct answer. The first Java didn’t either. The second Java took a LOT of rework, because he had all his own utility classes and one of them clashed with my Coordinates class. (He was also using a method deprecated in more recent Java that I had to work around.)
In the end, for part 2, I wrote code that outputs a table of numbers so I could find the cycle. I had it look at the top 90 lines of the rocks and look for when it would see that pattern repeat later. (I didn’t know the size of the deepest whole at that time, but I figured I needed a good amount.) The outputs for the example input looked like this:
The columns are: currentRock#, numberOfLinesOccupied, theRock#ItsARepeatOf
So you can see that 107-72 = 35, the cycle size
And if you look ahead at 107+35 = 142 you'll see the lines
used there is 222 and subtracting the 169 at 107 gives
you 222-169 = 53, so there are 53 lines used per cycle.
107, 169, 72
108, 170, 73
109, 172, 74
110, 172, 75
111, 173, 76
112, 175, 77
113, 176, 78
114, 178, 79
115, 178, 80
116, 179, 81
117, 182, 82
118, 184, 83
119, 184, 84
120, 184, 85
121, 185, 86
122, 188, 87
123, 191, 88
124, 195, 89
125, 195, 90
126, 196, 91
127, 198, 92
128, 201, 93
129, 201, 94
130, 202, 95
131, 203, 96
132, 206, 97
133, 208, 98
134, 210, 99
135, 210, 100
136, 210, 101
137, 212, 102
138, 215, 103
139, 219, 104
140, 219, 105
141, 220, 106
142, 222, 107
143, 223, 108
144, 225, 109
145, 225, 110
146, 226, 111
147, 228, 112
...
So in the end I printed enough tabular info to be able to do the math like so:
// cycle is 35, 53 rocks per cycle
final long cycleSize = 35;
final long rocksPerCycle = 53;
final long cycleStartRock = 107;
final long numLinesAtCycleStart = 169;
final long numberOfCycles = (1_000_000_000_000L - cycleStartRock) / cycleSize;
final long numberOfAdditionalRocks = 1_000_000_000_000L - numberOfCycles * cycleSize - cycleStartRock; // 13 additional rocks
final long cycleLookupAt13 = 15;
final long result = numberOfCycles * rocksPerCycle + numLinesAtCycleStart + cycleLookupAt13;
So here’s my class for RockTetris. I left in lots of debugging stuff including commented out values that I changed for different purposes, to give you a sense of how it changed as I tried things. The method findPeriod() was what I used to print the table above.
final class RockTetris
{
private final static String[] rock1 =
"""
####
""".split("\n");
private final static String[] rock2 =
"""
#
###
#
""".split("\n");
private final static String[] rock3 =
"""
#
#
###
""".split("\n");
private final static String[] rock4 =
"""
#
#
#
#
""".split("\n");
private final static String[] rock5 =
"""
##
##
""".split("\n");
private static final Rock[] rocks = {new Rock(rock1), new Rock(rock2), new Rock(rock3), new Rock(rock4), new Rock(rock5)};
private final char[][] playfield;
private final static int PLAYFIELD_WIDTH = 7;
private final static int PLAYFIELD_HEIGHT = 8000;
//private final static int PLAYFIELD_HEIGHT = 4000;
// private final static int PLAYFIELD_HEIGHT = 20;
// private final static int PLAYFIELD_HEIGHT = 60;
private final static int HEIGHT_MARGIN = 60;
private final char[] windMovements;
private int currentWindMovementIndex = 0;
private int highestRockPosition = 0;
private long numberOfLinesEverUsed = 0;
private int currentRockShapeIndex = 0;
private Rock currentRockShape = rocks[currentRockShapeIndex];
private int deepestHole = 0;
public RockTetris(final String windMovements)
{
this.windMovements = windMovements.toCharArray();
playfield = new char[PLAYFIELD_WIDTH][PLAYFIELD_HEIGHT];
clearPlayfield();
highestRockPosition = PLAYFIELD_HEIGHT;
}
public void playRounds(final long numberOfRocks)
{
for (int i=0; i<numberOfRocks; i++)
//for (int i=0; i<10; i++)
{
playNextRock();
final int[] holeDepths = getHoleDepths();
for (int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (holeDepths[x] > deepestHole) deepestHole = holeDepths[x];
}
}
System.out.println("Deepest hole: " + deepestHole);
// dumpPlayfield();
}
private int[] getHoleDepths()
{
final int[] result = new int[PLAYFIELD_WIDTH];
for (int x=0; x<PLAYFIELD_WIDTH; x++)
{
result[x] = getHoleDepthAt(x);
}
return result;
}
private int getHoleDepthAt(final int x)
{
for (int y=highestRockPosition; y<PLAYFIELD_HEIGHT; y++)
{
if (playfield[x][y] != ' ') return y-highestRockPosition;
}
return PLAYFIELD_HEIGHT - highestRockPosition;
}
public long getNumberOfLinesEverUsed()
{
return numberOfLinesEverUsed + (PLAYFIELD_HEIGHT-highestRockPosition);
}
private void playNextRock()
{
//dumpPlayfield();
if (highestRockPosition <= HEIGHT_MARGIN)
{
scrollPlayFieldDown(HEIGHT_MARGIN);
highestRockPosition +=HEIGHT_MARGIN;
numberOfLinesEverUsed +=HEIGHT_MARGIN;
}
final Coordinates CoordinatesOfCurrentRock = new Coordinates(2, highestRockPosition-4);
setCurrentRockShape();
// dumpPlayfieldWithCurrentRock(CoordinatesOfCurrentRock);
while (true)
{
applyWind(CoordinatesOfCurrentRock);
//dumpPlayfieldWithCurrentRock(CoordinatesOfCurrentRock);
if (rockHasSomethingBelow(CoordinatesOfCurrentRock))
{
break;
}
else
{
applyGravity(CoordinatesOfCurrentRock);
//dumpPlayfieldWithCurrentRock(CoordinatesOfCurrentRock);
}
}
putRockInPlayfield(CoordinatesOfCurrentRock);
// dumpPlayfield();
}
private boolean isSomethingInTheWayOfWind(Coordinates coordinatesOfCurrentRock)
{
final int windXDelta;
if (windMovements[currentWindMovementIndex] == '<')
{
windXDelta = -1;
}
else
{
windXDelta = 1;
}
final int numRockPieces = currentRockShape.getNumberOfRockPieces();
for (int i=0; i<numRockPieces; i++)
{
final Coordinates rockPieceCoordinates = currentRockShape.getRockPiece(i, coordinatesOfCurrentRock);
final Coordinates adjacentPieceCoordinates = rockPieceCoordinates.clone();
if (windXDelta<0)
adjacentPieceCoordinates.moveLeft();
else
adjacentPieceCoordinates.moveRight();
final int adjX = adjacentPieceCoordinates.getX();
final int adjY = adjacentPieceCoordinates.getY();
if ((adjX<0) || (adjX>=PLAYFIELD_WIDTH)) return true;
final char whatsThere = playfield[adjX][adjY];
if (whatsThere != ' ') return true;
}
return false;
}
private void putRockInPlayfield(final Coordinates coordinatesOfCurrentRock)
{
placeRockInto(playfield, coordinatesOfCurrentRock, '#', true);
}
private void placeRockInto(final char[][] playfieldForRock, final Coordinates coordinatesOfCurrentRock, final char rockChar, final boolean shouldUpdateHeight)
{
final int numRockPieces = currentRockShape.getNumberOfRockPieces();
for (int i=0; i<numRockPieces; i++)
{
final Coordinates rockPieceCoordinates = currentRockShape.getRockPiece(i, coordinatesOfCurrentRock);
final int rockPieceX = rockPieceCoordinates.getX();
final int rockPieceY = rockPieceCoordinates.getY();
if (shouldUpdateHeight && (rockPieceY < highestRockPosition)) highestRockPosition = rockPieceY;
playfieldForRock[rockPieceX][rockPieceY] = rockChar;
}
}
private void applyGravity(final Coordinates coordinatesOfCurrentRock)
{
coordinatesOfCurrentRock.moveDown();
}
private void applyWind(final Coordinates coordinatesOfCurrentRock)
{
final int windXDelta;
if (windMovements[currentWindMovementIndex] == '<')
{
//System.out.println("Left Wind");
windXDelta = -1;
}
else
{
//System.out.println("Right Wind");
windXDelta = 1;
}
if (!isSomethingInTheWayOfWind(coordinatesOfCurrentRock))
{
if (windXDelta<0)
coordinatesOfCurrentRock.moveLeft();
else
coordinatesOfCurrentRock.moveRight();
}
currentWindMovementIndex++;
if (currentWindMovementIndex >= windMovements.length) currentWindMovementIndex = 0;
}
private boolean rockHasSomethingBelow(final Coordinates coordinatesOfCurrentRock)
{
if (coordinatesOfCurrentRock.getY() >= PLAYFIELD_HEIGHT-1) return true;
final int numRockPieces = currentRockShape.getNumberOfRockPieces();
for (int i=0; i<numRockPieces; i++)
{
final Coordinates rockPieceCoordinates = currentRockShape.getRockPiece(i, coordinatesOfCurrentRock);
final Coordinates belowPieceCoordinates = rockPieceCoordinates.clone();
belowPieceCoordinates.moveDown();
final char whatsThere = playfield[belowPieceCoordinates.getX()][belowPieceCoordinates.getY()];
if (whatsThere != ' ') return true;
}
return false;
}
private void setCurrentRockShape()
{
currentRockShape = rocks[currentRockShapeIndex];
currentRockShapeIndex++;
if (currentRockShapeIndex >= rocks.length) currentRockShapeIndex = 0;
}
private void scrollPlayFieldDown(final int numLinesToRemove)
{
for (int y=PLAYFIELD_HEIGHT-numLinesToRemove-1; y>=0; y--)
{
for (int x=0; x<PLAYFIELD_WIDTH; x++)
{
playfield[x][y+numLinesToRemove] = playfield[x][y];
}
}
for (int y=numLinesToRemove-1; y>=0; y--)
{
for (int x=0; x<PLAYFIELD_WIDTH; x++)
{
playfield[x][y] = ' ';
}
}
}
private void clearPlayfield()
{
for (int y=0; y<PLAYFIELD_HEIGHT; y++)
{
for (int x=0; x<PLAYFIELD_WIDTH; x++)
{
playfield[x][y] = ' ';
}
}
}
private void dumpPlayfield()
{
dumpSpecificPlayfield(playfield);
}
private void dumpSpecificPlayfield(final char[][] specificPlayfield)
{
System.out.println(" -------");
for (int y=0; y<PLAYFIELD_HEIGHT; y++)
{
if (y == highestRockPosition)
System.out.print('+');
else
System.out.print('|');
for (int x=0; x<PLAYFIELD_WIDTH; x++)
{
System.out.print(specificPlayfield[x][y]);
}
if (y == highestRockPosition)
System.out.print('+');
else
System.out.print('|');
System.out.println();
}
System.out.println(" -------");
}
private void dumpPlayfieldWithCurrentRock(final Coordinates coordinatesOfCurrentRock)
{
final char[][] tempPlayfield = new char[PLAYFIELD_WIDTH][PLAYFIELD_HEIGHT];
for (int y=0; y<PLAYFIELD_HEIGHT; y++)
{
for (int x=0; x<PLAYFIELD_WIDTH; x++)
{
tempPlayfield[x][y] = playfield[x][y];
}
}
placeRockInto(tempPlayfield, coordinatesOfCurrentRock, '@', false);
dumpSpecificPlayfield(tempPlayfield);
}
// Trying using hole depths
@org.eclipse.jdt.annotation.NonNullByDefault({})
record State(int rockIndex, int windIndex, long topOfPile1, long topOfPile2, long topOfPile3, long topOfPile4, long topOfPile5, long topOfPile6, long topOfPile7, long topOfPile8, long topOfPile9, long topOfPile10) {};
private Set<State> knownStates = new HashSet<>();
private Map<State, Long> prevIndex= new HashMap<>();
public void findPeriod()
{
long rockNumber = 0;
while(rockNumber <= 4000)
{
rockNumber++;
final int rockIndex = currentRockShapeIndex;
final int windIndex = currentWindMovementIndex;
playNextRock();
long top1 = 0;
long p2=1;
if (highestRockPosition >= PLAYFIELD_HEIGHT-90) continue;
for (int y=highestRockPosition; y<highestRockPosition+9; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top1+= p2;
}
p2*=2;
}
}
long top2 = 0;
p2=1;
for (int y=highestRockPosition+9; y<highestRockPosition+18; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top2+= p2;
}
p2*=2;
}
}
long top3 = 0;
p2=1;
for (int y=highestRockPosition+18; y<highestRockPosition+27; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top3+= p2;
}
p2*=2;
}
}
long top4 = 0;
p2=1;
for (int y=highestRockPosition+27; y<highestRockPosition+36; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top4+= p2;
}
p2*=2;
}
}
long top5 = 0;
p2=1;
for (int y=highestRockPosition+36; y<highestRockPosition+45; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top5+= p2;
}
p2*=2;
}
}
long top6 = 0;
p2=1;
for (int y=highestRockPosition+45; y<highestRockPosition+54; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top6+= p2;
}
p2*=2;
}
}
long top7 = 0;
p2=1;
for (int y=highestRockPosition+54; y<highestRockPosition+63; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top7+= p2;
}
p2*=2;
}
}
long top8 = 0;
p2=1;
for (int y=highestRockPosition+63; y<highestRockPosition+72; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top8+= p2;
}
p2*=2;
}
}
long top9 = 0;
p2=1;
for (int y=highestRockPosition+72; y<highestRockPosition+81; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top9+= p2;
}
p2*=2;
}
}
long top10 = 0;
p2=1;
for (int y=highestRockPosition+81; y<highestRockPosition+90; y++)
{
for(int x=0; x<PLAYFIELD_WIDTH; x++)
{
if (playfield[x][y]=='#')
{
top10+= p2;
}
p2*=2;
}
}
final State state = new State(rockIndex, windIndex, top1, top2, top3, top4, top5, top6, top7, top8, top9, top10);
if (knownStates.contains(state))
{
System.out.println(rockNumber + ", " + (numberOfLinesEverUsed + (long)(PLAYFIELD_HEIGHT-highestRockPosition)) + ", " + prevIndex.get(state));
//break;
}
knownStates.add(state);
prevIndex.put(state, rockNumber);
}
}
}
The driver is pretty predictable. You can see my commenting out and whatnot as I did stuff in part 2 by hand and when I was trying other people’s Java code.
final class Day17
{
public static long part1(final String day17InputLine, final int numberOfRocks)
{
final RockTetris rt = new RockTetris(day17InputLine);
rt.playRounds(numberOfRocks);
return rt.getNumberOfLinesEverUsed();
//return new OtherCode2().part1(day17InputLine);
}
public static long part2(final String day17InputLine, final long numberOfRocks)
{
final RockTetris rt = new RockTetris(day17InputLine);
//rt.playRounds(numberOfRocks);
rt.findPeriod();
return 0;//rt.getNumberOfLinesEverUsed();
/*
final OtherCode2 oc = new OtherCode2();
oc.part1(day17InputLine);
return oc.part2(day17InputLine);
*/
}
}