day 18 puzzle 1

This commit is contained in:
Louis Heredero 2023-12-18 22:09:14 +01:00
parent 56b7f06e77
commit 2c429c5bbb
5 changed files with 396 additions and 0 deletions

77
src/day18/Painter.scala Normal file
View File

@ -0,0 +1,77 @@
package day18
import scala.collection.mutable.ArrayBuffer
class Painter(startX: Int, startY: Int, excludeX: Int, excludeY: Int) extends Walker(startX, startY, excludeX, excludeY) {
def walk(grid: Array[Array[Byte]], zones: Array[Array[Int]], path: ArrayBuffer[(Int, Int)]): Unit = {
val prevX: Int = lastX
val prevY: Int = lastY
val curX: Int = x
val curY: Int = y
val curTile: Byte = grid(y)(x)
super.walk(grid)
val newX: Int = x
val newY: Int = y
val dx: Int = newX - prevX
val dy: Int = newY - prevY
// West-East
if (curTile == 5) {
// Going east
if (dx > 0) {
setZone(path, zones, curX, curY - 1, 1)
setZone(path, zones, curX, curY + 1, 2)
// Going west
} else {
setZone(path, zones, curX, curY - 1, 2)
setZone(path, zones, curX, curY + 1, 1)
}
// North-South
} else if (curTile == 10) {
// Going south
if (dy > 0) {
setZone(path, zones, curX+1, curY, 1)
setZone(path, zones, curX-1, curY, 2)
// Going north
} else {
setZone(path, zones, curX+1, curY, 2)
setZone(path, zones, curX-1, curY, 1)
}
// Corner
} else if (curTile != 15) {
val east: Boolean = (curTile & 1) != 0
val south: Boolean = (curTile & 2) != 0
val toEast: Boolean = dx > 0
val toSouth: Boolean = dy > 0
val offsetBottom: Boolean = east ^ toEast ^ toSouth
val zoneY: Boolean = !toSouth ^ east
val offsetRight: Boolean = south ^ toEast ^ toSouth
val zoneX: Boolean = toEast ^ south
val offsetY: Int = if (offsetBottom) 1 else -1
val zoneYVal: Int = if (zoneY) 2 else 1
setZone(path, zones, curX, curY+offsetY, zoneYVal)
val offsetX: Int = if (offsetRight) 1 else -1
val zoneXVal: Int = if (zoneX) 2 else 1
setZone(path, zones, curX+offsetX, curY, zoneXVal)
}
}
private def setZone(path: ArrayBuffer[(Int, Int)], zones: Array[Array[Int]], posX: Int, posY: Int, zone: Int): Unit = {
val height: Int = zones.length
val width: Int = if (height == 0) 0 else zones(0).length
if (0 <= posX && posX < width && 0 <= posY && posY < height) {
if (!path.contains((posX, posY))) {
zones(posY)(posX) = zone
}
}
}
}

234
src/day18/Puzzle1.scala Normal file
View File

