Improve and fix solver

This commit is contained in:
2024-07-30 23:11:45 +02:00
parent a019ea406d
commit 3a2865f199
10 changed files with 334 additions and 92 deletions

View File

@@ -1,8 +1,45 @@
package ch.dissem.yaep.domain package ch.dissem.yaep.domain
import ch.dissem.yaep.domain.Animal.ANT
import ch.dissem.yaep.domain.Animal.DOG
import ch.dissem.yaep.domain.Animal.GOAT
import ch.dissem.yaep.domain.Animal.SLOTH
import ch.dissem.yaep.domain.Animal.SNAIL
import ch.dissem.yaep.domain.Animal.ZEBRA
import ch.dissem.yaep.domain.Dessert.CAKE
import ch.dissem.yaep.domain.Dessert.CUPCAKE
import ch.dissem.yaep.domain.Dessert.CUSTARD
import ch.dissem.yaep.domain.Dessert.DOUGHNUT
import ch.dissem.yaep.domain.Dessert.ICE_CREAM
import ch.dissem.yaep.domain.Dessert.PIE
import ch.dissem.yaep.domain.Drink.BEER
import ch.dissem.yaep.domain.Drink.BEVERAGE
import ch.dissem.yaep.domain.Drink.COFFEE
import ch.dissem.yaep.domain.Drink.MILK
import ch.dissem.yaep.domain.Drink.TEA
import ch.dissem.yaep.domain.Drink.WINE
import ch.dissem.yaep.domain.Fruit.BANANA
import ch.dissem.yaep.domain.Fruit.GRAPES
import ch.dissem.yaep.domain.Fruit.MANGO
import ch.dissem.yaep.domain.Fruit.PEAR
import ch.dissem.yaep.domain.Fruit.PINEAPPLE
import ch.dissem.yaep.domain.Fruit.WATERMELON
import ch.dissem.yaep.domain.Nationality.CANADA
import ch.dissem.yaep.domain.Nationality.ENGLAND
import ch.dissem.yaep.domain.Nationality.JAPAN
import ch.dissem.yaep.domain.Nationality.SPAIN
import ch.dissem.yaep.domain.Nationality.SWITZERLAND
import ch.dissem.yaep.domain.Nationality.UKRAINE
import ch.dissem.yaep.domain.Profession.ASTRONAUT
import ch.dissem.yaep.domain.Profession.HEALTH_WORKER
import ch.dissem.yaep.domain.Profession.ROCK_STAR
import ch.dissem.yaep.domain.Profession.SCIENTIST
import ch.dissem.yaep.domain.Profession.SOFTWARE_DEV
import ch.dissem.yaep.domain.Profession.TEACHER
class Game( class Game(
val grid: Grid, val grid: Grid,
val clues: List<Clue> val clues: Collection<Clue>
) { ) {
val horizontalClues = clues.filterIsInstance<HorizontalClue>() val horizontalClues = clues.filterIsInstance<HorizontalClue>()
val verticalClues = clues.filterIsInstance<SameColumnClue<ItemClass<*>, ItemClass<*>>>() val verticalClues = clues.filterIsInstance<SameColumnClue<ItemClass<*>, ItemClass<*>>>()
@@ -29,9 +66,61 @@ class Game(
fun isValid(): Boolean = areCategoriesValid() && clues.all { it.isValid(grid) } fun isValid(): Boolean = areCategoriesValid() && clues.all { it.isValid(grid) }
fun isSolved(): Boolean = grid.cells.all { it.selection != null } val isSolved: Boolean
get() = grid.rows.all { it.isSolved }
override fun toString(): String { override fun toString(): String {
return grid.toString() + "\n\n" + clues.joinToString("\n* ", prefix = "* ") return grid.toString() + "\n\n" + clues.joinToString("\n* ", prefix = "* ")
} }
companion object {
fun parse(description: String): Game {
// TODO get options from string
val rowOptions = listOf<List<Item<*>>>(
listOf(
HEALTH_WORKER,
ROCK_STAR,
SCIENTIST,
SOFTWARE_DEV,
ASTRONAUT,
TEACHER
).map { Item(it) },
listOf(ANT, DOG, GOAT, SLOTH, SNAIL, ZEBRA).map { Item(it) },
listOf(WATERMELON, MANGO, PEAR, GRAPES, PINEAPPLE, BANANA).map { Item(it) },
listOf(CUPCAKE, ICE_CREAM, DOUGHNUT, CAKE, PIE, CUSTARD).map { Item(it) },
listOf(SWITZERLAND, ENGLAND, JAPAN, UKRAINE, CANADA, SPAIN).map { Item(it) },
listOf(WINE, BEVERAGE, BEER, COFFEE, TEA, MILK).map { Item(it) }
)
val optionMap = rowOptions.flatten().associateBy(
keySelector = { it.itemType },
valueTransform = { it }
)
val clues: Collection<Clue> = description.lines()
.filter { it.startsWith("* ") }
.map { line ->
Clue.parse(line) {
optionMap[it] ?: throw IllegalArgumentException("Unknown option $it")
}
}
return Game(
grid = Grid(
rows = rowOptions
.map { options -> createRow<ItemClass<*>>(options) }
.toList()
),
clues = clues
)
}
private fun <C : ItemClass<C>> createRow(options: List<Item<*>>): GameRow<*> {
@Suppress("UNCHECKED_CAST")
options as List<Item<C>>
return GameRow(
category = options.first().itemType.companion,
options = options,
cells = options.map { GameCell(options = options.toMutableList()) }
)
}
}
} }

View File

@@ -1,8 +1,8 @@
package ch.dissem.yaep.domain package ch.dissem.yaep.domain
class GameCell<C : ItemClass<C>>( class GameCell<C : ItemClass<C>>(
selection: Item<C>?, selection: Item<C>?=null,
val solution: Item<C>, val solution: Item<C>? = null,
options: Collection<Item<C>> options: Collection<Item<C>>
) { ) {
val selectionChangedListeners = mutableListOf<(Item<C>?) -> Unit>() val selectionChangedListeners = mutableListOf<(Item<C>?) -> Unit>()

View File

@@ -5,43 +5,38 @@ class GameRow<C : ItemClass<C>>(
val options: List<Item<C>>, val options: List<Item<C>>,
val cells: List<GameCell<C>> val cells: List<GameCell<C>>
) : List<GameCell<C>> by cells { ) : List<GameCell<C>> by cells {
var isSolved = false
private set
fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element } fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element }
fun cleanupOptions() { fun cleanupOptions() {
cells.forEach { if (
cleanupOptions(it) isSolved && all {
it.solution != null
&& it.options.size == 1
&& it.options.single() == it.selection
} }
// do { ) return
// var selectedSingleOption = false do {
// cells.forEach { cleanupOptions(it) } var changed = false
// val selections = cells.mapNotNull { it.selection } val selections = filter { it.selection != null }.let { cellsWithSelection ->
// options cellsWithSelection
// .filter { !selections.contains(it) } .filter { it.options.size != 1 }
// .forEach { option -> .forEach {
// if (cells.count { cell -> cell.options.contains(option) } == 1) { it.options.clear()
// cells it.options.add(it.selection!!)
// .filter { it.selection == null }
// .first { cell -> cell.options.contains(option) }
// .let { it.selection = option }
// selectedSingleOption = true
// }
// }
// } while (selectedSingleOption)
} }
cellsWithSelection.mapNotNull { it.selection }.toMutableSet()
private fun cleanupOptions(cell: GameCell<C>, justSelected: Boolean = true) {
if ((justSelected && cell.selection != null) || (cell.options.size == 1 && cell.selection == null)) {
val selection = cell.selection
if (selection == null) {
cell.selection = cell.options.first()
} else {
cell.options.clear()
cell.options.add(selection)
} }
filter { otherCell -> otherCell != cell && otherCell.hasNoSelection() }.forEach { otherCell -> filter { it.selection == null }
otherCell.options.remove(cell.selection) .forEach { it.options.removeAll(selections) }
cleanupOptions(otherCell, false) filter { it.selection == null && it.options.size == 1 }
.forEach {
changed = true
it.selection = it.options.single()
} }
if (!changed) {
filter { it.selection == null } filter { it.selection == null }
.flatMap { c -> c.options.map { o -> o to c } } .flatMap { c -> c.options.map { o -> o to c } }
.groupBy { it.first } .groupBy { it.first }
@@ -49,9 +44,15 @@ class GameRow<C : ItemClass<C>>(
.forEach { .forEach {
val c = it.value.single().second val c = it.value.single().second
c.selection = it.key c.selection = it.key
cleanupOptions(c, true) changed = true
} }
} }
} while (changed)
isSolved = all {
it.solution != null
&& it.options.size == 1
&& it.options.single() == it.selection
}
} }
} }

