Skip to content

Commit 35e8de1

Browse files
committed
One more animation: 2024-20 (Race Condition)
* Make the AnimationRecorder drop frames that repeat the last one. * Add the ability to override specified position in a frame. * Add an animation for year 2024, day 20, part 1.
1 parent f672251 commit 35e8de1

File tree

5 files changed

+100
-15
lines changed

5 files changed

+100
-15
lines changed
1.73 MB
Loading

animations/animations.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,10 @@ Iteratively, all accessible rolls of paper (those with fewer than four rolls of
1515
are removed by a forklift.
1616

1717
![Printing Department animation](2025-04_PrintingDepartment.gif)
18+
19+
### Year 2024, Day 20 - Race Condition, Part 1
20+
21+
After following the (unique) path from start to goal, highlight the positions on that path in red that are starting
22+
positions for cheats of at most 2 picoseconds length saving at least 10 picoseconds.
23+
24+
![Race Condition animation](2024-20_RaceCondition.gif)

src/main/kotlin/de/ronny_h/aoc/extensions/animation/AnimationRecorder.kt

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,56 @@
11
package de.ronny_h.aoc.extensions.animation
22

33
import de.ronny_h.aoc.extensions.asList
4+
import de.ronny_h.aoc.extensions.grids.Coordinates
45
import io.github.oshai.kotlinlogging.KotlinLogging
56
import java.awt.Color
67
import java.io.File
8+
import kotlin.math.max
79

8-
class AnimationRecorder {
10+
class AnimationRecorder(private val compact: Boolean = false) {
911
private val frames = mutableListOf<List<String>>()
1012
private val logger = KotlinLogging.logger {}
1113

14+
/**
15+
* Records the given [frame]. If the recorder is [compact], frames that are identical with the last recorded frame
16+
* get dropped.
17+
*/
1218
fun record(frame: String) {
13-
frames.add(frame.asList())
19+
val newFrame = frame.asList()
20+
if (frames.isEmpty() || !compact || frames.last() != newFrame) {
21+
frames.add(newFrame)
22+
}
1423
}
1524

25+
/**
26+
* Repeats the last recorded frame while overriding the positions in [overrides] with the [overrideChar].
27+
*/
28+
fun recordLastFrameWithOverrides(overrides: List<Coordinates>, overrideChar: Char) {
29+
if (frames.isEmpty()) return
30+
frames
31+
.last()
32+
.mapIndexed { y, line ->
33+
line.mapIndexed { x, char ->
34+
if (Coordinates(x, y) in overrides) overrideChar else char
35+
}.joinToString("")
36+
}
37+
.let { frames.add(it) }
38+
}
39+
40+
/**
41+
* Repeats the last [nFrames] frames [times] times.
42+
*/
43+
fun repeatLast(nFrames: Int, times: Int) {
44+
val toRepeat = frames.subList(max(0, frames.size - nFrames), frames.size).toList()
45+
repeat(times) {
46+
frames.addAll(toRepeat)
47+
}
48+
}
49+
50+
/**
51+
* Saves the recorded frames sequence to an animated GIF file named [fileName]. If specified, colors are mapped
52+
* according to [colors] and the background gets colored in [background] color.
53+
*/
1654
fun saveTo(fileName: String, colors: Map<Char, Color> = emptyMap(), background: Color = purpleBlue) {
1755
frames
1856
.map { it.createImage(colors, background) }

src/main/kotlin/de/ronny_h/aoc/extensions/animation/GifSequenceWriter.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import javax.imageio.stream.ImageOutputStream
2121
val green = Color(0, 153, 0)
2222
val lightGreen = Color(0, 186, 13)
2323
val purpleBlue = Color(15, 15, 35)
24-
val lightGrey = Color(204, 204, 204)
24+
val lightBlue = Color(0, 200, 255)
2525
val gray = Color(70, 70, 70)
26+
val lightGrey = Color(204, 204, 204)
2627
val darkGrey = Color(16, 16, 26)
28+
val yellow = Color(255, 255, 102)
2729

2830
private const val scale = 1
2931
private const val fontSize = scale * 20
Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
11
package de.ronny_h.aoc.year2024.day20
22

33
import de.ronny_h.aoc.AdventOfCode
4+
import de.ronny_h.aoc.extensions.animation.*
45
import de.ronny_h.aoc.extensions.graphs.shortestpath.ShortestPath
56
import de.ronny_h.aoc.extensions.graphs.shortestpath.aStar
67
import de.ronny_h.aoc.extensions.grids.Coordinates
78
import de.ronny_h.aoc.extensions.grids.Direction
89
import de.ronny_h.aoc.extensions.grids.Grid
10+
import de.ronny_h.aoc.year2024.day20.RaceTrack.Companion.EMPTY
11+
import de.ronny_h.aoc.year2024.day20.RaceTrack.Companion.GOAL
12+
import de.ronny_h.aoc.year2024.day20.RaceTrack.Companion.SHORTCUT_START
13+
import de.ronny_h.aoc.year2024.day20.RaceTrack.Companion.START
14+
import de.ronny_h.aoc.year2024.day20.RaceTrack.Companion.VISITED
15+
import de.ronny_h.aoc.year2024.day20.RaceTrack.Companion.WALL
16+
import java.awt.Color.red
917

1018
fun main() = RaceCondition().run(1438, 1026446)
1119

1220
class RaceCondition : AdventOfCode<Int>(2024, 20) {
1321
fun part1(input: List<String>, minPicosecondsSaved: Int, shortcutMaxLength: Int): Int {
1422
val track = RaceTrack(input)
15-
track.printGrid()
16-
return track.countAllShortcutsSavingAtLeast(minPicosecondsSaved, shortcutMaxLength)
23+
// track.recorder = AnimationRecorder()
24+
// track.printGrid()
25+
val result = track.countAllShortcutsSavingAtLeast(minPicosecondsSaved, shortcutMaxLength)
26+
track.recorder?.saveTo(
27+
"animations/$year-${paddedDay()}_${javaClass.simpleName}.gif",
28+
mapOf(
29+
START to green,
30+
GOAL to yellow,
31+
WALL to lightGrey,
32+
EMPTY to gray,
33+
VISITED to lightBlue,
34+
SHORTCUT_START to red,
35+
),
36+
)
37+
return result
1738
}
1839

1940
fun part1Small(input: List<String>) = part1(input, 10, 2)
@@ -24,19 +45,28 @@ class RaceCondition : AdventOfCode<Int>(2024, 20) {
2445

2546
}
2647

27-
private class RaceTrack(input: List<String>) : Grid<Char>(input, '#') {
28-
private val wall = nullElement
48+
private class RaceTrack(input: List<String>) : Grid<Char>(input, WALL) {
49+
50+
companion object {
51+
const val START = 'S'
52+
const val GOAL = 'E'
53+
const val WALL = '#'
54+
const val EMPTY = '.'
55+
const val VISITED = 'o'
56+
const val SHORTCUT_START = 'x'
57+
}
58+
2959
override fun Char.toElementType(): Char = this
3060

31-
private val start = find('S')
32-
private val goal = find('E')
61+
private val start = find(START)
62+
private val goal = find(GOAL)
3363

3464
fun shortestPath(): ShortestPath<Coordinates> {
3565
val neighbours: (Coordinates) -> List<Coordinates> = { node ->
3666
Direction
3767
.entries
3868
.map { node + it }
39-
.filter { get(node) != wall }
69+
.filter { get(node) != WALL }
4070
}
4171

4272
val d: (Coordinates, Coordinates) -> Int = { _, _ ->
@@ -46,7 +76,12 @@ private class RaceTrack(input: List<String>) : Grid<Char>(input, '#') {
4676

4777
val h: (Coordinates) -> Int = { it taxiDistanceTo goal }
4878

49-
return aStar(start, { this == goal }, neighbours, d, h)
79+
return aStar(
80+
start, { this == goal }, neighbours, d, h,
81+
printIt = { visited, _, _ ->
82+
recorder?.record(toString(visited.filter { get(it) == EMPTY }.toSet(), VISITED))
83+
}
84+
)
5085
}
5186

5287
fun countAllShortcutsSavingAtLeast(minToSave: Int, shortcutMaxLength: Int): Int {
@@ -61,11 +96,14 @@ private class RaceTrack(input: List<String>) : Grid<Char>(input, '#') {
6196
fun Coordinates.isShorterToGoalThan(position: Coordinates): Boolean =
6297
distances.getValue(this) + (this taxiDistanceTo position) <= distances.getValue(position) - minToSave
6398

64-
return shortestPath.path.flatMap { position ->
99+
val shortcutStartPositions = shortestPath.path.flatMap { position ->
65100
shortestPath
66101
.path
67-
.filter { it.isInShortcutRangeFrom(position) }
68-
.filter { it.isShorterToGoalThan(position) }
69-
}.size
102+
.filter { position.isInShortcutRangeFrom(it) }
103+
.filter { position.isShorterToGoalThan(it) }
104+
}
105+
recorder?.recordLastFrameWithOverrides(shortcutStartPositions, SHORTCUT_START)
106+
recorder?.repeatLast(2, 3)
107+
return shortcutStartPositions.size
70108
}
71109
}

0 commit comments

Comments
 (0)