diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/generator.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/generator.kt index 09b2dca..ca1e21b 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/generator.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/generator.kt @@ -43,6 +43,8 @@ fun generateGame(size: Int = 6): Game { return Game(grid.toGrid(), clues) } +fun Game.solve(): PuzzleSolution = solve(grid, clues) + internal fun solve( grid: Grid, clues: Collection @@ -57,7 +59,7 @@ internal fun solve( clues.forEach { clue -> removedOptions = clue.removeForbiddenOptions(grid) || removedOptions } - } catch (e: UnsolvablePuzzleException) { + } catch (_: UnsolvablePuzzleException) { return NO_SOLUTION } // TODO: this breaks stuff and is probably unnecessary, but further tests might be needed @@ -86,7 +88,7 @@ internal fun solve( return MULTIPLE_SOLUTIONS } -internal enum class PuzzleSolution { +enum class PuzzleSolution { NO_SOLUTION, SOLVABLE, MULTIPLE_SOLUTIONS diff --git a/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameSolverTest.kt b/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameSolverTest.kt new file mode 100644 index 0000000..54b1ce3 --- /dev/null +++ b/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameSolverTest.kt @@ -0,0 +1,72 @@ +package ch.dissem.yaep.domain + +import ch.dissem.yaep.domain.PuzzleSolution.SOLVABLE +import ch.tutteli.atrium.api.fluent.en_GB.toEqual +import ch.tutteli.atrium.api.verbs.expect +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlin.io.path.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.createFile +import kotlin.io.path.isDirectory +import kotlin.io.path.notExists +import kotlin.io.path.writeText +import kotlin.test.Test +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +class GameSolverTest { + private val log = KotlinLogging.logger {} + + @Test + @OptIn(ExperimentalTime::class) + fun `find problematic clues`() { + var game: Game + var neighbours: List> + var i = 0 + do { + game = generateGame() + val triplets = game.horizontalClues.filterIsInstance>() + neighbours = game.horizontalClues.filterIsInstance>() + .filter { n -> + triplets.any { t -> + t.contains(n.a) && t.contains(n.b) + } + } + i++ + log.info { "Found ${neighbours.size} neighbours after $i tries: $neighbours" } + } while (neighbours.isEmpty()) + + val dirName = """${System.getProperty("user.home")}/.yaep""" + val dir = Path(dirName) + if (dir.notExists()) { + dir.createDirectories() + } else if (!dir.isDirectory()) { + log.error { "Yaep data directory already exists and is not a directory: $dir" } + log.info { "Game: $game" } + } else { + val fileName = "$dirName/problematic-${Clock.System.now()}.yaep" + Path(fileName) + .createFile() + .writeText(game.toString()) + log.info { "Saved game to $fileName" } + } + + } + + fun TripletClue<*, *, *>.contains(item: Item<*>): Boolean { + return this.a.itemType == item.itemType || this.b.itemType == item.itemType || this.c.itemType == item.itemType + } + + @Test + fun `ensure game can be solved`() { + val gameString = this.javaClass.classLoader.getResourceAsStream("games/001.yaep")!! + .bufferedReader() + .readText() + val game = Game.parse(gameString) + + val solution = game.solve() + + expect(solution).toEqual(SOLVABLE) + } + +} diff --git a/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/NeighbourClueTest.kt b/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/NeighbourClueTest.kt index f57a44e..d9b2838 100644 --- a/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/NeighbourClueTest.kt +++ b/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/NeighbourClueTest.kt @@ -1,5 +1,6 @@ package ch.dissem.yaep.domain +import ch.tutteli.atrium.api.fluent.en_GB.notToEqualNull import ch.tutteli.atrium.api.fluent.en_GB.toEqual import ch.tutteli.atrium.api.verbs.expect import kotlin.test.Test @@ -15,8 +16,13 @@ class NeighbourClueTest : ClueTest() { val a = grid[ia][j - 1] val b = grid[ib][j] - expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true) - expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true) + expect(a.solution).notToEqualNull() + expect(b.solution).notToEqualNull() + a.solution!! + b.solution!! + + expect(NeighbourClue(a.solution, b.solution).isValid(grid)).toEqual(true) + expect(NeighbourClue(b.solution, a.solution).isValid(grid)).toEqual(true) } } } @@ -35,8 +41,13 @@ class NeighbourClueTest : ClueTest() { val a = grid[ia][ja] val b = grid[ib][jb] - expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false) - expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false) + expect(a.solution).notToEqualNull() + expect(b.solution).notToEqualNull() + a.solution!! + b.solution!! + + expect(NeighbourClue(a.solution, b.solution).isValid(grid)).toEqual(false) + expect(NeighbourClue(b.solution, a.solution).isValid(grid)).toEqual(false) } } } @@ -54,36 +65,41 @@ class NeighbourClueTest : ClueTest() { val a = rowA[j - 1] val b = rowB[j] + expect(a.solution).notToEqualNull() + expect(b.solution).notToEqualNull() + a.solution!! + b.solution!! + rowA.forEach { it.selection = null; it.options.clear() } rowB.forEach { it.selection = null; it.options.clear() } a.selection = null - a.options.add(a.solution!!) + a.options.add(a.solution) b.selection = b.solution b.options.clear() - expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true) - expect(NeighbourClue(b.solution!!, a.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) a.selection = a.solution a.options.clear() 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(b.solution!!, a.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) if (j < size - 1) { val notA = rowA[j + 1] a.selection = null a.options.clear() - notA.options.add(a.solution!!) + notA.options.add(a.solution) b.selection = b.solution b.options.clear() - expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true) - expect(NeighbourClue(b.solution!!, a.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) } } } @@ -98,12 +114,17 @@ class NeighbourClueTest : ClueTest() { val a = rowA[1] val b = rowB[2] + expect(a.solution).notToEqualNull() + expect(b.solution).notToEqualNull() + a.solution!! + b.solution!! + a.selection = a.solution rowB[3].selection = b.solution - expect(NeighbourClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false) - expect(NeighbourClue(b.solution!!, a.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) } } diff --git a/domain/src/commonTest/resources/games/001.yaep b/domain/src/commonTest/resources/games/001.yaep new file mode 100644 index 0000000..a12194e --- /dev/null +++ b/domain/src/commonTest/resources/games/001.yaep @@ -0,0 +1,26 @@ +⬛⬛⬛⬛⬛⬛ +⬛⬛⬛⬛⬛⬛ +⬛⬛⬛⬛⬛⬛ +⬛⬛⬛🥧⬛⬛ +⬛⬛⬛⬛⬛⬛ +⬛⬛⬛⬛⬛⬛ + +* CUSTARD is between the neighbours COOKIE and ASTRONAUT to both sides +* CHOCOLATE is between the neighbours SKATEBOARD and BEVERAGE to both sides +* SWITZERLAND is between the neighbours TEACHER and TAXI to both sides +* ANT is between the neighbours ROCK_STAR and BEVERAGE to both sides +* HEALTH_WORKER is between the neighbours LOLLIPOP and NORWAY to both sides +* MILK is between the neighbours TAXI and BUS to both sides +* SNAIL is between the neighbours BEVERAGE and SKATEBOARD to both sides +* HEALTH_WORKER is between the neighbours FIREFIGHTER and ASTRONAUT to both sides +* BEER is left of GOAT +* DOG is between the neighbours COCKTAIL and COOKIE to both sides +* UKRAINE is left of GOAT +* TAXI is between the neighbours MOTOR_SCOOTER and MILK to both sides +* FIREFIGHTER is left of SWEDEN +* PIE is between the neighbours UNITED_KINGDOM and WINE to both sides +* BEVERAGE is between the neighbours TRAM_CAR and TAXI to both sides +* BEVERAGE is left of FIREFIGHTER +* ZEBRA and SOFTWARE_DEV are in the same column +* SWITZERLAND is left of BEVERAGE +* PIE is at position 3 \ No newline at end of file