View File

@@ -7,6 +7,26 @@ sealed class Clue {
* @return `true` if any option was removed * @return `true` if any option was removed
*/ */
abstract fun removeForbiddenOptions(grid: Grid): Boolean abstract fun removeForbiddenOptions(grid: Grid): Boolean
companion object {
fun <T : ItemClass<T>> parse(line: String, mapper: (ItemClass<*>) -> Item<T>): Clue {
return NeighbourClue.parse(line, mapper)
?: OrderClue.parse(line, mapper)
?: TripletClue.parse(line, mapper)
?: SameColumnClue.parse(line, mapper)
?: PositionClue.parse(line, mapper)
?: throw IllegalStateException("Unknown clue: $line")
}
}
}
sealed interface ClueParser {
/**
* Parses a string description of a clue into a clue.
*
* Returns `null` if `line` isn't a representation of that clue.
*/
fun <T : ItemClass<T>> parse(line: String, mapper: (ItemClass<*>) -> Item<T>): Clue?
} }
sealed class HorizontalClue : Clue() sealed class HorizontalClue : Clue()
@@ -66,6 +86,21 @@ class NeighbourClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: I
} }
override fun toString() = "$aType is next to $bType" override fun toString() = "$aType is next to $bType"
companion object : ClueParser {
override fun <T : ItemClass<T>> parse(
line: String,
mapper: (ItemClass<*>) -> Item<T>
): Clue? {
val regex = Regex("^(\\* )?(?<a>[A-Z_]+) is next to (?<b>[A-Z_]+)$")
val matchResult = regex.matchEntire(line) ?: return null
val a = ItemClass.parse(matchResult.groups["a"]!!.value)
val b = ItemClass.parse(matchResult.groups["b"]!!.value)
return NeighbourClue(mapper(a), mapper(b))
}
}
} }
class OrderClue<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, val right: Item<R>) : class OrderClue<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, val right: Item<R>) :
@@ -113,6 +148,21 @@ class OrderClue<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, val right
} }
override fun toString() = "$leftType is left of $rightType" override fun toString() = "$leftType is left of $rightType"
companion object : ClueParser {
override fun <T : ItemClass<T>> parse(
line: String,
mapper: (ItemClass<*>) -> Item<T>
): Clue? {
val regex = Regex("^(\\* )?(?<left>[A-Z_]+) is left of (?<right>[A-Z_]+)$")
val matchResult = regex.matchEntire(line) ?: return null
val left = ItemClass.parse(matchResult.groups["left"]!!.value)
val right = ItemClass.parse(matchResult.groups["right"]!!.value)
return OrderClue(mapper(left), mapper(right))
}
}
} }
class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>( class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
@@ -242,6 +292,23 @@ class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
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"
companion object : ClueParser {
override fun <T : ItemClass<T>> parse(
line: String,
mapper: (ItemClass<*>) -> Item<T>
): Clue? {
val regex =
Regex("^(\\* )?(?<b>[A-Z_]+) is between the neighbours (?<a>[A-Z_]+) and (?<c>[A-Z_]+) to both sides$")
val matchResult = regex.matchEntire(line) ?: return null
val a = ItemClass.parse(matchResult.groups["a"]!!.value)
val b = ItemClass.parse(matchResult.groups["b"]!!.value)
val c = ItemClass.parse(matchResult.groups["c"]!!.value)
return TripletClue(mapper(a), mapper(b), mapper(c))
}
}
} }
class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) : Clue() { class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) : Clue() {
@@ -291,6 +358,22 @@ class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b:
} }
override fun toString(): String = "$aType and $bType are in the same column" override fun toString(): String = "$aType and $bType are in the same column"
companion object : ClueParser {
override fun <T : ItemClass<T>> parse(
line: String,
mapper: (ItemClass<*>) -> Item<T>
): Clue? {
val regex = Regex("^(\\* )?(?<a>[A-Z_]+) and (?<b>[A-Z_]+) are in the same column$")
val matchResult = regex.matchEntire(line) ?: return null
val a = ItemClass.parse(matchResult.groups["a"]!!.value)
val b = ItemClass.parse(matchResult.groups["b"]!!.value)
return SameColumnClue(mapper(a), mapper(b))
}
}
} }
class PositionClue<C : ItemClass<C>>(val item: Item<C>, val index: Int) : Clue() { class PositionClue<C : ItemClass<C>>(val item: Item<C>, val index: Int) : Clue() {
@@ -307,14 +390,31 @@ class PositionClue<C : ItemClass<C>>(val item: Item<C>, val index: Int) : Clue()
val row = grid[itemType.companion] val row = grid[itemType.companion]
var removed = false var removed = false
row.forEachIndexed { i, cell -> row.forEachIndexed { i, cell ->
if (i == index) { if (cell.hasNoSelection()) {
removed = cell.options.retainAll { it == item } || removed removed = if (i == index) {
cell.options.retainAll { it == item } || removed
} else { } else {
removed = cell.options.remove(item) || removed cell.options.remove(item) || removed
}
} }
} }
return removed return removed
} }
override fun toString() = "$itemType is at position $index" override fun toString() = "$itemType is at position $index"
companion object : ClueParser {
override fun <T : ItemClass<T>> parse(
line: String,
mapper: (ItemClass<*>) -> Item<T>
): Clue? {
val regex = Regex("^(\\* )?(?<type>[A-Z_]+) is at position (?<pos>\\d)$")
val matchResult = regex.matchEntire(line) ?: return null
val type = ItemClass.parse(matchResult.groups["type"]!!.value)
val pos = matchResult.groups["pos"]!!.value.toInt()
return PositionClue(mapper(type), pos)
}
}
} }

