Create test that solves saved games

and finds problematic ones
This commit is contained in:
Christian Basler
2025-05-17 06:51:21 +02:00
parent d146ae11f7
commit 6d9e50fb88
4 changed files with 138 additions and 17 deletions

View File

@@ -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<Clue>
@@ -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

View File

@@ -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<NeighbourClue<*, *>>
var i = 0
do {
game = generateGame()
val triplets = game.horizontalClues.filterIsInstance<TripletClue<*, *, *>>()
neighbours = game.horizontalClues.filterIsInstance<NeighbourClue<*, *>>()
.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)
}
}

View File

@@ -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)
}
}

View File

@@ -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