From 82a5660bc901c6f5f2d8db89104c580cea76d6a1 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Wed, 10 Jul 2024 22:34:53 +0200 Subject: [PATCH] Fix solver generator and/or solver still needs optimization --- .gitignore | 1 + .../kotlin/ch/dissem/yaep/ui/common/App.kt | 2 +- .../main/kotlin/ch/dissem/yaep/domain/Grid.kt | 7 +- .../kotlin/ch/dissem/yaep/domain/clues.kt | 4 +- .../kotlin/ch/dissem/yaep/domain/generator.kt | 39 ++++++---- .../kotlin/ch/dissem/yaep/domain/items.kt | 10 +-- .../kotlin/ch/dissem/yaep/domain/GameTest.kt | 78 +++++++++++++------ .../ch/dissem/yaep/domain/TripletClueTest.kt | 6 +- 8 files changed, 99 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index 33e4c75..63c3ef0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ captures !*.xcodeproj/project.xcworkspace/ !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings +.kotlin diff --git a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt index b5c1cfc..d329400 100644 --- a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt +++ b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt @@ -54,7 +54,7 @@ fun PuzzleGrid( grid: Grid ) { Column(modifier = modifier) { - for (row in grid.rows) { + for (row in grid) { Row( modifier = Modifier .fillMaxWidth() diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/Grid.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/Grid.kt index 895b6a8..04f7510 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/Grid.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/Grid.kt @@ -1,8 +1,9 @@ package ch.dissem.yaep.domain +@Suppress("UNCHECKED_CAST") class Grid( - val rows: List>> -) : List>> by rows { + val rows: List> +) : List>> by rows as List>> { fun > 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 ?: " " } } } 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 c807699..35d8202 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt @@ -97,8 +97,10 @@ class TripletClue, B : ItemClass, C : ItemClass>( ia - 1 -> { return if (ic != -1) { ic != ia - 2 - } else { + } else if (ia - 2 >= 0) { !rowC[ia - 2].mayBe(c) + } else { + true } } 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 05a7c86..ba7246d 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/generator.kt @@ -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>>> = 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 ): 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 > GameRow.cleanupOptions(cell: GameCell) { fun getAllClues(rows: List>>>): MutableSet { val clues = mutableSetOf() -// 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 -> diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/items.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/items.kt index 3706953..ce78263 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/items.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/items.kt @@ -1,6 +1,6 @@ package ch.dissem.yaep.domain -enum class Animals(symbol: String) : ItemClass { +enum class Animal(symbol: String) : ItemClass { ZEBRA("🦓"), OCTOPUS("🐙"), GOAT("🐐"), @@ -12,10 +12,10 @@ enum class Animals(symbol: String) : ItemClass { override val symbols: Array = arrayOf(symbol) override val companion - get() = Animals + get() = Animal - companion object : ItemClassCompanion { - override val items: List = entries + companion object : ItemClassCompanion { + override val items: List = entries } } @@ -156,7 +156,7 @@ sealed interface ItemClass> { companion object { val classes: List> = listOf( - Animals, + Animal, Nationality, Drink, Profession, diff --git a/domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt b/domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt index 8795a85..fd4840d 100644 --- a/domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt +++ b/domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt @@ -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>>> = 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) + } + } \ No newline at end of file diff --git a/domain/src/test/kotlin/ch/dissem/yaep/domain/TripletClueTest.kt b/domain/src/test/kotlin/ch/dissem/yaep/domain/TripletClueTest.kt index c0e2fe6..bbb0970 100644 --- a/domain/src/test/kotlin/ch/dissem/yaep/domain/TripletClueTest.kt +++ b/domain/src/test/kotlin/ch/dissem/yaep/domain/TripletClueTest.kt @@ -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]