View File

@@ -152,6 +152,8 @@ private fun idic(symbol: String): Array<String> = Array(GENDERS.size * SKIN_TONE
sealed interface ItemClass<out SELF : ItemClass<SELF>> { sealed interface ItemClass<out SELF : ItemClass<SELF>> {
val symbols: Array<String> val symbols: Array<String>
val name: String
val companion: ItemClassCompanion<SELF> val companion: ItemClassCompanion<SELF>
companion object { companion object {
@@ -168,6 +170,10 @@ sealed interface ItemClass<out SELF : ItemClass<SELF>> {
fun randomClasses(n: Int): List<ItemClassCompanion<*>> { fun randomClasses(n: Int): List<ItemClassCompanion<*>> {
return classes.shuffled().take(n) return classes.shuffled().take(n)
} }
fun parse(name: String): ItemClass<*> {
return classes.mapNotNull { it.parse(name) }.single()
}
} }
} }
@@ -177,4 +183,8 @@ sealed interface ItemClassCompanion<out C : ItemClass<C>> {
fun randomItems(n: Int): List<C> { fun randomItems(n: Int): List<C> {
return items.shuffled().take(n) return items.shuffled().take(n)
} }
fun parse(name: String): C? {
return items.firstOrNull { it.name == name }
}
} }

View File

@@ -30,7 +30,11 @@ class GameTest {
game = generateGame() game = generateGame()
} }
println("Generated game #$i in ${time.inWholeMilliseconds}ms") println("Generated game #$i in ${time.inWholeMilliseconds}ms")
expect(solve(game.grid, game.clues)).toEqual(SOLVABLE) val solvable = solve(game.grid, game.clues)
if (solvable != SOLVABLE) {
println("Puzzle:\n$game")
}
expect(solvable).toEqual(SOLVABLE)
expect(time).toBeLessThan(500.milliseconds) expect(time).toBeLessThan(500.milliseconds)
if (time < fastest) { if (time < fastest) {
fastest = time fastest = time
@@ -67,8 +71,8 @@ class GameTest {
feature(Game::areCategoriesValid).toEqual(true) feature(Game::areCategoriesValid).toEqual(true)
feature(Game::isValid).toEqual(true) feature(Game::isValid).toEqual(true)
feature(Game::clues) { feature(Game::clues) {
feature(List<Clue>::size).toBeGreaterThan(5) feature(Collection<Clue>::size).toBeGreaterThan(5)
feature(List<Clue>::size).toBeLessThan(30) feature(Collection<Clue>::size).toBeLessThan(30)
} }
} }
println("Clues: ${game.clues.size}") println("Clues: ${game.clues.size}")
@@ -124,4 +128,42 @@ class GameTest {
expect(solve(game.grid, game.clues)).toEqual(SOLVABLE) expect(solve(game.grid, game.clues)).toEqual(SOLVABLE)
} }
@Test
fun `ensure specific game is solvable`() {
val game = Game.parse("""
👩🏿‍⚕️👨🏽‍🎤👩🏿‍⚕️ 👩🏾‍🚀🧑🏿‍🏫
🐜🐕 🐐 🐐
🍉🥭🍐🍇🍍
🧁🍨🍩🍰🥧
🇨🇭🇬🇧🇯🇵🇺🇦🇬🇧🇨🇦
🍷🧃🍺🧃
* ZEBRA is between the neighbours PIE and PEAR to both sides
* WINE is at position 0
* SLOTH is between the neighbours ZEBRA and COFFEE to both sides
* ICE_CREAM is left of MANGO
* SWITZERLAND is at position 0
* PIE is at position 4
* SCIENTIST is between the neighbours ASTRONAUT and PEAR to both sides
* ROCK_STAR is between the neighbours SNAIL and ANT to both sides
* SNAIL is between the neighbours ROCK_STAR and TEA to both sides
* SOFTWARE_DEV is left of SLOTH
* SOFTWARE_DEV is left of HEALTH_WORKER
* MILK is between the neighbours CUSTARD and ZEBRA to both sides
* SLOTH is between the neighbours CUSTARD and CAKE to both sides
* SPAIN is between the neighbours CUSTARD and GRAPES to both sides
* SCIENTIST is between the neighbours SNAIL and SLOTH to both sides
* DOG is between the neighbours CUPCAKE and BEER to both sides
* SNAIL is between the neighbours BANANA and GRAPES to both sides
* SLOTH is between the neighbours GRAPES and CANADA to both sides
* UKRAINE and SCIENTIST are in the same column
* DOG is between the neighbours JAPAN and SWITZERLAND to both sides
* SLOTH is between the neighbours GOAT and TEA to both sides
* ROCK_STAR and ENGLAND are in the same column
* ROCK_STAR is next to DOUGHNUT
* PINEAPPLE is between the neighbours TEACHER and GRAPES to both sides
""".trimIndent())
expect(solve(game.grid, game.clues)).toEqual(SOLVABLE)
}
} }

