commit 9017c4379c9035917d4b588b72a26cf339211151 Author: Alexander Gehrke Date: Thu Dec 2 23:34:40 2021 +0100 Add base structure and library stuff from last year diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..047aaca --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.bloop +.bsp +target/ +project/project +project/bloop.sbt +project/metals.sbt diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..b5d109f --- /dev/null +++ b/build.sbt @@ -0,0 +1,20 @@ +lazy val root = project + .in(file(".")) + .settings( + name := "aoc2021", + version := "0.1.0", + + scalaVersion := "3.1.0", + scalacOptions ++= Seq( + "-Yexplicit-nulls", + "-language:strict-equality", + "-deprecation", + ), + libraryDependencies ++= Seq( + "org.typelevel" %% "cats-core" % "2.6.1", + "org.typelevel" %% "cats-effect" % "3.1.1", + "org.typelevel" %% "kittens" % "3.0.0-M1", + ) + ) + +Runtime / unmanagedSources += baseDirectory.value / "input" diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..67d27a1 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.5.3 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..32c4ba8 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +//addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.29") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3") diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day1.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day1.scala new file mode 100644 index 0000000..cd58684 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day1.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day1(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day10.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day10.scala new file mode 100644 index 0000000..7125092 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day10.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day10(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day11.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day11.scala new file mode 100644 index 0000000..be34951 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day11.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day11(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day12.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day12.scala new file mode 100644 index 0000000..3c98a57 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day12.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day12(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day13.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day13.scala new file mode 100644 index 0000000..230a145 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day13.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day13(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day14.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day14.scala new file mode 100644 index 0000000..506abd4 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day14.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day14(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day15.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day15.scala new file mode 100644 index 0000000..982ffe5 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day15.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day15(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day16.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day16.scala new file mode 100644 index 0000000..053d9ba --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day16.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day16(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day17.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day17.scala new file mode 100644 index 0000000..6cf7c46 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day17.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day17(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day18.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day18.scala new file mode 100644 index 0000000..a4c0eb2 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day18.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day18(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day19.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day19.scala new file mode 100644 index 0000000..b79c880 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day19.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day19(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day2.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day2.scala new file mode 100644 index 0000000..2d71836 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day2.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day2(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day20.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day20.scala new file mode 100644 index 0000000..9f42879 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day20.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day20(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day21.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day21.scala new file mode 100644 index 0000000..59b5659 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day21.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day21(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day22.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day22.scala new file mode 100644 index 0000000..22d782f --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day22.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day22(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day23.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day23.scala new file mode 100644 index 0000000..ce8468f --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day23.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day23(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day24.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day24.scala new file mode 100644 index 0000000..8350506 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day24.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day24(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day25.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day25.scala new file mode 100644 index 0000000..a283fd9 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day25.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day25(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day3.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day3.scala new file mode 100644 index 0000000..ab7b569 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day3.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day3(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day4.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day4.scala new file mode 100644 index 0000000..8141f42 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day4.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day4(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day5.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day5.scala new file mode 100644 index 0000000..62021b7 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day5.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day5(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day6.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day6.scala new file mode 100644 index 0000000..e86d217 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day6.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day6(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day7.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day7.scala new file mode 100644 index 0000000..ebeaab1 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day7.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day7(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day8.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day8.scala new file mode 100644 index 0000000..dc892b4 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day8.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day8(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/2021/day9.scala b/src/main/scala/de.qwertyuiop.aoc/2021/day9.scala new file mode 100644 index 0000000..710c992 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/2021/day9.scala @@ -0,0 +1,6 @@ +package de.qwertyuiop.aoc.`2021` + +import de.qwertyuiop.aoc.lib.* +import cats.*, cats.implicits.given + +def day9(using InputSource): Unit = ??? diff --git a/src/main/scala/de.qwertyuiop.aoc/Main.scala b/src/main/scala/de.qwertyuiop.aoc/Main.scala new file mode 100644 index 0000000..772d9d1 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/Main.scala @@ -0,0 +1,37 @@ +package de.qwertyuiop.aoc +import de.qwertyuiop.aoc.lib._ +import de.qwertyuiop.aoc.`2021`._ +@main def runDay(inputDir: String, day: Int, sample: Int*): Unit = + given InputSource = inputSource(inputDir, day, sample.headOption) + day match { + case 1 => day1 + case 2 => day2 + case 3 => day3 + case 4 => day4 + case 5 => day5 + case 6 => day6 + case 7 => day7 + case 8 => day8 + case 9 => day9 + case 10 => day10 + case 11 => day11 + case 12 => day12 + case 13 => day13 + case 14 => day14 + case 15 => day15 + case 16 => day16 + case 17 => day17 + case 18 => day18 + case 19 => day19 + case 20 => day20 + case 21 => day21 + case 22 => day22 + case 23 => day23 + case 24 => day24 + case 25 => day25 + case _ => println("No such day implemented") + } + +def inputSource(inputDir:String, day: Int, sample: Option[Int]) = + sample.map(InputSource.SampleLocation(inputDir, day, _)) + .getOrElse(InputSource.Location(inputDir, day)) diff --git a/src/main/scala/de.qwertyuiop.aoc/lib/RingBuffer.scala b/src/main/scala/de.qwertyuiop.aoc/lib/RingBuffer.scala new file mode 100644 index 0000000..024fe78 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/lib/RingBuffer.scala @@ -0,0 +1,28 @@ +package de.qwertyuiop.aoc.lib + +import scala.annotation.targetName + +/* Wrap a vector to create a limited size ring buffer */ + +case class RingBuffer[A](underlying: Vector[A], maxSize: Int): + /* pass through any methods to the underlying vector, except appending and + * prepending single elements */ + export underlying.{ :+ => _, appended => _, +: => _, prepended => _, _ } + + inline def full: Boolean = underlying.size == maxSize + + @targetName("appended") def :+[B >: A](b: B): RingBuffer[B] = + val appendedVec = (if full then underlying.tail else underlying) :+ b + new RingBuffer(appendedVec, maxSize) + + @targetName("prepended") def +:[B >: A](b: B): RingBuffer[B] = + val prependedVec = b +: (if full then underlying.init else underlying) + new RingBuffer(prependedVec, maxSize) + +object RingBuffer: + def apply[A](underlying: Vector[A], maxSize: Int): RingBuffer[A] = + new RingBuffer( + if underlying.size > maxSize then underlying.view.slice(0, maxSize).toVector + else underlying, + maxSize + ) diff --git a/src/main/scala/de.qwertyuiop.aoc/lib/extensions.scala b/src/main/scala/de.qwertyuiop.aoc/lib/extensions.scala new file mode 100644 index 0000000..0bada30 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/lib/extensions.scala @@ -0,0 +1,23 @@ +package de.qwertyuiop.aoc.lib + +/* for splitting input with separator lines */ +extension [A](input: List[A])(using CanEqual[A,A]) + def split(separator: A, keepSeparator: Boolean = false): LazyList[List[A]] = + input.span(_ != separator) match + case (Nil, Nil) => LazyList() + case (h, Nil) => LazyList(h) + case (h, tail @ (_ :: t)) => + h #:: (if keepSeparator then tail else t).split(separator) + + +/* Using -Yexplicit-nulls isn't really ready for use with the java standard + * library. e.g. String doesn't have `@NotNull` annotations for its methods + */ +extension (s: String) + def splitOnce(regex: String): Option[(String, String)] = + s.split(regex, 2) match + case Array(a, b) => Some((a.nn, b.nn)) + case _ => None + +extension [K,V,W](map: Map[K,V]) + def mapValuesS(f: V => W): Map[K, W] = map.view.mapValues(f).toMap diff --git a/src/main/scala/de.qwertyuiop.aoc/lib/inputs.scala b/src/main/scala/de.qwertyuiop.aoc/lib/inputs.scala new file mode 100644 index 0000000..c7986c5 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/lib/inputs.scala @@ -0,0 +1,31 @@ +package de.qwertyuiop.aoc.lib + +import scala.io.Source + +enum InputSource: + case Location(inputDir: String, day: Int) + case Fixed(lines: List[String]) + case SampleLocation(inputDir: String, day: Int, sample: Int) + +def inputLines(using src: InputSource): Iterator[String] = + src match + case InputSource.Location(inputDir, day) => scala.io.Source.fromFile(s"${inputDir}/day${day}.txt").getLines + case InputSource.Fixed(input) => input.iterator + case InputSource.SampleLocation(inputDir, day, sample) => scala.io.Source.fromFile(s"${inputDir}/day${day}-sample${sample}.txt").getLines + + +def simpleInput(using l: InputSource): String = inputLines.mkString + +def input[A](format: String => A = identity)(using l: InputSource): List[A] = + inputF(format)(List) + +def inputF[A, C[_]](format: String => A = identity)(coll: collection.Factory[A, C[A]])(using l: InputSource): C[A] = + inputLines.map(format).to(coll) + +def flatInput[A](format: String => List[A] = List.apply)(using l: InputSource): List[A] = + flatInputF(format)(List) + +def flatInputF[A, C[_]](format: String => IterableOnce[A] = identity)(coll: collection.Factory[A, C[A]])(using l: InputSource): C[A] = + inputLines.flatMap(format).to(coll) + +def boolChar(trueChar: Char): String => Vector[Boolean] = _.map(_ == trueChar).toVector diff --git a/src/main/scala/de.qwertyuiop.aoc/lib/misc.scala b/src/main/scala/de.qwertyuiop.aoc/lib/misc.scala new file mode 100644 index 0000000..9507073 --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/lib/misc.scala @@ -0,0 +1,9 @@ +package de.qwertyuiop.aoc.lib + +import scala.collection.mutable + +def memoize[I, O](f: I => O): I => O = new mutable.HashMap[I, O]() {self => + override def apply(key: I) = self.synchronized(getOrElseUpdate(key, f(key))) +} + +def repeat[T](n: Int)(f: T => T): T => T = Function.chain(Seq.fill(n)(f)) diff --git a/src/main/scala/de.qwertyuiop.aoc/lib/vectors.scala b/src/main/scala/de.qwertyuiop.aoc/lib/vectors.scala new file mode 100644 index 0000000..708688a --- /dev/null +++ b/src/main/scala/de.qwertyuiop.aoc/lib/vectors.scala @@ -0,0 +1,145 @@ +package de.qwertyuiop.aoc.lib + +import cats.*, cats.implicits.given +import cats.derived.semiauto + +object Vectors: + import scala.Numeric.Implicits.given + import scala.compiletime.ops.int.* + import Directions.* + + opaque type Vec2D[T] = (T, T) + opaque type Vec3D[T] = (T, T, T) + opaque type Vec4D[T] = (T, T, T, T) + + def Vec2D[T](x: T, y: T): Vec2D[T] = (x,y) + def Vec3D[T](x: T, y: T, z: T): Vec3D[T] = (x,y,z) + def Vec4D[T](x: T, y: T, z: T, w: T): Vec4D[T] = (x,y,z,w) + + trait Vec[T]: + extension (v: T) + def +(w: T): T + def neighbours: List[T] + + given [T : Show]: Show[Vec2D[T]] with + def show(v: Vec2D[T]): String = show"Vec2D(${v._1}, ${v._2})" + given [T : Show]: Show[Vec3D[T]] with + def show(v: Vec3D[T]): String = show"Vec3D(${v._1}, ${v._2}, ${v._3})" + given [T : Show]: Show[Vec4D[T]] with + def show(v: Vec4D[T]): String = show"Vec4D(${v._1}, ${v._2}, ${v._3}, ${v._4})" + + given [T: Numeric]: Vec[Vec2D[T]] with + extension (v: Vec2D[T]) + def +(w: Vec2D[T]): Vec2D[T] = (v._1 + w._1, v._2 + w._2) + def -(w: Vec2D[T]): Vec2D[T] = (v._1 - w._1, v._2 - w._2) + def *(x: T): Vec2D[T] = (v._1 * x, v._2 * x) + + def x: T = v._1 + def y: T = v._2 + + def rot(deg: Dir): Vec2D[T] = deg match + case North => (-v._2, v._1) + case West => (-v._1, -v._2) + case South => (v._2, -v._1) + case East => v + + def left(deg: Int): Vec2D[T] = repeat[Vec2D[T]](deg / 90)(_.rotLeft)(v) + def right(deg: Int): Vec2D[T] = repeat[Vec2D[T]](deg / 90)(_.rotRight)(v) + + def rotLeft: Vec2D[T] = (-v._2, v._1) + def rotRight: Vec2D[T] = (v._2, -v._1) + + def move(dir: Dir, dist: T): Vec2D[T] = dir match + case North => (v._1, v._2 + dist) + case South => (v._1, v._2 - dist) + case East => (v._1 + dist, v._2) + case West => (v._1 - dist, v._2) + + def manhattan: T = v._1.abs + v._2.abs + def neighbours: List[Vec2D[T]] = + neighbourCoords(2).map(n => v + (n(0), n(1))) + + given [T: Monoid]: Monoid[Vec2D[T]] = semiauto.monoid + given [T: Monoid]: Monoid[Vec3D[T]] = semiauto.monoid + given [T: Monoid]: Monoid[Vec4D[T]] = semiauto.monoid + + + given [T: Numeric]: Vec[Vec3D[T]] with + extension (v: Vec3D[T]) + def +(w: Vec3D[T]): Vec3D[T] = (v._1 + w._1, v._2 + w._2, v._3 + w._3) + def neighbours: List[Vec3D[T]] = + neighbourCoords(3).map(n => v + (n(0), n(1), n(2))) + + def x: T = v._1 + def y: T = v._2 + def z: T = v._3 + + given [T: Numeric]: Vec[Vec4D[T]] with + extension (v: Vec4D[T]) + def +(u: Vec4D[T]): Vec4D[T] = (v._1 + u._1, v._2 + u._2, v._3 + u._3, v._4 + u._4) + def neighbours: List[Vec4D[T]] = + neighbourCoords(4).map(n => v + (n(0), n(1), n(2), n(3))) + + def x: T = v._1 + def y: T = v._2 + def z: T = v._3 + def w: T = v._4 + + + /* compute these only once per type and dimension*/ + import scala.collection.mutable + private var _neighbourCache = mutable.Map[(Numeric[_], Int), List[List[_]]]() + def neighbourCoords[T](dim: Int)(using n: Numeric[T]): List[List[T]] = + _neighbourCache.get((n, dim)) match + case None => + val self = List.fill(dim)(n.zero) + val neighs = List.fill(dim)(List(-n.one, n.zero, n.one)).sequence[List, T] + .filter(_ != self) + _neighbourCache.put((n, dim), neighs) + neighs + case Some(neighs) => neighs.asInstanceOf[List[List[T]]] + +object Directions: + opaque type Dir = Int + val East: Dir = 0 + val North: Dir = 90 + val West: Dir = 180 + val South: Dir = 270 + + extension (c: Char) + def cardinal: Dir = c match + case 'E' => East + case 'N' => North + case 'W' => West + case 'S' => South + + def Dir(deg: Int): Dir = (deg % 360 + 360) % 360 + + private val rotationOrder = Vector(North, West, South, East, North, West) + + extension (dir: Dir) + def +(deg: Int|Dir): Dir = (dir + deg) % 360 + def -(deg: Int|Dir): Dir = ((dir - deg) % 360 + 360) % 360 + def unary_- : Dir = 360 - dir + def str: String = dir match + case East => "East" + case North => "North" + case West => "West" + case South => "South" + + def rotLeft: Dir = rotationOrder(rotationOrder.indexOf(dir) + 1) + def rotRight: Dir = rotationOrder(rotationOrder.lastIndexOf(dir) - 1) + def flip: Dir = rotationOrder(rotationOrder.indexOf(dir) + 2) + + def flipHor: Dir = dir match + case West => East + case East => West + case o => o + + def flipVert: Dir = dir match + case North => South + case South => North + case o => o + + given Show[Dir] with + def show(d: Dir): String = d.str