diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/UnsolvablePuzzleException.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/UnsolvablePuzzleException.kt new file mode 100644 index 0000000..a176365 --- /dev/null +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/UnsolvablePuzzleException.kt @@ -0,0 +1,3 @@ +package ch.dissem.yaep.domain + +class UnsolvablePuzzleException(e: Exception) : Exception(e) diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt index 5b7ce01..1f62b6b 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt @@ -2,6 +2,11 @@ package ch.dissem.yaep.domain sealed class Clue { abstract fun isRuleViolated(grid: Grid): Boolean + + /** + * @return `true` if any option was removed + */ + abstract fun removeForbiddenOptions(grid: Grid): Boolean } sealed class HorizontalClue : Clue() @@ -42,6 +47,37 @@ class NeighbourClue, B : ItemClass>(val a: Item, val b: I return true } + override fun removeForbiddenOptions(grid: Grid): Boolean { + val rowA: GameRow = grid[aType.companion] + val rowB: GameRow = grid[bType.companion] + + var removed = removeForbiddenOptions(a, b, rowA, rowB) + removed = removeForbiddenOptions(b, a, rowB, rowA) || removed + + return removed + } + + private fun , Y : ItemClass> removeForbiddenOptions( + x: Item, + y: Item, + rowX: GameRow, + rowY: GameRow + ): Boolean { + var removed = false + + for (iX in rowX.indices) { + val cellX = rowX[iX] + if (cellX.options.contains(x)) { + if (!rowY.getOrNull(iX - 1).mayBe(y) && !rowY.getOrNull(iX + 1).mayBe(y)) { + cellX.options.remove(x) + removed = true + } + } + } + + return removed + } + override fun toString() = "$aType is next to $bType" } @@ -69,6 +105,24 @@ class OrderClue, R : ItemClass>(val left: Item, val right return rowLeft.indexOfFirst { it.mayBe(left) } >= rowRight.indexOfLast { it.mayBe(right) } } + override fun removeForbiddenOptions(grid: Grid): Boolean { + val rowL = grid[leftType.companion] + val rowR = grid[rightType.companion] + + val firstL = rowL.indexOfFirst { it.options.contains(left) } + val lastR = rowR.indexOfLast { it.options.contains(right) } + + var removed = false + try { + rowL.takeLast(rowL.size - lastR + 1) + .forEach { removed = it.options.remove(left) || removed } + rowR.take(firstL).forEach { removed = it.options.remove(right) || removed } + } catch (e: IllegalArgumentException) { + throw UnsolvablePuzzleException(e) + } + return removed + } + override fun toString() = "$leftType is left of $rightType" } @@ -161,6 +215,10 @@ class TripletClue, B : ItemClass, C : ItemClass>( return true } + override fun removeForbiddenOptions(grid: Grid): Boolean { + TODO("Not yet implemented") + } + override fun toString(): String = "$bType is between the neighbours $aType and $cType to both sides" } @@ -196,6 +254,25 @@ class SameColumnClue, B : ItemClass>(val a: Item, val b: return true } + override fun removeForbiddenOptions(grid: Grid): Boolean { + val rowA = grid[aType.companion] + val rowB = grid[bType.companion] + + var removed = false + for (i in rowA.indices) { + val cellA = rowA[i] + val cellB = rowB[i] + + if (!cellA.mayBe(a)) { + removed = cellB.options.remove(b) || removed + } + if (!cellB.mayBe(b)) { + removed = cellA.options.remove(a) || removed + } + } + return removed + } + override fun toString(): String = "$aType and $bType are in the same column" } @@ -209,5 +286,18 @@ class PositionClue>(val item: Item, val index: Int) : Clue() return grid[item].mayBe(item) } + override fun removeForbiddenOptions(grid: Grid): Boolean { + val row = grid[itemType.companion] + var removed = false + row.forEachIndexed { i, cell -> + if (i == index) { + removed = cell.options.retainAll { it == item } || removed + } else { + removed = cell.options.remove(item) || removed + } + } + return removed + } + override fun toString() = "$itemType is at position $index" } diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt index e690791..5f7ba53 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt @@ -44,23 +44,29 @@ internal fun solve( // Start with a grid where each cell is a list of all possible items. // First, set the positions of the items that are already known. - clues.filterIsInstance>>().forEach { position -> - val row = grid[position.itemType.companion] - val cell = row[position.index] - cell.options.retainAll { it == position.item } - row.cleanupOptions(cell) - } +// clues.filterIsInstance>>().forEach { position -> +// position.removeForbiddenOptions(grid) +// } +// grid.forEach { row -> row.forEach { cell -> row.cleanupOptions(cell) } } // For each clue, remove any items that violate the clue. // If any cell has only one item left, remove that item from all other cells. // Repeat until no more items can be removed. - val otherClues = clues.filter { it !is PositionClue<*> } +// val otherClues = clues.filter { it !is PositionClue<*> } var removedOptions: Boolean do { removedOptions = false - grid.forEach { row -> - removedOptions = row.tryOptionsForClues(grid, otherClues) || removedOptions + try { + clues.forEach { clue -> + removedOptions = clue.removeForbiddenOptions(grid) || removedOptions + } + } catch (e: UnsolvablePuzzleException) { + return NO_SOLUTION } + grid.forEach { row -> row.forEach { cell -> row.cleanupOptions(cell) } } +// grid.forEach { row -> +// removedOptions = row.tryOptionsForClues(grid, otherClues) || removedOptions +// } } while (removedOptions) // If any cell has no items left, the puzzle has no solution. @@ -70,15 +76,16 @@ internal fun solve( if (grid.cells.all { it.selection != null }) return SOLVABLE // If there are still cells with multiple items, pick one and try each item in turn, then go back to step 2. - return if ( - grid.cells - .filter { it.selection == null } - .any { it.countSolutions(grid, clues) == 1 } - ) { - SOLVABLE - } else { - MULTIPLE_SOLUTIONS - } +// return if ( +// grid.cells +// .filter { it.selection == null } +// .any { it.countSolutions(grid, clues) == 1 } +// ) { +// SOLVABLE +// } else { +// MULTIPLE_SOLUTIONS +// } + return MULTIPLE_SOLUTIONS } internal fun > GameCell.countSolutions(