@ -0,0 +1,234 @@
package day18
import util.Ansi
import scala.collection.immutable.HashMap
import scala.collection.mutable.ArrayBuffer
import scala.io.{BufferedSource, Source}
object Puzzle1 {
var grid: Array[Array[Byte]] = Array.empty
var zones: Array[Array[Int]] = Array.empty
val OFFSETS: Array[(Int, Int)] = Array(
(1, 0), (0, 1), (-1, 0), (0, -1)
)
val DIRS: String = "RDLU"
// bits: NWSE
val TILES: Map[Char, Byte] = HashMap(
'|' -> 10,
'-' -> 5,
'L' -> 9,
'J' -> 12,
'7' -> 6,
'F' -> 3,
'.' -> 0,
'S' -> 15
)
val TILE_CHARS: Map[Int, Char] = HashMap(
10 -> '│',
5 -> '─',
9 -> '└',
12 -> '┘',
6 -> '┐',
3 -> '┌',
0 -> ' ',
15 -> '█'
)
val TILE_CHARS_BOLD: Map[Int, Char] = HashMap(
10 -> '┃',
5 -> '━',
9 -> '┗',
12 -> '┛',
6 -> '┓',
3 -> '┏',
0 -> ' ',
15 -> '█'
)
var height: Int = 0
var width: Int = 0
var startX: Int = 0
var startY: Int = 0
var path: ArrayBuffer[(Int, Int)] = new ArrayBuffer()
def loadInput(path: String): Unit = {
val source: BufferedSource = Source.fromFile(path)
val lines: Array[String] = source.getLines().toArray
var x: Int = 0
var y: Int = 0
var minX: Int = 0
var minY: Int = 0
var maxX: Int = 0
var maxY: Int = 0
for (line: String <- lines) {
val parts: Array[String] = line.split(" ")
val dir: Int = DIRS.indexOf(parts(0))
val dist: Int = parts(1).toInt
val offset: (Int, Int) = OFFSETS(dir)
x += offset._1*dist
y += offset._2*dist
minX = math.min(minX, x)
minY = math.min(minY, y)
maxX = math.max(maxX, x)
maxY = math.max(maxY, y)
}
width = maxX - minX + 1
height = maxY - minY + 1
grid = Array.ofDim(height, width)
zones = Array.ofDim(height, width)
x = -minX
y = -minY
var prevDir: Int = DIRS.indexOf(lines.last.split(" ")(0))
startX = x
startY = y
for ((line: String, i: Int) <- lines.zipWithIndex) {
val parts: Array[String] = line.split(" ")
val dir: Int = DIRS.indexOf(parts(0))
val dist: Int = parts(1).toInt
val offset: (Int, Int) = OFFSETS(dir)
for (_: Int <- 0 until dist) {
val prevBit: Int = 1 << ((prevDir + 2) % 4)
val curBit: Int = 1 << dir
grid(y)(x) = (prevBit| curBit).toByte
x += offset._1
y += offset._2
prevDir = dir
}
}
source.close()
}
def display(): Unit = {
for (y: Int <- 0 until height) {
for (x: Int <- 0 until width) {
val tile: Byte = grid(y)(x)
val zone: Int = zones(y)(x)
if (zone == 1) {
print(Ansi.BG_RGB(163, 61, 61))
} else if (zone == 2) {
print(Ansi.BG_RGB(77, 163, 61))
}
if (path.contains((x, y))) {
print(Ansi.BOLD)
print(TILE_CHARS_BOLD(tile))
} else {
print(TILE_CHARS(tile))
}
print(Ansi.CLEAR)
}
println()
}
}
def calculateArea(): Int = {
path = new ArrayBuffer()
path.addOne((startX, startY))
val walker: Walker = new Walker(startX, startY)
do {
walker.walk(grid)
path.addOne(walker.getPos())
} while (walker.getX() != startX || walker.getY() != startY)
println(s"Found path (length = ${path.length})")
val painter: Painter = new Painter(startX, startY, 2*startX-path(1)._1, 2*startY-path(1)._2)
do {
painter.walk(grid, zones, path)
} while (painter.getX() != startX || painter.getY() != startY)
println("Painted path neighbours")
var newZones: Array[Array[Int]] = Array.ofDim(height, width)
var changed: Boolean = true
var exteriorZone: Int = 0
do {
changed = false
newZones = copyZones()
for (y: Int <- 0 until height) {
for (x: Int <- 0 until width) {
if (zones(y)(x) != 0) {
if (floodTile(x, y, newZones, path)) {
changed = true
}
}
}
}
zones = newZones
} while (changed)
println("Flooded zones")
for (y: Int <- 0 until height) {
for (x: Int <- 0 until width) {
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) {
if (zones(y)(x) != 0) {
exteriorZone = zones(y)(x)
}
}
}
}
println(s"Found exterior zone: $exteriorZone")
var area: Int = 0
for (y: Int <- 0 until height) {
for (x: Int <- 0 until width) {
if (zones(y)(x) != exteriorZone) area += 1
}
}
println(s"Calculated area: $area")
return area
}
def floodTile(x: Int, y: Int, newZones: Array[Array[Int]], path: ArrayBuffer[(Int, Int)]): Boolean = {
var changed: Boolean = false
for ((dx: Int, dy: Int) <- Walker.OFFSETS) {
val x2: Int = x + dx
val y2: Int = y + dy
if (0 <= x2 && x2 < width && 0 <= y2 && y2 < height) {
if (zones(y2)(x2) == 0 && !path.contains((x2, y2))) {
newZones(y2)(x2) = zones(y)(x)
changed = true
}
}
}
return changed
}
def copyZones(): Array[Array[Int]] = {
val result: Array[Array[Int]] = Array.ofDim(height, width)
for (y: Int <- 0 until height) {
for (x: Int <- 0 until width) {
result(y)(x) = zones(y)(x)
}
}
return result
}
def solve(path: String): Int = {
loadInput(path)
val solution: Int = calculateArea()
return solution
}
def main(args: Array[String]): Unit = {
val solution: Int = solve("./res/day18/input1.txt")
println(solution)
//display()
}
}

