Skip to content

Commit 3d53c12

Browse files
committed
Solution 2017-21 (Fractal Art)
1 parent 89c2be5 commit 3d53c12

File tree

2 files changed

+295
-0
lines changed

2 files changed

+295
-0
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package de.ronny_h.aoc.year2017.day21
2+
3+
import de.ronny_h.aoc.AdventOfCode
4+
import de.ronny_h.aoc.extensions.asList
5+
import de.ronny_h.aoc.extensions.grids.Coordinates
6+
import de.ronny_h.aoc.extensions.grids.Grid
7+
8+
fun main() = FractalArt().run(150, 2606275)
9+
10+
class FractalArt : AdventOfCode<Int>(2017, 21) {
11+
override fun part1(input: List<String>): Int = applyTheRulesToTheGivenInitConfiguration(input, 5)
12+
override fun part2(input: List<String>): Int = applyTheRulesToTheGivenInitConfiguration(input, 18)
13+
14+
fun applyTheRulesToTheGivenInitConfiguration(input: List<String>, iterations: Int): Int {
15+
val rules = input.parseEnhancementRules()
16+
val initConfiguration = """
17+
.#.
18+
..#
19+
###
20+
""".asList()
21+
var grid = FractalArtGrid(initConfiguration)
22+
repeat(iterations) {
23+
grid = grid.applyRules(rules)
24+
}
25+
return grid.countPixelsThatAreOn()
26+
}
27+
}
28+
29+
data class Rule(val pattern: List<String>, val onCount: Int, val replacement: List<String>)
30+
data class RuleSet(val even: List<Rule>, val odd: List<Rule>)
31+
32+
private const val on = '#'
33+
private const val off = '.'
34+
35+
fun List<String>.parseEnhancementRules(): RuleSet {
36+
val rules = map { input ->
37+
val (p, r) = input.split(" => ")
38+
Rule(pattern = p.split("/"), onCount = p.count { it == on }, replacement = r.split("/"))
39+
}.partition { it.pattern.size % 2 == 0 }
40+
return RuleSet(even = rules.first, odd = rules.second)
41+
}
42+
43+
class FractalArtGrid(size: Int) : Grid<Char>(size, size, off) {
44+
45+
constructor(initConfiguration: List<String>) : this(initConfiguration.size) {
46+
check(initConfiguration.size == initConfiguration[0].length) { "FractalArtGrid has to be square" }
47+
initGrid(initConfiguration)
48+
}
49+
50+
override fun Char.toElementType(): Char = this
51+
52+
fun countPixelsThatAreOn(): Int = forEachElement { _, _, e -> e }.filter { it == on }.count()
53+
54+
fun applyRules(rules: RuleSet): FractalArtGrid {
55+
val squareSize = if (height % 2 == 0) 2 else 3
56+
val numberOfSquares = height / squareSize
57+
val newGrid = FractalArtGrid(numberOfSquares * (squareSize + 1))
58+
val rulesToApply = if (height % 2 == 0) rules.even else rules.odd
59+
forEachSquareOfSize(squareSize) { start, square ->
60+
var applied = false
61+
for (rule in rulesToApply) {
62+
if (rule.onCount != square.countPixelsThatAreOn()) {
63+
// filter out bad candidates early
64+
continue
65+
}
66+
if (squareSize == 2 && rule.onCount != 2 || rule.ruleMatches(square) || rule.ruleMatches(square.reversed())) {
67+
newGrid.applyRuleAt(
68+
rule,
69+
Coordinates(start.row + (start.row / squareSize), start.col + (start.col / squareSize))
70+
)
71+
applied = true
72+
break
73+
}
74+
}
75+
if (!applied) {
76+
throw RuntimeException("No rule to apply for square $square at $start.")
77+
}
78+
}
79+
return newGrid
80+
}
81+
82+
private fun Rule.ruleMatches(square: List<List<Char>>): Boolean =
83+
// exact match
84+
square.allMatch { row, col, value -> pattern[row][col] == value } ||
85+
86+
// rotate 90° right
87+
// 0 1
88+
// 0 [ 0 , 1 ] -> [ 2 , 0 ] = [ 1,0 , 0,0 ] = [ size-col,row , size-col,row ]
89+
// 1 [ 2 , 3 ] [ 3 , 1 ] [ 1,1 , 0,1 ] [ size-col,row , size-col,row ]
90+
square.allMatch { row, col, value -> pattern[square.lastIndex - col][row] == value } ||
91+
92+
// rotate 180°
93+
// 0 1
94+
// 0 [ 0 , 1 ] -> [ 3 , 2 ] = [ 1,1 , 1,0 ] = [ size-row,size-col , size-row,size-col ]
95+
// 1 [ 2 , 3 ] [ 1 , 0 ] [ 0,1 , 0,0 ] [ size-row,size-col , size-row,size-col ]
96+
square.allMatch { row, col, value -> pattern[square.lastIndex - row][square.lastIndex - col] == value } ||
97+
98+
// rotate 90° left
99+
// 0 1
100+
// 0 [ 0 , 1 ] -> [ 1 , 3 ] = [ 0,1 , 1,1 ] = [ col,size-row , col,size-row ]
101+
// 1 [ 2 , 3 ] [ 0 , 2 ] [ 0,0 , 1,0 ] [ col,size-row , col,size-row ]
102+
square.allMatch { row, col, value -> pattern[col][square.lastIndex - row] == value }
103+
104+
private fun List<List<Char>>.allMatch(condition: (Int, Int, Char) -> Boolean): Boolean {
105+
forEachIndexed { row, rowList ->
106+
rowList.forEachIndexed { col, value ->
107+
if (!condition(row, col, value)) {
108+
return false
109+
}
110+
}
111+
}
112+
return true
113+
}
114+
115+
private fun applyRuleAt(rule: Rule, start: Coordinates) {
116+
rule.replacement.forEachIndexed { row, rowString ->
117+
rowString.forEachIndexed { col, value ->
118+
this[start.row + row, start.col + col] = value
119+
}
120+
}
121+
}
122+
123+
private fun forEachSquareOfSize(size: Int, block: (Coordinates, List<List<Char>>) -> Unit) {
124+
for (row in 0..<height step size) {
125+
for (col in 0..<height step size) {
126+
block(Coordinates(row, col), subGridAt(row, col, size))
127+
}
128+
}
129+
}
130+
}
131+
132+
private fun List<List<Char>>.countPixelsThatAreOn() = sumOf { row ->
133+
row.count { it == on }
134+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package de.ronny_h.aoc.year2017.day21
2+
3+
import de.ronny_h.aoc.extensions.asList
4+
import io.kotest.core.spec.style.StringSpec
5+
import io.kotest.matchers.shouldBe
6+
7+
class FractalArtTest : StringSpec({
8+
9+
val input = """
10+
../.# => ##./#../...
11+
.#./..#/### => #..#/..../..../#..#
12+
""".asList()
13+
14+
"input can be parsed" {
15+
input.parseEnhancementRules() shouldBe RuleSet(
16+
listOf(
17+
Rule(
18+
pattern = listOf("..", ".#"),
19+
onCount = 1,
20+
replacement = listOf("##.", "#..", "...")
21+
)
22+
),
23+
listOf(
24+
Rule(
25+
pattern = listOf(".#.", "..#", "###"),
26+
onCount = 5,
27+
replacement = listOf("#..#", "....", "....", "#..#")
28+
)
29+
),
30+
)
31+
}
32+
33+
"a 3x3 rule applies to a 3x3 grid that exactly matches the pattern" {
34+
val rules = listOf(".#./..#/### => #..#/..../..../#..#").parseEnhancementRules()
35+
val initConfiguration = """
36+
.#.
37+
..#
38+
###
39+
""".asList()
40+
val grid = FractalArtGrid(initConfiguration)
41+
42+
grid.applyRules(rules).toString().trimIndent() shouldBe """
43+
#..#
44+
....
45+
....
46+
#..#
47+
""".trimIndent()
48+
}
49+
50+
"a 3x3 rule applies to a 3x3 grid that matches the rotated pattern" {
51+
val rules = listOf(".#./..#/### => #..#/..../..../#..#").parseEnhancementRules()
52+
val initConfiguration = """
53+
.##
54+
#.#
55+
..#
56+
""".asList()
57+
val grid = FractalArtGrid(initConfiguration)
58+
59+
grid.applyRules(rules).toString().trimIndent() shouldBe """
60+
#..#
61+
....
62+
....
63+
#..#
64+
""".trimIndent()
65+
}
66+
67+
"a 3x3 rule applies to a 3x3 grid that matches the rotated and flipped pattern" {
68+
val rules = listOf(".#./..#/### => #..#/..../..../#..#").parseEnhancementRules()
69+
val initConfiguration = """
70+
..#
71+
#.#
72+
.##
73+
""".asList()
74+
val grid = FractalArtGrid(initConfiguration)
75+
76+
grid.applyRules(rules).toString().trimIndent() shouldBe """
77+
#..#
78+
....
79+
....
80+
#..#
81+
""".trimIndent()
82+
}
83+
84+
"a 3x3 rule applies to a 3x3 grid that matches the pattern flipped horizontally" {
85+
val rules = listOf(".#./..#/### => #..#/..../..../#..#").parseEnhancementRules()
86+
val initConfiguration = """
87+
.#.
88+
#..
89+
###
90+
""".asList()
91+
val grid = FractalArtGrid(initConfiguration)
92+
93+
grid.applyRules(rules).toString().trimIndent() shouldBe """
94+
#..#
95+
....
96+
....
97+
#..#
98+
""".trimIndent()
99+
}
100+
101+
"a 3x3 rule applies to a 3x3 grid that matches the pattern flipped vertically" {
102+
val rules = listOf(".#./..#/### => #..#/..../..../#..#").parseEnhancementRules()
103+
val initConfiguration = """
104+
###
105+
..#
106+
.#.
107+
""".asList()
108+
val grid = FractalArtGrid(initConfiguration)
109+
110+
grid.applyRules(rules).toString().trimIndent() shouldBe """
111+
#..#
112+
....
113+
....
114+
#..#
115+
""".trimIndent()
116+
}
117+
118+
"a 2x2 rule applies to a 4x4 grid for exact matches of the pattern" {
119+
val rules = listOf("../.# => ##./#../...").parseEnhancementRules()
120+
val initConfiguration = """
121+
....
122+
.#.#
123+
....
124+
.#.#
125+
""".asList()
126+
val grid = FractalArtGrid(initConfiguration)
127+
128+
grid.applyRules(rules).toString().trimIndent() shouldBe """
129+
##.##.
130+
#..#..
131+
......
132+
##.##.
133+
#..#..
134+
......
135+
""".trimIndent()
136+
}
137+
138+
"a 2x2 rule applies to a 4x4 grid for rotated matches of the pattern" {
139+
val rules = listOf("#./.# => ##./#../...").parseEnhancementRules()
140+
val initConfiguration = """
141+
#..#
142+
.##.
143+
.##.
144+
#..#
145+
""".asList()
146+
val grid = FractalArtGrid(initConfiguration)
147+
148+
grid.applyRules(rules).toString().trimIndent() shouldBe """
149+
##.##.
150+
#..#..
151+
......
152+
##.##.
153+
#..#..
154+
......
155+
""".trimIndent()
156+
}
157+
158+
"part 1 and 2: with the example rules, after 2 iterations 12 pixels are on" {
159+
FractalArt().applyTheRulesToTheGivenInitConfiguration(input, 2) shouldBe 12
160+
}
161+
})

0 commit comments

Comments
 (0)