View File

@@ -15,8 +15,8 @@ class NeighbourClueTest : ClueTest() {
val a = grid[ia][j - 1] val a = grid[ia][j - 1]
val b = grid[ib][j] val b = grid[ib][j]
expect(NeighbourClue(a.solution, b.solution).isValid(grid)).toEqual(true) expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true)
expect(NeighbourClue(b.solution, a.solution).isValid(grid)).toEqual(true) expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
} }
} }
} }
@@ -35,8 +35,8 @@ class NeighbourClueTest : ClueTest() {
val a = grid[ia][ja] val a = grid[ia][ja]
val b = grid[ib][jb] val b = grid[ib][jb]
expect(NeighbourClue(a.solution, b.solution).isValid(grid)).toEqual(false) expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false)
expect(NeighbourClue(b.solution, a.solution).isValid(grid)).toEqual(false) expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
} }
} }
} }
@@ -58,32 +58,32 @@ class NeighbourClueTest : ClueTest() {
rowB.forEach { it.selection = null; it.options.clear() } rowB.forEach { it.selection = null; it.options.clear() }
a.selection = null a.selection = null
a.options.add(a.solution) a.options.add(a.solution!!)
b.selection = b.solution b.selection = b.solution
b.options.clear() b.options.clear()
expect(NeighbourClue(a.solution, b.solution).isValid(grid)).toEqual(true) expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true)
expect(NeighbourClue(b.solution, a.solution).isValid(grid)).toEqual(true) expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
a.selection = a.solution a.selection = a.solution
a.options.clear() a.options.clear()
b.selection = null b.selection = null
b.options.add(b.solution) b.options.add(b.solution!!)
expect(NeighbourClue(a.solution, b.solution).isValid(grid)).toEqual(true) expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true)
expect(NeighbourClue(b.solution, a.solution).isValid(grid)).toEqual(true) expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
if (j < size - 1) { if (j < size - 1) {
val notA = rowA[j + 1] val notA = rowA[j + 1]
a.selection = null a.selection = null
a.options.clear() a.options.clear()
notA.options.add(a.solution) notA.options.add(a.solution!!)
b.selection = b.solution b.selection = b.solution
b.options.clear() b.options.clear()
expect(NeighbourClue(a.solution, b.solution).isValid(grid)).toEqual(true) expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true)
expect(NeighbourClue(b.solution, a.solution).isValid(grid)).toEqual(true) expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
} }
} }
} }
@@ -102,8 +102,8 @@ class NeighbourClueTest : ClueTest() {
rowB[3].selection = b.solution rowB[3].selection = b.solution
expect(NeighbourClue(a.solution, b.solution).isValid(grid)).toEqual(false) expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false)
expect(NeighbourClue(b.solution, a.solution).isValid(grid)).toEqual(false) expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
} }
} }