62
src/day18/Walker.scala Normal file
View File

@ -0,0 +1,62 @@
package day18
class Walker {
protected var x: Int = 0
protected var y: Int = 0
protected var lastX: Int = -1
protected var lastY: Int = -1
protected var distance: Int = 0
def this(startX: Int, startY: Int) = {
this()
x = startX
y = startY
}
def this(startX: Int, startY: Int, excludeX: Int, excludeY: Int) = {
this(startX, startY)
lastX = excludeX
lastY = excludeY
}
def getX(): Int = x
def getY(): Int = y
def getPos(): (Int, Int) = (x, y)
def getDistance(): Int = distance
def walk(grid: Array[Array[Byte]]): Unit = {
val height: Int = grid.length
val width: Int = if (height == 0) 0 else grid(0).length
val (x2: Int, y2: Int) = getNextPos(grid, width, height)
lastX = x
lastY = y
x = x2
y = y2
distance += 1
}
private def getNextPos(grid: Array[Array[Byte]], width: Int, height: Int): (Int, Int) = {
val curTile: Byte = grid(y)(x)
for (((dx: Int, dy: Int), i: Int) <- Walker.OFFSETS.zipWithIndex) {
val x2: Int = x + dx
val y2: Int = y + dy
if (x2 != lastX || y2 != lastY) {
if (0 <= x2 && x2 < width && 0 <= y2 && y2 < height) {
val bit: Byte = (1 << i).toByte
val bit2: Byte = (1 << ((i + 2) % 4)).toByte
if ((curTile & bit) != 0 && (grid(y2)(x2) & bit2) != 0) {
return (x2, y2)
}
}
}
}
throw new Exception("Dead-end path")
}
}
object Walker {
val OFFSETS: Array[(Int, Int)] = Array(
(1, 0), (0, 1), (-1, 0), (0, -1)
)
}

View File

@ -0,0 +1,9 @@
package day18
import org.scalatest.funsuite.AnyFunSuite
class Puzzle1Test extends AnyFunSuite {
test("Puzzle1.solve") {
assert(Puzzle1.solve("tests_res/day18/input1.txt") == 62)
}
}

View File

@ -0,0 +1,14 @@
R 6 (#70c710)
D 5 (#0dc571)
L 2 (#5713f0)
D 2 (#d2c081)
R 2 (#59c680)
D 2 (#411b91)
L 5 (#8ceee2)
U 2 (#caa173)
L 1 (#1b58a2)
U 2 (#caa171)
R 2 (#7807d2)
U 3 (#a77fa3)
L 2 (#015232)
U 2 (#7a21e3)