Fix solver

generator and/or solver still needs optimization
This commit is contained in:
2024-07-10 22:34:53 +02:00
parent 9a38d85e84
commit 82a5660bc9
8 changed files with 99 additions and 48 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ captures
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
.kotlin

View File

@@ -54,7 +54,7 @@ fun PuzzleGrid(
grid: Grid
) {
Column(modifier = modifier) {
for (row in grid.rows) {
for (row in grid) {
Row(
modifier = Modifier
.fillMaxWidth()

View File

@@ -1,8 +1,9 @@
package ch.dissem.yaep.domain
@Suppress("UNCHECKED_CAST")
class Grid(
val rows: List<GameRow<ItemClass<*>>>
) : List<GameRow<ItemClass<*>>> by rows {
val rows: List<GameRow<*>>
) : List<GameRow<ItemClass<*>>> by rows as List<GameRow<ItemClass<*>>> {
fun <C : ItemClass<C>> indexOf(element: C): Int {
return this[element.companion]
@@ -19,7 +20,7 @@ class Grid(
}
override fun toString(): String {
return rows.joinToString("\n") { row ->
return joinToString("\n") { row ->
row.joinToString("") { it.selection?.symbol ?: " " }
}
}

View File

@@ -97,8 +97,10 @@ class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
ia - 1 -> {
return if (ic != -1) {
ic != ia - 2
} else {
} else if (ia - 2 >= 0) {
!rowC[ia - 2].mayBe(c)
} else {
true
}
}

View File

@@ -7,19 +7,17 @@ import kotlin.random.Random
fun generateGame(size: Int = 6): Game {
// Generate a random puzzle instance.
// val classes = ItemClass.randomClasses(size)
val classes = ItemClass.classes.take(size)
val classes = ItemClass.randomClasses(size)
val grid: List<List<Item<ItemClass<*>>>> = classes.map {
// it.randomItems(size).map { item -> Item(item) }
it.items.take(size).map { item -> Item(item) }
it.randomItems(size).map { item -> Item(item) }
}
// Build a set C of all possible clues that pertain to this puzzle instance.
// (There are a finite and in fact quite small number of possible clues: for example
// if there are 5 houses, there are 5 possible clues of the form "Person A lives in
// house B", 8 possible clues of the form "Person A lives next to house B", and so on.)
var clues = getAllClues(grid).sortedBy { it.toString() } //.shuffled()
var clues = getAllClues(grid).shuffled()
var i = 0
@@ -39,7 +37,7 @@ fun generateGame(size: Int = 6): Game {
// (You can speed this up by removing clues in batches rather than one at a time, but it makes the algorithm more complicated to describe.)
}
private fun solve(
internal fun solve(
grid: Grid,
clues: Collection<Clue>
): PuzzleSolution {
@@ -74,11 +72,26 @@ private fun solve(
if (grid.flatMap { it }.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.
// TODO: Does this need to be implemented? We would need to try all possible options to check if there are multiple solutions.
for (i in 0 until grid.size) {
for (j in 0 until grid.size) {
val cell = grid[i][j]
if (cell.selection == null) {
val options = cell.options.toList()
for (option in options) {
cell.selection = option
if (solve(grid, clues) == SOLVABLE) {
return SOLVABLE
}
}
cell.selection = null
cell.options.addAll(options)
}
}
}
return MULTIPLE_SOLUTIONS
}
private enum class PuzzleSolution {
internal enum class PuzzleSolution {
NO_SOLUTION,
SOLVABLE,
MULTIPLE_SOLUTIONS
@@ -112,11 +125,11 @@ fun <C : ItemClass<C>> GameRow<C>.cleanupOptions(cell: GameCell<C>) {
fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableSet<Clue> {
val clues = mutableSetOf<Clue>()
// rows.forEach { row ->
// row.forEachIndexed { i, item ->
// clues.add(PositionClue(item, i))
// }
// }
rows.forEach { row ->
row.forEachIndexed { i, item ->
clues.add(PositionClue(item, i))
}
}
rows.forEach { row ->
row.forEachIndexed { j, item ->

View File

@@ -1,6 +1,6 @@
package ch.dissem.yaep.domain
enum class Animals(symbol: String) : ItemClass<Animals> {
enum class Animal(symbol: String) : ItemClass<Animal> {
ZEBRA("🦓"),
OCTOPUS("🐙"),
GOAT("🐐"),
@@ -12,10 +12,10 @@ enum class Animals(symbol: String) : ItemClass<Animals> {
override val symbols: Array<String> = arrayOf(symbol)
override val companion
get() = Animals
get() = Animal
companion object : ItemClassCompanion<Animals> {
override val items: List<Animals> = entries
companion object : ItemClassCompanion<Animal> {
override val items: List<Animal> = entries
}
}
@@ -156,7 +156,7 @@ sealed interface ItemClass<out SELF : ItemClass<SELF>> {
companion object {
val classes: List<ItemClassCompanion<*>> = listOf(
Animals,
Animal,
Nationality,
Drink,
Profession,

View File

@@ -1,5 +1,14 @@
package ch.dissem.yaep.domain
import ch.dissem.yaep.domain.Animal.ANT
import ch.dissem.yaep.domain.Animal.OCTOPUS
import ch.dissem.yaep.domain.Animal.SNAIL
import ch.dissem.yaep.domain.Nationality.CANADA
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.FARMER
import ch.dissem.yaep.domain.Profession.SOFTWARE_DEV
import ch.tutteli.atrium.api.fluent.en_GB.feature
import ch.tutteli.atrium.api.fluent.en_GB.toBeLessThan
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
@@ -8,28 +17,6 @@ import kotlin.test.Test
class GameTest {
// @Test
// fun `try to find the error`() {
// val size = 6
// val classes = ItemClass.randomClasses(size)
//
// val grid: List<List<Item<ItemClass<*>>>> = classes.map {
// it.randomItems(size).map { item -> Item(item) }
// }
//
// // Build a set C of all possible clues that pertain to this puzzle instance.
// // (There are a finite and in fact quite small number of possible clues: for example
// // if there are 5 houses, there are 5 possible clues of the form "Person A lives in
// // house B", 8 possible clues of the form "Person A lives next to house B", and so on.)
// val clues = getAllClues(grid).shuffled()
//
// val gameGrid = grid.toGrid()
// gameGrid.flatMap { it }.forEach { it.selection = it.solution }
//
// expect(clues).toHaveElementsAndAll {
// feature { f(it::isRuleViolated, gameGrid) }.toEqual(false)
// }
// }
@Test
fun `ensure generated game is valid`() {
val game = generateGame()
@@ -42,4 +29,51 @@ class GameTest {
}
}
@Test
fun `ensure game can be solved`() {
val professions = listOf(
Item(ASTRONAUT),
Item(FARMER),
Item(SOFTWARE_DEV)
)
val animals = listOf(
Item(SNAIL),
Item(ANT),
Item(OCTOPUS)
)
val nationalities = listOf(
Item(UKRAINE),
Item(CANADA),
Item(SWITZERLAND)
)
val game = Game(
Grid(listOf(
GameRow(
Profession,
professions,
professions.map { GameCell(null, it, professions.toMutableList()) }
),
GameRow(
Animal,
animals,
animals.map { GameCell(null, it, animals.toMutableList()) }
),
GameRow(
Nationality,
nationalities,
nationalities.map { GameCell(null, it, nationalities.toMutableList()) }
)
)),
listOf(
TripletClue(professions[0], animals[1], nationalities[2]),
OrderClue(professions[0], nationalities[1]),
SameColumnClue(animals[1], nationalities[1]),
NeighbourClue(professions[0], professions[1]),
PositionClue(animals[0], 0)
)
)
expect(solve(game.grid, game.clues)).toEqual(PuzzleSolution.SOLVABLE)
}
}

View File

@@ -112,9 +112,9 @@ class TripletClueTest : ClueTest() {
@Test
fun `grid with a set and b and c as option on the same side is considered valid`() {
val grid = createGrid { null }
val rowA = grid.rows.random()
val rowB = grid.rows.random()
val rowC = grid.rows.random()
val rowA = grid.random()
val rowB = grid.random()
val rowC = grid.random()
val a = rowA[3]
val b = rowB[2]
val c = rowC[1]