View File

@@ -16,7 +16,7 @@ class OrderClueTest : ClueTest() {
val a = grid[ia][ja] val a = grid[ia][ja]
val b = grid[ib][jb] val b = grid[ib][jb]
expect(OrderClue(a.solution, b.solution).isValid(grid)).toEqual(true) expect(OrderClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true)
} }
} }
} }
@@ -35,7 +35,7 @@ class OrderClueTest : ClueTest() {
val a = grid[ia][ja] val a = grid[ia][ja]
val b = grid[ib][jb] val b = grid[ib][jb]
expect(OrderClue(b.solution, a.solution).isValid(grid)).toEqual(false) expect(OrderClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
} }
} }
} }
@@ -49,7 +49,7 @@ class OrderClueTest : ClueTest() {
for (rowA in grid.rows) { for (rowA in grid.rows) {
for (rowB in grid.rows) { for (rowB in grid.rows) {
for (i in 0 until size) { for (i in 0 until size) {
expect(OrderClue(rowA[i].solution, rowB[i].solution).isValid(grid)).toEqual(false) expect(OrderClue(rowA[i].solution!!, rowB[i].solution!!).isValid(grid)).toEqual(false)
} }
} }
} }

View File

@@ -14,8 +14,8 @@ class SameColumnClueTest : ClueTest() {
val a = grid[ia][j] val a = grid[ia][j]
val b = grid[ib][j] val b = grid[ib][j]
expect(SameColumnClue(a.solution, b.solution).isValid(grid)).toEqual(true) expect(SameColumnClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true)
expect(SameColumnClue(b.solution, a.solution).isValid(grid)).toEqual(true) expect(SameColumnClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
} }
} }
} }
@@ -32,8 +32,8 @@ class SameColumnClueTest : ClueTest() {
val a = grid[ia][ja] val a = grid[ia][ja]
val b = grid[ib][jb] val b = grid[ib][jb]
expect(SameColumnClue(a.solution, b.solution).isValid(grid)).toEqual(false) expect(SameColumnClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false)
expect(SameColumnClue(b.solution, a.solution).isValid(grid)).toEqual(false) expect(SameColumnClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
} }
} }
} }
@@ -54,8 +54,8 @@ class SameColumnClueTest : ClueTest() {
a.selection = a.solution a.selection = a.solution
b.selection = b.solution b.selection = b.solution
expect(SameColumnClue(a.solution, b.solution).isValid(grid)).toEqual(false) expect(SameColumnClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false)
expect(SameColumnClue(b.solution, a.solution).isValid(grid)).toEqual(false) expect(SameColumnClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
} }
} }
} }
@@ -87,8 +87,8 @@ class SameColumnClueTest : ClueTest() {
} }
} }
expect(SameColumnClue(a.solution, b.solution).isValid(grid)).toEqual(false) expect(SameColumnClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false)
expect(SameColumnClue(b.solution, a.solution).isValid(grid)).toEqual(false) expect(SameColumnClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
} }
} }
} }

