├── .gitignore ├── .scalafmt.conf ├── README.md ├── build.properties ├── build.sbt ├── project └── build.properties └── src ├── main └── scala │ └── com │ └── fq │ └── sudoku │ ├── CatsEffectDeferredRefRaceSolver.scala │ ├── CatsEffectQueueSolver.scala │ ├── FS2StreamSolver.scala │ └── Solver.scala └── test └── scala └── com └── fq └── sudoku ├── SolverTest.scala └── UtilsTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | data/ 3 | dist/* 4 | target/ 5 | lib_managed/ 6 | src_managed/ 7 | project/boot/ 8 | project/project/ 9 | project/target/ 10 | project/plugins/project/ 11 | .history 12 | .cache 13 | .lib/ 14 | .idea/* 15 | /.bsp/sbt.json 16 | .metals/ 17 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 2.5.0 2 | maxColumn = 100 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # concurrent-sudoku-solver 2 | 3 | Contains code for **Concurrent Sudoku Solver** blogpost series: 4 | - [Part 1: Single Candidate Technique + Domain Modelling](https://medium.com/@fqaiser94/concurrent-sudoku-solver-part-1-single-candidate-technique-domain-modelling-6c885a1e4ef3) 5 | - [Part 2: Using Cats Effect Ref + Deferred + IO.race](https://medium.com/@fqaiser94/concurrent-sudoku-solver-part-2-using-cats-effect-ref-deferred-io-race-a380a182c233) 6 | - [Part 3: Using Cats Effect Queue](https://medium.com/@fqaiser94/concurrent-sudoku-solver-part-3-using-cats-effect-queue-459e2da28b6) 7 | - [Part 4: Using FS2 Stream + Topic](https://medium.com/@fqaiser94/concurrent-sudoku-solver-part-4-using-fs2-stream-topic-949c8b099abb) -------------------------------------------------------------------------------- /build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.5.8 2 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / organization := "com.example" 2 | ThisBuild / scalaVersion := "2.13.5" 3 | 4 | lazy val root = (project in file(".")).settings( 5 | name := "concurrent-sudoku-solver", 6 | libraryDependencies ++= Seq( 7 | "org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.4", 8 | // "core" module - IO, IOApp, schedulers 9 | // This pulls in the kernel and std modules automatically. 10 | "org.typelevel" %% "cats-effect" % "3.3.5", 11 | // concurrency abstractions and primitives (Concurrent, Sync, Async etc.) 12 | "org.typelevel" %% "cats-effect-kernel" % "3.3.5", 13 | // standard "effect" library (Queues, Console, Random etc.) 14 | "org.typelevel" %% "cats-effect-std" % "3.3.5", 15 | // better monadic for compiler plugin as suggested by documentation 16 | compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), 17 | "co.fs2" %% "fs2-core" % "3.2.5", 18 | "org.typelevel" %% "munit-cats-effect-3" % "1.0.7" % Test 19 | ) 20 | ) 21 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /src/main/scala/com/fq/sudoku/CatsEffectDeferredRefRaceSolver.scala: -------------------------------------------------------------------------------- 1 | package com.fq.sudoku 2 | 3 | import cats.effect.{Deferred, IO, Ref} 4 | import cats.implicits._ 5 | import com.fq.sudoku.Solver._ 6 | 7 | object CatsEffectDeferredRefRaceSolver extends Solver[IO] { 8 | override def solve(givens: List[Value.Given]): IO[List[Value]] = 9 | for { 10 | allCells <- Coord.allCoords.traverse(Cell.make) 11 | givensMap = givens.map(g => g.coord -> g).toMap 12 | values <- allCells.parTraverse(_.solve(givensMap, allCells)) 13 | } yield values 14 | 15 | trait Cell { 16 | def coord: Coord 17 | protected[this] def deferredValue: Deferred[IO, Value] 18 | def getValue: IO[Value] = deferredValue.get 19 | def deduceSingleCandidate(allCells: List[Cell]): IO[Value] 20 | def solve(givensMap: Map[Coord, Value.Given], allCells: List[Cell]): IO[Value] = 21 | (givensMap.get(coord) match { 22 | case Some(givenValue) => IO.pure(givenValue) 23 | case None => deduceSingleCandidate(allCells) 24 | }).flatTap(deferredValue.complete) 25 | } 26 | 27 | object Cell { 28 | def make(_coord: Coord): IO[Cell] = 29 | for { 30 | _deferredValue <- Deferred[IO, Value] 31 | } yield new Cell { 32 | override val coord: Coord = _coord 33 | 34 | override val deferredValue: Deferred[IO, Value] = _deferredValue 35 | 36 | override def deduceSingleCandidate(allCells: List[Cell]): IO[Candidate.Single] = 37 | for { 38 | refCandidate <- Ref.of[IO, Candidate](Candidate.initial(coord)) 39 | peerCells = allCells.filter(cell => cell.coord.isPeerOf(coord)) 40 | listOfSingleCandidateOrNever = 41 | peerCells.map(peerCell => refineToSingleCandidateOrNever(refCandidate, peerCell)) 42 | singleCandidate <- raceMany(listOfSingleCandidateOrNever) 43 | } yield singleCandidate 44 | 45 | private def raceMany[T](listOfIOs: List[IO[T]]): IO[T] = 46 | listOfIOs.reduce((a, b) => a.race(b).map(_.merge)) 47 | 48 | private def refineToSingleCandidateOrNever( 49 | refCandidate: Ref[IO, Candidate], 50 | peerCell: Cell 51 | ): IO[Candidate.Single] = 52 | for { 53 | peerValue <- peerCell.getValue 54 | singleCandidate <- refCandidate.modify { 55 | case multiple: Candidate.Multiple => 56 | multiple.refine(peerValue) match { 57 | case single: Candidate.Single => (single, IO.pure(single)) 58 | case multiple: Candidate.Multiple => (multiple, IO.never) 59 | } 60 | case alreadySingle: Candidate.Single => (alreadySingle, IO.never) 61 | }.flatten 62 | } yield singleCandidate 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/com/fq/sudoku/CatsEffectQueueSolver.scala: -------------------------------------------------------------------------------- 1 | package com.fq.sudoku 2 | 3 | import cats.effect.IO 4 | import cats.effect.std.Queue 5 | import cats.implicits._ 6 | import com.fq.sudoku.Solver._ 7 | 8 | object CatsEffectQueueSolver extends Solver[IO] { 9 | override def solve(givenValues: List[Value.Given]): IO[List[Value]] = 10 | for { 11 | givenCoords <- IO(givenValues.map(_.coord).toSet) 12 | missingCoords = Coord.allCoords.filterNot(givenCoords.contains) 13 | missingCells <- missingCoords.traverse(MissingCell.make) 14 | broadcast = broadcastToPeers(missingCells)(_) 15 | _ <- givenValues.parTraverse_(broadcast) 16 | missingValues <- missingCells.parTraverse(cell => cell.solve.flatTap(broadcast)) 17 | } yield givenValues ++ missingValues 18 | 19 | case class MissingCell(coord: Coord, updatesQueue: Queue[IO, Value]) { 20 | val solve: IO[Candidate.Single] = refineToSingleCandidate(Candidate.initial(coord)) 21 | 22 | private def refineToSingleCandidate(candidates: Candidate.Multiple): IO[Candidate.Single] = 23 | for { 24 | peerValue <- updatesQueue.take 25 | singleCandidate <- candidates.refine(peerValue) match { 26 | case single: Candidate.Single => IO.pure(single) 27 | case multiple: Candidate.Multiple => refineToSingleCandidate(multiple) 28 | } 29 | } yield singleCandidate 30 | } 31 | 32 | object MissingCell { 33 | def make(coord: Coord): IO[MissingCell] = 34 | Queue.unbounded[IO, Value].map(queue => MissingCell(coord, queue)) 35 | } 36 | 37 | def broadcastToPeers(cells: List[MissingCell])(update: Value): IO[Unit] = 38 | cells 39 | .filter(cell => cell.coord.isPeerOf(update.coord)) 40 | .parTraverse_(cell => cell.updatesQueue.offer(update)) 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/com/fq/sudoku/FS2StreamSolver.scala: -------------------------------------------------------------------------------- 1 | package com.fq.sudoku 2 | 3 | import cats.effect.{IO, Resource} 4 | import cats.implicits.toTraverseOps 5 | import com.fq.sudoku.Solver._ 6 | import fs2.Stream 7 | import fs2.concurrent.Topic 8 | 9 | object FS2StreamSolver extends Solver[IO] { 10 | override def solve(givens: List[Value.Given]): IO[List[Value]] = 11 | valuesStream(givens).compile.toList 12 | 13 | def valuesStream(givens: List[Value.Given]): Stream[IO, Value] = 14 | for { 15 | updatesTopic <- Stream.eval(Topic[IO, Value]) 16 | givenCoords = givens.map(_.coord).toSet 17 | missingCoords = Coord.allCoords.filterNot(givenCoords.contains) 18 | givenValuesStream = Stream.emits(givens) 19 | missingValueStreamsResource = missingCoords.traverse(missingValueStreamResource(updatesTopic)) 20 | missingValueStreams <- Stream.resource(missingValueStreamsResource) 21 | missingValuesStream = missingValueStreams.reduce(_ merge _) 22 | valuesStream = givenValuesStream ++ missingValuesStream 23 | publishedValuesStream = valuesStream.evalTap(updatesTopic.publish1) 24 | value <- publishedValuesStream 25 | } yield value 26 | 27 | def missingValueStreamResource( 28 | updatesTopic: Topic[IO, Value] 29 | )(coord: Coord): Resource[IO, Stream[IO, Candidate.Single]] = 30 | updatesTopic 31 | .subscribeAwait(81) 32 | .map { updatesStream => 33 | updatesStream 34 | .filter(_.coord.isPeerOf(coord)) 35 | .mapAccumulate[Candidate, Candidate](Candidate.initial(coord)) { 36 | case (multiple: Candidate.Multiple, peerValue) => 37 | val nextCandidate = multiple.refine(peerValue) 38 | (nextCandidate, nextCandidate) 39 | case (single: Candidate.Single, _) => (single, single) 40 | } 41 | .collectFirst { case (_, single: Candidate.Single) => single } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/com/fq/sudoku/Solver.scala: -------------------------------------------------------------------------------- 1 | package com.fq.sudoku 2 | 3 | import com.fq.sudoku.Solver.Value 4 | 5 | trait Solver[F[_]] { 6 | def solve(givens: List[Value.Given]): F[List[Value]] 7 | } 8 | 9 | object Solver { 10 | case class Coord(row: Int, col: Int) { 11 | def isPeerOf(that: Coord): Boolean = 12 | (inSameRowAs(that) || inSameColAs(that) || inSameBoxAs(that)) && notThis(that) 13 | private def notThis(that: Coord): Boolean = this != that 14 | private def inSameRowAs(that: Coord): Boolean = this.row == that.row 15 | private def inSameColAs(that: Coord): Boolean = this.col == that.col 16 | private def inSameBoxAs(that: Coord): Boolean = 17 | (this.row / 3) == (that.row / 3) && (this.col / 3) == (that.col / 3) 18 | } 19 | 20 | object Coord { 21 | val rowIndices: List[Int] = (0 to 8).toList 22 | val colIndices: List[Int] = (0 to 8).toList 23 | 24 | val allCoords: List[Coord] = for { 25 | row <- rowIndices 26 | col <- colIndices 27 | } yield Coord(row, col) 28 | } 29 | 30 | /** Value -----> Given (can be constructed by user) 31 | * \ 32 | * Candidate ----> Single (cannot be constructed, only refined to from Multiple) 33 | * \ 34 | * ----> Multiple (cannot be constructed except as initial) 35 | */ 36 | sealed trait Value { 37 | val coord: Coord 38 | val value: Int 39 | } 40 | 41 | sealed trait Candidate { 42 | val coord: Coord 43 | } 44 | 45 | object Value { 46 | case class Given(coord: Coord, value: Int) extends Value 47 | } 48 | 49 | object Candidate { 50 | class Single private[Candidate] (override val coord: Coord, override val value: Int) 51 | extends Value 52 | with Candidate 53 | 54 | class Multiple private[Candidate] (override val coord: Coord, candidates: Set[Int]) 55 | extends Candidate { 56 | def refine(peerValue: Value): Candidate = { 57 | val newValues = 58 | candidates -- Option.when(coord.isPeerOf(peerValue.coord))(peerValue.value) 59 | newValues.toList match { 60 | case Nil => throw new IllegalStateException() // unreachable 61 | case singleCandidate :: Nil => new Single(coord, singleCandidate) 62 | case multipleCandidates => new Multiple(coord, multipleCandidates.toSet) 63 | } 64 | } 65 | } 66 | 67 | def initial(coord: Coord): Multiple = new Multiple(coord, (1 to 9).toSet) 68 | } 69 | 70 | def toString(list: List[Value]): String = { 71 | val board: Map[Coord, Int] = list.map(cs => cs.coord -> cs.value).toMap 72 | 73 | val newLine = "\n" 74 | val rowSep = s"+-------+-------+-------+" 75 | 76 | Coord.rowIndices 77 | .map(row => 78 | Coord.colIndices 79 | .map(col => 80 | board.get(Coord(row, col)) match { 81 | case None => "_" 82 | case Some(value) => value 83 | } 84 | ) 85 | .grouped(3) 86 | .map(_.mkString(" ")) 87 | .toList 88 | .mkString("| ", " | ", " |") 89 | ) 90 | .grouped(3) 91 | .map(_.grouped(3).toList.map(_.mkString(newLine))) 92 | .toList 93 | .map(_.mkString + s"$newLine$rowSep") 94 | .mkString(s"$rowSep$newLine", newLine, "") 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/scala/com/fq/sudoku/SolverTest.scala: -------------------------------------------------------------------------------- 1 | package com.fq.sudoku 2 | 3 | import cats.effect.IO 4 | import com.fq.sudoku.Solver.{Candidate, Coord, Value} 5 | import munit.CatsEffectSuite 6 | 7 | class SolverTest extends CatsEffectSuite { 8 | 9 | sealed trait SudokuValue { 10 | def value: Int 11 | } 12 | 13 | object SudokuValue { 14 | case class Given(value: Int) extends SudokuValue 15 | 16 | case class Missing(value: Int) extends SudokuValue 17 | } 18 | 19 | def toSudokuValue(value: Value): (Coord, SudokuValue) = value match { 20 | case Value.Given(coord, x) => coord -> SudokuValue.Given(x) 21 | case single: Candidate.Single => single.coord -> SudokuValue.Missing(single.value) 22 | } 23 | 24 | 25 | // Easy puzzle 26 | val testCase1: Map[Coord, SudokuValue] = Map( 27 | Coord(0, 0) -> SudokuValue.Missing(2), 28 | Coord(0, 1) -> SudokuValue.Given(6), 29 | Coord(0, 2) -> SudokuValue.Missing(1), 30 | Coord(0, 3) -> SudokuValue.Given(3), 31 | Coord(0, 4) -> SudokuValue.Missing(7), 32 | Coord(0, 5) -> SudokuValue.Missing(5), 33 | Coord(0, 6) -> SudokuValue.Given(8), 34 | Coord(0, 7) -> SudokuValue.Missing(9), 35 | Coord(0, 8) -> SudokuValue.Given(4), 36 | Coord(1, 0) -> SudokuValue.Given(5), 37 | Coord(1, 1) -> SudokuValue.Given(3), 38 | Coord(1, 2) -> SudokuValue.Given(7), 39 | Coord(1, 3) -> SudokuValue.Missing(8), 40 | Coord(1, 4) -> SudokuValue.Given(9), 41 | Coord(1, 5) -> SudokuValue.Missing(4), 42 | Coord(1, 6) -> SudokuValue.Missing(1), 43 | Coord(1, 7) -> SudokuValue.Missing(6), 44 | Coord(1, 8) -> SudokuValue.Missing(2), 45 | Coord(2, 0) -> SudokuValue.Missing(9), 46 | Coord(2, 1) -> SudokuValue.Given(4), 47 | Coord(2, 2) -> SudokuValue.Missing(8), 48 | Coord(2, 3) -> SudokuValue.Missing(2), 49 | Coord(2, 4) -> SudokuValue.Missing(1), 50 | Coord(2, 5) -> SudokuValue.Given(6), 51 | Coord(2, 6) -> SudokuValue.Given(3), 52 | Coord(2, 7) -> SudokuValue.Missing(5), 53 | Coord(2, 8) -> SudokuValue.Given(7), 54 | Coord(3, 0) -> SudokuValue.Missing(6), 55 | Coord(3, 1) -> SudokuValue.Given(9), 56 | Coord(3, 2) -> SudokuValue.Missing(4), 57 | Coord(3, 3) -> SudokuValue.Missing(7), 58 | Coord(3, 4) -> SudokuValue.Given(5), 59 | Coord(3, 5) -> SudokuValue.Given(1), 60 | Coord(3, 6) -> SudokuValue.Given(2), 61 | Coord(3, 7) -> SudokuValue.Given(3), 62 | Coord(3, 8) -> SudokuValue.Given(8), 63 | Coord(4, 0) -> SudokuValue.Missing(8), 64 | Coord(4, 1) -> SudokuValue.Missing(2), 65 | Coord(4, 2) -> SudokuValue.Missing(5), 66 | Coord(4, 3) -> SudokuValue.Missing(9), 67 | Coord(4, 4) -> SudokuValue.Missing(4), 68 | Coord(4, 5) -> SudokuValue.Missing(3), 69 | Coord(4, 6) -> SudokuValue.Missing(6), 70 | Coord(4, 7) -> SudokuValue.Missing(7), 71 | Coord(4, 8) -> SudokuValue.Missing(1), 72 | Coord(5, 0) -> SudokuValue.Given(7), 73 | Coord(5, 1) -> SudokuValue.Given(1), 74 | Coord(5, 2) -> SudokuValue.Given(3), 75 | Coord(5, 3) -> SudokuValue.Given(6), 76 | Coord(5, 4) -> SudokuValue.Given(2), 77 | Coord(5, 5) -> SudokuValue.Missing(8), 78 | Coord(5, 6) -> SudokuValue.Missing(9), 79 | Coord(5, 7) -> SudokuValue.Given(4), 80 | Coord(5, 8) -> SudokuValue.Missing(5), 81 | Coord(6, 0) -> SudokuValue.Given(3), 82 | Coord(6, 1) -> SudokuValue.Missing(5), 83 | Coord(6, 2) -> SudokuValue.Given(6), 84 | Coord(6, 3) -> SudokuValue.Given(4), 85 | Coord(6, 4) -> SudokuValue.Missing(8), 86 | Coord(6, 5) -> SudokuValue.Missing(2), 87 | Coord(6, 6) -> SudokuValue.Missing(7), 88 | Coord(6, 7) -> SudokuValue.Given(1), 89 | Coord(6, 8) -> SudokuValue.Missing(9), 90 | Coord(7, 0) -> SudokuValue.Missing(4), 91 | Coord(7, 1) -> SudokuValue.Missing(8), 92 | Coord(7, 2) -> SudokuValue.Missing(9), 93 | Coord(7, 3) -> SudokuValue.Missing(1), 94 | Coord(7, 4) -> SudokuValue.Given(6), 95 | Coord(7, 5) -> SudokuValue.Missing(7), 96 | Coord(7, 6) -> SudokuValue.Given(5), 97 | Coord(7, 7) -> SudokuValue.Given(2), 98 | Coord(7, 8) -> SudokuValue.Given(3), 99 | Coord(8, 0) -> SudokuValue.Given(1), 100 | Coord(8, 1) -> SudokuValue.Missing(7), 101 | Coord(8, 2) -> SudokuValue.Given(2), 102 | Coord(8, 3) -> SudokuValue.Missing(5), 103 | Coord(8, 4) -> SudokuValue.Missing(3), 104 | Coord(8, 5) -> SudokuValue.Given(9), 105 | Coord(8, 6) -> SudokuValue.Missing(4), 106 | Coord(8, 7) -> SudokuValue.Given(8), 107 | Coord(8, 8) -> SudokuValue.Missing(6) 108 | ) 109 | 110 | // Easy puzzle: https://sandiway.arizona.edu/sudoku/examples.html 111 | val testCase2: Map[Coord, SudokuValue] = Map( 112 | Coord(0, 0) -> SudokuValue.Missing(4), 113 | Coord(0, 1) -> SudokuValue.Missing(3), 114 | Coord(0, 2) -> SudokuValue.Missing(5), 115 | Coord(0, 3) -> SudokuValue.Given(2), 116 | Coord(0, 4) -> SudokuValue.Given(6), 117 | Coord(0, 5) -> SudokuValue.Missing(9), 118 | Coord(0, 6) -> SudokuValue.Given(7), 119 | Coord(0, 7) -> SudokuValue.Missing(8), 120 | Coord(0, 8) -> SudokuValue.Given(1), 121 | Coord(1, 0) -> SudokuValue.Given(6), 122 | Coord(1, 1) -> SudokuValue.Given(8), 123 | Coord(1, 2) -> SudokuValue.Missing(2), 124 | Coord(1, 3) -> SudokuValue.Missing(5), 125 | Coord(1, 4) -> SudokuValue.Given(7), 126 | Coord(1, 5) -> SudokuValue.Missing(1), 127 | Coord(1, 6) -> SudokuValue.Missing(4), 128 | Coord(1, 7) -> SudokuValue.Given(9), 129 | Coord(1, 8) -> SudokuValue.Missing(3), 130 | Coord(2, 0) -> SudokuValue.Given(1), 131 | Coord(2, 1) -> SudokuValue.Given(9), 132 | Coord(2, 2) -> SudokuValue.Missing(7), 133 | Coord(2, 3) -> SudokuValue.Missing(8), 134 | Coord(2, 4) -> SudokuValue.Missing(3), 135 | Coord(2, 5) -> SudokuValue.Given(4), 136 | Coord(2, 6) -> SudokuValue.Given(5), 137 | Coord(2, 7) -> SudokuValue.Missing(6), 138 | Coord(2, 8) -> SudokuValue.Missing(2), 139 | Coord(3, 0) -> SudokuValue.Given(8), 140 | Coord(3, 1) -> SudokuValue.Given(2), 141 | Coord(3, 2) -> SudokuValue.Missing(6), 142 | Coord(3, 3) -> SudokuValue.Given(1), 143 | Coord(3, 4) -> SudokuValue.Missing(9), 144 | Coord(3, 5) -> SudokuValue.Missing(5), 145 | Coord(3, 6) -> SudokuValue.Missing(3), 146 | Coord(3, 7) -> SudokuValue.Given(4), 147 | Coord(3, 8) -> SudokuValue.Missing(7), 148 | Coord(4, 0) -> SudokuValue.Missing(3), 149 | Coord(4, 1) -> SudokuValue.Missing(7), 150 | Coord(4, 2) -> SudokuValue.Given(4), 151 | Coord(4, 3) -> SudokuValue.Given(6), 152 | Coord(4, 4) -> SudokuValue.Missing(8), 153 | Coord(4, 5) -> SudokuValue.Given(2), 154 | Coord(4, 6) -> SudokuValue.Given(9), 155 | Coord(4, 7) -> SudokuValue.Missing(1), 156 | Coord(4, 8) -> SudokuValue.Missing(5), 157 | Coord(5, 0) -> SudokuValue.Missing(9), 158 | Coord(5, 1) -> SudokuValue.Given(5), 159 | Coord(5, 2) -> SudokuValue.Missing(1), 160 | Coord(5, 3) -> SudokuValue.Missing(7), 161 | Coord(5, 4) -> SudokuValue.Missing(4), 162 | Coord(5, 5) -> SudokuValue.Given(3), 163 | Coord(5, 6) -> SudokuValue.Missing(6), 164 | Coord(5, 7) -> SudokuValue.Given(2), 165 | Coord(5, 8) -> SudokuValue.Given(8), 166 | Coord(6, 0) -> SudokuValue.Missing(5), 167 | Coord(6, 1) -> SudokuValue.Missing(1), 168 | Coord(6, 2) -> SudokuValue.Given(9), 169 | Coord(6, 3) -> SudokuValue.Given(3), 170 | Coord(6, 4) -> SudokuValue.Missing(2), 171 | Coord(6, 5) -> SudokuValue.Missing(6), 172 | Coord(6, 6) -> SudokuValue.Missing(8), 173 | Coord(6, 7) -> SudokuValue.Given(7), 174 | Coord(6, 8) -> SudokuValue.Given(4), 175 | Coord(7, 0) -> SudokuValue.Missing(2), 176 | Coord(7, 1) -> SudokuValue.Given(4), 177 | Coord(7, 2) -> SudokuValue.Missing(8), 178 | Coord(7, 3) -> SudokuValue.Missing(9), 179 | Coord(7, 4) -> SudokuValue.Given(5), 180 | Coord(7, 5) -> SudokuValue.Missing(7), 181 | Coord(7, 6) -> SudokuValue.Missing(1), 182 | Coord(7, 7) -> SudokuValue.Given(3), 183 | Coord(7, 8) -> SudokuValue.Given(6), 184 | Coord(8, 0) -> SudokuValue.Given(7), 185 | Coord(8, 1) -> SudokuValue.Missing(6), 186 | Coord(8, 2) -> SudokuValue.Given(3), 187 | Coord(8, 3) -> SudokuValue.Missing(4), 188 | Coord(8, 4) -> SudokuValue.Given(1), 189 | Coord(8, 5) -> SudokuValue.Given(8), 190 | Coord(8, 6) -> SudokuValue.Missing(2), 191 | Coord(8, 7) -> SudokuValue.Missing(5), 192 | Coord(8, 8) -> SudokuValue.Missing(9) 193 | ) 194 | 195 | // Easy puzzle: https://sandiway.arizona.edu/sudoku/examples.html 196 | val testCase3: Map[Coord, SudokuValue] = Map( 197 | Coord(0, 0) -> SudokuValue.Given(1), 198 | Coord(0, 1) -> SudokuValue.Missing(5), 199 | Coord(0, 2) -> SudokuValue.Missing(2), 200 | Coord(0, 3) -> SudokuValue.Given(4), 201 | Coord(0, 4) -> SudokuValue.Given(8), 202 | Coord(0, 5) -> SudokuValue.Given(9), 203 | Coord(0, 6) -> SudokuValue.Missing(3), 204 | Coord(0, 7) -> SudokuValue.Missing(7), 205 | Coord(0, 8) -> SudokuValue.Given(6), 206 | Coord(1, 0) -> SudokuValue.Given(7), 207 | Coord(1, 1) -> SudokuValue.Given(3), 208 | Coord(1, 2) -> SudokuValue.Missing(9), 209 | Coord(1, 3) -> SudokuValue.Missing(2), 210 | Coord(1, 4) -> SudokuValue.Missing(5), 211 | Coord(1, 5) -> SudokuValue.Missing(6), 212 | Coord(1, 6) -> SudokuValue.Missing(8), 213 | Coord(1, 7) -> SudokuValue.Given(4), 214 | Coord(1, 8) -> SudokuValue.Missing(1), 215 | Coord(2, 0) -> SudokuValue.Missing(4), 216 | Coord(2, 1) -> SudokuValue.Missing(6), 217 | Coord(2, 2) -> SudokuValue.Missing(8), 218 | Coord(2, 3) -> SudokuValue.Missing(3), 219 | Coord(2, 4) -> SudokuValue.Missing(7), 220 | Coord(2, 5) -> SudokuValue.Given(1), 221 | Coord(2, 6) -> SudokuValue.Given(2), 222 | Coord(2, 7) -> SudokuValue.Given(9), 223 | Coord(2, 8) -> SudokuValue.Given(5), 224 | Coord(3, 0) -> SudokuValue.Missing(3), 225 | Coord(3, 1) -> SudokuValue.Missing(8), 226 | Coord(3, 2) -> SudokuValue.Given(7), 227 | Coord(3, 3) -> SudokuValue.Given(1), 228 | Coord(3, 4) -> SudokuValue.Given(2), 229 | Coord(3, 5) -> SudokuValue.Missing(4), 230 | Coord(3, 6) -> SudokuValue.Given(6), 231 | Coord(3, 7) -> SudokuValue.Missing(5), 232 | Coord(3, 8) -> SudokuValue.Missing(9), 233 | Coord(4, 0) -> SudokuValue.Given(5), 234 | Coord(4, 1) -> SudokuValue.Missing(9), 235 | Coord(4, 2) -> SudokuValue.Missing(1), 236 | Coord(4, 3) -> SudokuValue.Given(7), 237 | Coord(4, 4) -> SudokuValue.Missing(6), 238 | Coord(4, 5) -> SudokuValue.Given(3), 239 | Coord(4, 6) -> SudokuValue.Missing(4), 240 | Coord(4, 7) -> SudokuValue.Missing(2), 241 | Coord(4, 8) -> SudokuValue.Given(8), 242 | Coord(5, 0) -> SudokuValue.Missing(2), 243 | Coord(5, 1) -> SudokuValue.Missing(4), 244 | Coord(5, 2) -> SudokuValue.Given(6), 245 | Coord(5, 3) -> SudokuValue.Missing(8), 246 | Coord(5, 4) -> SudokuValue.Given(9), 247 | Coord(5, 5) -> SudokuValue.Given(5), 248 | Coord(5, 6) -> SudokuValue.Given(7), 249 | Coord(5, 7) -> SudokuValue.Missing(1), 250 | Coord(5, 8) -> SudokuValue.Missing(3), 251 | Coord(6, 0) -> SudokuValue.Given(9), 252 | Coord(6, 1) -> SudokuValue.Given(1), 253 | Coord(6, 2) -> SudokuValue.Given(4), 254 | Coord(6, 3) -> SudokuValue.Given(6), 255 | Coord(6, 4) -> SudokuValue.Missing(3), 256 | Coord(6, 5) -> SudokuValue.Missing(7), 257 | Coord(6, 6) -> SudokuValue.Missing(5), 258 | Coord(6, 7) -> SudokuValue.Missing(8), 259 | Coord(6, 8) -> SudokuValue.Missing(2), 260 | Coord(7, 0) -> SudokuValue.Missing(6), 261 | Coord(7, 1) -> SudokuValue.Given(2), 262 | Coord(7, 2) -> SudokuValue.Missing(5), 263 | Coord(7, 3) -> SudokuValue.Missing(9), 264 | Coord(7, 4) -> SudokuValue.Missing(4), 265 | Coord(7, 5) -> SudokuValue.Missing(8), 266 | Coord(7, 6) -> SudokuValue.Missing(1), 267 | Coord(7, 7) -> SudokuValue.Given(3), 268 | Coord(7, 8) -> SudokuValue.Given(7), 269 | Coord(8, 0) -> SudokuValue.Given(8), 270 | Coord(8, 1) -> SudokuValue.Missing(7), 271 | Coord(8, 2) -> SudokuValue.Missing(3), 272 | Coord(8, 3) -> SudokuValue.Given(5), 273 | Coord(8, 4) -> SudokuValue.Given(1), 274 | Coord(8, 5) -> SudokuValue.Given(2), 275 | Coord(8, 6) -> SudokuValue.Missing(9), 276 | Coord(8, 7) -> SudokuValue.Missing(6), 277 | Coord(8, 8) -> SudokuValue.Given(4) 278 | ) 279 | 280 | // Intermediate puzzle: https://sandiway.arizona.edu/sudoku/examples.html 281 | // Can't be solved with just single candidate technique 282 | val testCase4: Map[Coord, SudokuValue] = Map( 283 | Coord(0, 0) -> SudokuValue.Missing(1), 284 | Coord(0, 1) -> SudokuValue.Given(2), 285 | Coord(0, 2) -> SudokuValue.Missing(3), 286 | Coord(0, 3) -> SudokuValue.Given(6), 287 | Coord(0, 4) -> SudokuValue.Missing(7), 288 | Coord(0, 5) -> SudokuValue.Given(8), 289 | Coord(0, 6) -> SudokuValue.Missing(9), 290 | Coord(0, 7) -> SudokuValue.Missing(4), 291 | Coord(0, 8) -> SudokuValue.Missing(5), 292 | Coord(1, 0) -> SudokuValue.Given(5), 293 | Coord(1, 1) -> SudokuValue.Given(8), 294 | Coord(1, 2) -> SudokuValue.Missing(4), 295 | Coord(1, 3) -> SudokuValue.Missing(2), 296 | Coord(1, 4) -> SudokuValue.Missing(3), 297 | Coord(1, 5) -> SudokuValue.Given(9), 298 | Coord(1, 6) -> SudokuValue.Given(7), 299 | Coord(1, 7) -> SudokuValue.Missing(6), 300 | Coord(1, 8) -> SudokuValue.Missing(1), 301 | Coord(2, 0) -> SudokuValue.Missing(9), 302 | Coord(2, 1) -> SudokuValue.Missing(6), 303 | Coord(2, 2) -> SudokuValue.Missing(7), 304 | Coord(2, 3) -> SudokuValue.Missing(1), 305 | Coord(2, 4) -> SudokuValue.Given(4), 306 | Coord(2, 5) -> SudokuValue.Missing(5), 307 | Coord(2, 6) -> SudokuValue.Missing(3), 308 | Coord(2, 7) -> SudokuValue.Missing(2), 309 | Coord(2, 8) -> SudokuValue.Missing(8), 310 | Coord(3, 0) -> SudokuValue.Given(3), 311 | Coord(3, 1) -> SudokuValue.Given(7), 312 | Coord(3, 2) -> SudokuValue.Missing(2), 313 | Coord(3, 3) -> SudokuValue.Missing(4), 314 | Coord(3, 4) -> SudokuValue.Missing(6), 315 | Coord(3, 5) -> SudokuValue.Missing(1), 316 | Coord(3, 6) -> SudokuValue.Given(5), 317 | Coord(3, 7) -> SudokuValue.Missing(8), 318 | Coord(3, 8) -> SudokuValue.Missing(9), 319 | Coord(4, 0) -> SudokuValue.Given(6), 320 | Coord(4, 1) -> SudokuValue.Missing(9), 321 | Coord(4, 2) -> SudokuValue.Missing(1), 322 | Coord(4, 3) -> SudokuValue.Missing(5), 323 | Coord(4, 4) -> SudokuValue.Missing(8), 324 | Coord(4, 5) -> SudokuValue.Missing(3), 325 | Coord(4, 6) -> SudokuValue.Missing(2), 326 | Coord(4, 7) -> SudokuValue.Missing(7), 327 | Coord(4, 8) -> SudokuValue.Given(4), 328 | Coord(5, 0) -> SudokuValue.Missing(4), 329 | Coord(5, 1) -> SudokuValue.Missing(5), 330 | Coord(5, 2) -> SudokuValue.Given(8), 331 | Coord(5, 3) -> SudokuValue.Missing(7), 332 | Coord(5, 4) -> SudokuValue.Missing(9), 333 | Coord(5, 5) -> SudokuValue.Missing(2), 334 | Coord(5, 6) -> SudokuValue.Missing(6), 335 | Coord(5, 7) -> SudokuValue.Given(1), 336 | Coord(5, 8) -> SudokuValue.Given(3), 337 | Coord(6, 0) -> SudokuValue.Missing(8), 338 | Coord(6, 1) -> SudokuValue.Missing(3), 339 | Coord(6, 2) -> SudokuValue.Missing(6), 340 | Coord(6, 3) -> SudokuValue.Missing(9), 341 | Coord(6, 4) -> SudokuValue.Given(2), 342 | Coord(6, 5) -> SudokuValue.Missing(4), 343 | Coord(6, 6) -> SudokuValue.Missing(1), 344 | Coord(6, 7) -> SudokuValue.Missing(5), 345 | Coord(6, 8) -> SudokuValue.Missing(7), 346 | Coord(7, 0) -> SudokuValue.Missing(2), 347 | Coord(7, 1) -> SudokuValue.Missing(1), 348 | Coord(7, 2) -> SudokuValue.Given(9), 349 | Coord(7, 3) -> SudokuValue.Given(8), 350 | Coord(7, 4) -> SudokuValue.Missing(5), 351 | Coord(7, 5) -> SudokuValue.Missing(7), 352 | Coord(7, 6) -> SudokuValue.Missing(4), 353 | Coord(7, 7) -> SudokuValue.Given(3), 354 | Coord(7, 8) -> SudokuValue.Given(6), 355 | Coord(8, 0) -> SudokuValue.Missing(7), 356 | Coord(8, 1) -> SudokuValue.Missing(4), 357 | Coord(8, 2) -> SudokuValue.Missing(5), 358 | Coord(8, 3) -> SudokuValue.Given(3), 359 | Coord(8, 4) -> SudokuValue.Missing(1), 360 | Coord(8, 5) -> SudokuValue.Given(6), 361 | Coord(8, 6) -> SudokuValue.Missing(8), 362 | Coord(8, 7) -> SudokuValue.Missing(9), 363 | Coord(8, 8) -> SudokuValue.Given(2) 364 | ) 365 | 366 | val testCases: Seq[Map[Coord, SudokuValue]] = Seq(testCase1, testCase2, testCase3) 367 | 368 | val solvers: List[Solver[IO]] = 369 | List( 370 | CatsEffectDeferredRefRaceSolver, 371 | CatsEffectQueueSolver, 372 | FS2StreamSolver 373 | ) 374 | 375 | solvers.foreach { solver => 376 | testCases.zipWithIndex 377 | .foreach { 378 | case (expected, idx) => 379 | test(s"${solver.getClass.getSimpleName.dropRight(1)} should solve sudoku puzzle #$idx") { 380 | val givens: List[Value.Given] = expected.collect { 381 | case (k, SudokuValue.Given(v)) => Value.Given(k, v) 382 | }.toList 383 | 384 | val result: IO[List[Solver.Value]] = solver.solve(givens) 385 | assertIO(result.map(_.map(toSudokuValue).toMap), expected) 386 | } 387 | } 388 | } 389 | 390 | } 391 | -------------------------------------------------------------------------------- /src/test/scala/com/fq/sudoku/UtilsTest.scala: -------------------------------------------------------------------------------- 1 | package com.fq.sudoku 2 | 3 | import com.fq.sudoku.Solver.Value 4 | import com.fq.sudoku.Solver.Coord._ 5 | import munit.FunSuite 6 | 7 | class UtilsTest extends FunSuite { 8 | 9 | test("boardToString") { 10 | assertEquals( 11 | Solver.toString(allCoords.map(Value.Given(_, 1))), 12 | """+-------+-------+-------+ 13 | || 1 1 1 | 1 1 1 | 1 1 1 | 14 | || 1 1 1 | 1 1 1 | 1 1 1 | 15 | || 1 1 1 | 1 1 1 | 1 1 1 | 16 | |+-------+-------+-------+ 17 | || 1 1 1 | 1 1 1 | 1 1 1 | 18 | || 1 1 1 | 1 1 1 | 1 1 1 | 19 | || 1 1 1 | 1 1 1 | 1 1 1 | 20 | |+-------+-------+-------+ 21 | || 1 1 1 | 1 1 1 | 1 1 1 | 22 | || 1 1 1 | 1 1 1 | 1 1 1 | 23 | || 1 1 1 | 1 1 1 | 1 1 1 | 24 | |+-------+-------+-------+""".stripMargin 25 | ) 26 | } 27 | 28 | test("boardToString can handle missing values") { 29 | assertEquals( 30 | Solver.toString(List.empty), 31 | """+-------+-------+-------+ 32 | || _ _ _ | _ _ _ | _ _ _ | 33 | || _ _ _ | _ _ _ | _ _ _ | 34 | || _ _ _ | _ _ _ | _ _ _ | 35 | |+-------+-------+-------+ 36 | || _ _ _ | _ _ _ | _ _ _ | 37 | || _ _ _ | _ _ _ | _ _ _ | 38 | || _ _ _ | _ _ _ | _ _ _ | 39 | |+-------+-------+-------+ 40 | || _ _ _ | _ _ _ | _ _ _ | 41 | || _ _ _ | _ _ _ | _ _ _ | 42 | || _ _ _ | _ _ _ | _ _ _ | 43 | |+-------+-------+-------+""".stripMargin 44 | ) 45 | } 46 | 47 | } 48 | --------------------------------------------------------------------------------