From 46ba2480f9aa5d25b7e1ef05475121b9ea492fe3 Mon Sep 17 00:00:00 2001 From: scarf Date: Thu, 3 Oct 2024 09:36:40 +0900 Subject: [PATCH] feat: 2015 day 18 --- 2015/day18.scala | 90 +++++++++++++++++++++++++++++++++ 2015/day18.test.scala | 115 ++++++++++++++++++++++++++++++++++++++++++ 2015/grid.scala | 46 +++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 2015/day18.scala create mode 100644 2015/day18.test.scala create mode 100644 2015/grid.scala diff --git a/2015/day18.scala b/2015/day18.scala new file mode 100644 index 0000000..becfa6c --- /dev/null +++ b/2015/day18.scala @@ -0,0 +1,90 @@ +package `2015`.day18 + +import prelude.* +import utils.* + +enum Cell: + case On, Off + override def toString = if this == Cell.On then "#" else "." + +extension (sc: StringContext) + def gol(args: Any*): GameOfLifeGrid = + sc.s(args*).trim.stripMargin.pipe(GameOfLifeGrid.of) + +class GameOfLifeGrid(override val s: Size) extends Grid[Cell](s): + def apply(buf: Grid[Cell]) = + for + y <- 0 until s.height + x <- 0 until s.width + do this(Point(y, x)) = buf(Point(y, x)) + + def neighbors(pos: Point): Int = + val coords = for + dy <- -1 to 1 + dx <- -1 to 1 + if dy != 0 || dx != 0 + yield pos + Point(dy, dx) + coords.map(this.get).flatten.count(_ == Cell.On) + + def next(pos: Point): Cell = + val n = neighbors(pos) + apply(pos) match + case Cell.On if n == 2 || n == 3 => Cell.On + case Cell.Off if n == 3 => Cell.On + case _ => Cell.Off + +object GameOfLifeGrid: + def of(other: String): GameOfLifeGrid = + other.split("\n").toVector.pipe(of) + + def of(other: Seq[String]): GameOfLifeGrid = + val s = Size(other.size, other.head.size) + val g = new GameOfLifeGrid(s) + for + y <- 0 until s.height + x <- 0 until s.width + do g(Point(y, x)) = if other(y)(x) == '#' then Cell.On else Cell.Off + g + +extension (g: GameOfLifeGrid) + def part1Next: GameOfLifeGrid = + val nextGrid = new GameOfLifeGrid(g.s) + for + y <- 0 until g.s.height + x <- 0 until g.s.width + do nextGrid(Point(y, x)) = g.next(Point(y, x)) + nextGrid + + def stuckCorners(): GameOfLifeGrid = + Set( + Point(0, 0), + Point(0, g.s.width - 1), + Point(g.s.height - 1, 0), + Point(g.s.height - 1, g.s.width - 1), + ).foreach { g.update(_, Cell.On) } + g + + def part2Next: GameOfLifeGrid = + val nextGrid = new GameOfLifeGrid(g.s) + g.stuckCorners() + for + y <- 0 until g.s.height + x <- 0 until g.s.width + do nextGrid(Point(y, x)) = g.next(Point(y, x)) + nextGrid.stuckCorners() + nextGrid + +def solution(grid: GameOfLifeGrid, f: GameOfLifeGrid => GameOfLifeGrid) = + Iterator.iterate(grid)(f).drop(100).next.raw.count(_ == Cell.On) + +def part1(grid: GameOfLifeGrid) = + solution(grid, _.part1Next) + +def part2(grid: GameOfLifeGrid) = + solution(grid, _.part2Next) + +@main def main() = + val raw = fromFile(".cache/2015/18.txt").mkString + + println(part1(GameOfLifeGrid.of(raw))) + println(part2(GameOfLifeGrid.of(raw))) diff --git a/2015/day18.test.scala b/2015/day18.test.scala new file mode 100644 index 0000000..0599593 --- /dev/null +++ b/2015/day18.test.scala @@ -0,0 +1,115 @@ +package `2015`.day18 + +import prelude.* +import munit.FunSuite + +class Tests extends FunSuite: + test("part1"): + val grid = gol""" + |.#.#.# + |...##. + |#....# + |..#... + |#.#..# + |####.. + """ + val steps = Iterator + .iterate(grid)(_.part1Next) + .take(5) + .drop(1) + .toSeq + + val expected = Seq( + gol""" + |..##.. + |..##.# + |...##. + |...... + |#..... + |#.##.. + """, + gol""" + |..###. + |...... + |..###. + |...... + |.#.... + |.#.... + """, + gol""" + |...#.. + |...... + |...#.. + |..##.. + |...... + |...... + """, + gol""" + |...... + |...... + |..##.. + |..##.. + |...... + |...... + """, + ) + assertEquals(steps, expected) + + test("part2"): + val grid = gol""" + |##.#.# + |...##. + |#....# + |..#... + |#.#..# + |####.# + """ + val steps = Iterator + .iterate(grid)(_.part2Next) + .take(6) + .drop(1) + .toSeq + + val expected = Seq( + gol""" + |#.##.# + |####.# + |...##. + |...... + |#...#. + |#.#### + """, + gol""" + |#..#.# + |#....# + |.#.##. + |...##. + |.#..## + |##.### + """, + gol""" + |#...## + |####.# + |..##.# + |...... + |##.... + |####.# + """, + gol""" + |#.#### + |#....# + |...#.. + |.##... + |#..... + |#.#..# + """, + gol""" + |##.### + |.##..# + |.##... + |.##... + |#.#... + |##...# + """, + ) + assertEquals(steps, expected) diff --git a/2015/grid.scala b/2015/grid.scala new file mode 100644 index 0000000..5fa2380 --- /dev/null +++ b/2015/grid.scala @@ -0,0 +1,46 @@ +package utils + +import scala.reflect.ClassTag + +trait GridOps[A: ClassTag]: + def s: Size + def raw: Array[A] + + def apply(pos: Point): A + + def update(pos: Point, a: A): Unit + + override def toString(): String = + val sb = new StringBuilder + for y <- 0 until s.height do + for x <- 0 until s.width do + sb.append(apply(Point(y, x)) match + case null => ' ' + case a => a.toString.head, + ) + sb.append('\n') + sb.toString + + def data: Array[Array[A]] = raw.grouped(s.width).toArray + def get(pos: Point): Option[A] = + if pos.y >= 0 && pos.y < s.height && pos.x >= 0 && pos.x < s.width then + Some(apply(pos)) + else None + + def of(grid: Seq[Seq[A]]): Grid[A] = + val s = Size(grid(0).length, grid.length) + val g = new Grid[A](s) + for + y <- 0 until s.height + x <- 0 until s.width + do g(Point(y, x)) = grid(y)(x) + g + +class Grid[A: ClassTag](val s: Size) extends GridOps[A]: + val raw = new Array[A](s.width * s.height) + def apply(pos: Point): A = raw(pos.y * s.width + pos.x) + def update(pos: Point, a: A): Unit = raw(pos.y * s.width + pos.x) = a + + override def equals(that: Any): Boolean = that match + case that: Grid[?] => this.s == that.s && this.raw.sameElements(that.raw) + case _ => false