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 { sealed class Clue {
abstract fun isRuleViolated(grid: Grid): Boolean abstract fun isRuleViolated(grid: Grid): Boolean
/**
* @return `true` if any option was removed
*/
abstract fun removeForbiddenOptions(grid: Grid): Boolean
} }
sealed class HorizontalClue : Clue() 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 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" 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) } 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" 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 return true
} }
override fun removeForbiddenOptions(grid: Grid): Boolean {
TODO("Not yet implemented")
}
override fun toString(): String = override fun toString(): String =
"$bType is between the neighbours $aType and $cType to both sides" "$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 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" 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) 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" 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. // 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. // First, set the positions of the items that are already known.
clues.filterIsInstance<PositionClue<ItemClass<*>>>().forEach { position -> // clues.filterIsInstance<PositionClue<ItemClass<*>>>().forEach { position ->
val row = grid[position.itemType.companion] // position.removeForbiddenOptions(grid)
val cell = row[position.index] // }
cell.options.retainAll { it == position.item } // grid.forEach { row -> row.forEach { cell -> row.cleanupOptions(cell) } }
row.cleanupOptions(cell)
}
// For each clue, remove any items that violate the clue. // 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. // If any cell has only one item left, remove that item from all other cells.
// Repeat until no more items can be removed. // 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 var removedOptions: Boolean
do { do {
removedOptions = false removedOptions = false
grid.forEach { row -> try {
removedOptions = row.tryOptionsForClues(grid, otherClues) || removedOptions 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) } while (removedOptions)
// If any cell has no items left, the puzzle has no solution. // 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 (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. // If there are still cells with multiple items, pick one and try each item in turn, then go back to step 2.
return if ( // return if (
grid.cells // grid.cells
.filter { it.selection == null } // .filter { it.selection == null }
.any { it.countSolutions(grid, clues) == 1 } // .any { it.countSolutions(grid, clues) == 1 }
) { // ) {
SOLVABLE // SOLVABLE
} else { // } else {
MULTIPLE_SOLUTIONS // MULTIPLE_SOLUTIONS
} // }
return MULTIPLE_SOLUTIONS
} }
internal fun <C : ItemClass<C>> GameCell<C>.countSolutions( internal fun <C : ItemClass<C>> GameCell<C>.countSolutions(