Create test that solves saved games
and finds problematic ones
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
26
domain/src/commonTest/resources/games/001.yaep
Normal file
26
domain/src/commonTest/resources/games/001.yaep
Normal 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
|
||||
Reference in New Issue
Block a user