Add Clue.removeForbiddenOptions (WIP)

This commit is contained in:
2024-07-14 14:48:01 +02:00
parent ca22fb9f2f
commit 0ece3a770e
3 changed files with 118 additions and 18 deletions

View File

@@ -0,0 +1,3 @@
package ch.dissem.yaep.domain
class UnsolvablePuzzleException(e: Exception) : Exception(e)

View File

@@ -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<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: I
return true
}
override fun removeForbiddenOptions(grid: Grid): Boolean {
val rowA: GameRow<A> = grid[aType.companion]
val rowB: GameRow<B> = grid[bType.companion]
var removed = removeForbiddenOptions(a, b, rowA, rowB)
removed = removeForbiddenOptions(b, a, rowB, rowA) || removed
return removed
}
private fun <X : ItemClass<X>, Y : ItemClass<Y>> removeForbiddenOptions(
x: Item<X>,
y: Item<Y>,
rowX: GameRow<X>,
rowY: GameRow<Y>
): 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<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, 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<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
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<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, 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<C : ItemClass<C>>(val item: Item<C>, 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"
}

View File

@@ -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<PositionClue<ItemClass<*>>>().forEach { position ->
val row = grid[position.itemType.companion]
val cell = row[position.index]
cell.options.retainAll { it == position.item }
row.cleanupOptions(cell)
}
// clues.filterIsInstance<PositionClue<ItemClass<*>>>().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 <C : ItemClass<C>> GameCell<C>.countSolutions(