View File

@@ -17,8 +17,8 @@ class TripletClueTest : ClueTest() {
val b = grid[ib][j - 1] val b = grid[ib][j - 1]
val c = grid[ic][j] val c = grid[ic][j]
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(true) expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(true)
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(true) expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
} }
} }
} }
@@ -38,14 +38,14 @@ class TripletClueTest : ClueTest() {
rowB[0].selection = b.solution rowB[0].selection = b.solution
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(false) expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(false)
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(false) expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
rowB[0].selection = null rowB[0].selection = null
rowB[grid.size - 1].selection = b.solution rowB[grid.size - 1].selection = b.solution
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(false) expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(false)
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(false) expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
} }
@Test @Test
@@ -61,10 +61,10 @@ class TripletClueTest : ClueTest() {
val rowC = grid[1] val rowC = grid[1]
val c = rowC[ic] val c = rowC[ic]
val clue = TripletClue(a.solution, b.solution, c.solution) val clue = TripletClue(a.solution!!, b.solution!!, c.solution!!)
b.selection = b.solution b.selection = b.solution
c.options.add(c.solution) c.options.add(c.solution!!)
rowA.forEachIndexed { index, notA -> rowA.forEachIndexed { index, notA ->
@@ -75,7 +75,7 @@ class TripletClueTest : ClueTest() {
} }
index == ic -> { index == ic -> {
rowC[ia].options.add(c.solution) rowC[ia].options.add(c.solution!!)
expect(clue.isValid(grid)).toEqual(true) expect(clue.isValid(grid)).toEqual(true)
} }
@@ -97,8 +97,8 @@ class TripletClueTest : ClueTest() {
a.selection = a.solution a.selection = a.solution
grid[1][4].selection = c.solution grid[1][4].selection = c.solution
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(false) expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(false)
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(false) expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
} }
@Test @Test
@@ -115,11 +115,11 @@ class TripletClueTest : ClueTest() {
b.options.clear() b.options.clear()
c.options.clear() c.options.clear()
rowB[4].options.add(b.solution) rowB[4].options.add(b.solution!!)
rowC[5].options.add(c.solution) rowC[5].options.add(c.solution!!)
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(true) expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(true)
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(true) expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
} }
@Test @Test
@@ -136,7 +136,7 @@ class TripletClueTest : ClueTest() {
rowB[4].selection = rowC[3].solution rowB[4].selection = rowC[3].solution
c.selection = rowC[3].solution c.selection = rowC[3].solution
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(false) expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(false)
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(false) expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
} }
} }