Improve and fix solver
This commit is contained in:
@@ -1,8 +1,45 @@
|
|||||||
package ch.dissem.yaep.domain
|
package ch.dissem.yaep.domain
|
||||||
|
|
||||||
|
import ch.dissem.yaep.domain.Animal.ANT
|
||||||
|
import ch.dissem.yaep.domain.Animal.DOG
|
||||||
|
import ch.dissem.yaep.domain.Animal.GOAT
|
||||||
|
import ch.dissem.yaep.domain.Animal.SLOTH
|
||||||
|
import ch.dissem.yaep.domain.Animal.SNAIL
|
||||||
|
import ch.dissem.yaep.domain.Animal.ZEBRA
|
||||||
|
import ch.dissem.yaep.domain.Dessert.CAKE
|
||||||
|
import ch.dissem.yaep.domain.Dessert.CUPCAKE
|
||||||
|
import ch.dissem.yaep.domain.Dessert.CUSTARD
|
||||||
|
import ch.dissem.yaep.domain.Dessert.DOUGHNUT
|
||||||
|
import ch.dissem.yaep.domain.Dessert.ICE_CREAM
|
||||||
|
import ch.dissem.yaep.domain.Dessert.PIE
|
||||||
|
import ch.dissem.yaep.domain.Drink.BEER
|
||||||
|
import ch.dissem.yaep.domain.Drink.BEVERAGE
|
||||||
|
import ch.dissem.yaep.domain.Drink.COFFEE
|
||||||
|
import ch.dissem.yaep.domain.Drink.MILK
|
||||||
|
import ch.dissem.yaep.domain.Drink.TEA
|
||||||
|
import ch.dissem.yaep.domain.Drink.WINE
|
||||||
|
import ch.dissem.yaep.domain.Fruit.BANANA
|
||||||
|
import ch.dissem.yaep.domain.Fruit.GRAPES
|
||||||
|
import ch.dissem.yaep.domain.Fruit.MANGO
|
||||||
|
import ch.dissem.yaep.domain.Fruit.PEAR
|
||||||
|
import ch.dissem.yaep.domain.Fruit.PINEAPPLE
|
||||||
|
import ch.dissem.yaep.domain.Fruit.WATERMELON
|
||||||
|
import ch.dissem.yaep.domain.Nationality.CANADA
|
||||||
|
import ch.dissem.yaep.domain.Nationality.ENGLAND
|
||||||
|
import ch.dissem.yaep.domain.Nationality.JAPAN
|
||||||
|
import ch.dissem.yaep.domain.Nationality.SPAIN
|
||||||
|
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.HEALTH_WORKER
|
||||||
|
import ch.dissem.yaep.domain.Profession.ROCK_STAR
|
||||||
|
import ch.dissem.yaep.domain.Profession.SCIENTIST
|
||||||
|
import ch.dissem.yaep.domain.Profession.SOFTWARE_DEV
|
||||||
|
import ch.dissem.yaep.domain.Profession.TEACHER
|
||||||
|
|
||||||
class Game(
|
class Game(
|
||||||
val grid: Grid,
|
val grid: Grid,
|
||||||
val clues: List<Clue>
|
val clues: Collection<Clue>
|
||||||
) {
|
) {
|
||||||
val horizontalClues = clues.filterIsInstance<HorizontalClue>()
|
val horizontalClues = clues.filterIsInstance<HorizontalClue>()
|
||||||
val verticalClues = clues.filterIsInstance<SameColumnClue<ItemClass<*>, ItemClass<*>>>()
|
val verticalClues = clues.filterIsInstance<SameColumnClue<ItemClass<*>, ItemClass<*>>>()
|
||||||
@@ -29,9 +66,61 @@ class Game(
|
|||||||
|
|
||||||
fun isValid(): Boolean = areCategoriesValid() && clues.all { it.isValid(grid) }
|
fun isValid(): Boolean = areCategoriesValid() && clues.all { it.isValid(grid) }
|
||||||
|
|
||||||
fun isSolved(): Boolean = grid.cells.all { it.selection != null }
|
val isSolved: Boolean
|
||||||
|
get() = grid.rows.all { it.isSolved }
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return grid.toString() + "\n\n" + clues.joinToString("\n* ", prefix = "* ")
|
return grid.toString() + "\n\n" + clues.joinToString("\n* ", prefix = "* ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(description: String): Game {
|
||||||
|
// TODO get options from string
|
||||||
|
val rowOptions = listOf<List<Item<*>>>(
|
||||||
|
listOf(
|
||||||
|
HEALTH_WORKER,
|
||||||
|
ROCK_STAR,
|
||||||
|
SCIENTIST,
|
||||||
|
SOFTWARE_DEV,
|
||||||
|
ASTRONAUT,
|
||||||
|
TEACHER
|
||||||
|
).map { Item(it) },
|
||||||
|
listOf(ANT, DOG, GOAT, SLOTH, SNAIL, ZEBRA).map { Item(it) },
|
||||||
|
listOf(WATERMELON, MANGO, PEAR, GRAPES, PINEAPPLE, BANANA).map { Item(it) },
|
||||||
|
listOf(CUPCAKE, ICE_CREAM, DOUGHNUT, CAKE, PIE, CUSTARD).map { Item(it) },
|
||||||
|
listOf(SWITZERLAND, ENGLAND, JAPAN, UKRAINE, CANADA, SPAIN).map { Item(it) },
|
||||||
|
listOf(WINE, BEVERAGE, BEER, COFFEE, TEA, MILK).map { Item(it) }
|
||||||
|
)
|
||||||
|
val optionMap = rowOptions.flatten().associateBy(
|
||||||
|
keySelector = { it.itemType },
|
||||||
|
valueTransform = { it }
|
||||||
|
)
|
||||||
|
val clues: Collection<Clue> = description.lines()
|
||||||
|
.filter { it.startsWith("* ") }
|
||||||
|
.map { line ->
|
||||||
|
Clue.parse(line) {
|
||||||
|
optionMap[it] ?: throw IllegalArgumentException("Unknown option $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Game(
|
||||||
|
grid = Grid(
|
||||||
|
rows = rowOptions
|
||||||
|
.map { options -> createRow<ItemClass<*>>(options) }
|
||||||
|
.toList()
|
||||||
|
),
|
||||||
|
clues = clues
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <C : ItemClass<C>> createRow(options: List<Item<*>>): GameRow<*> {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
options as List<Item<C>>
|
||||||
|
return GameRow(
|
||||||
|
category = options.first().itemType.companion,
|
||||||
|
options = options,
|
||||||
|
cells = options.map { GameCell(options = options.toMutableList()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package ch.dissem.yaep.domain
|
package ch.dissem.yaep.domain
|
||||||
|
|
||||||
class GameCell<C : ItemClass<C>>(
|
class GameCell<C : ItemClass<C>>(
|
||||||
selection: Item<C>?,
|
selection: Item<C>?=null,
|
||||||
val solution: Item<C>,
|
val solution: Item<C>? = null,
|
||||||
options: Collection<Item<C>>
|
options: Collection<Item<C>>
|
||||||
) {
|
) {
|
||||||
val selectionChangedListeners = mutableListOf<(Item<C>?) -> Unit>()
|
val selectionChangedListeners = mutableListOf<(Item<C>?) -> Unit>()
|
||||||
|
|||||||
@@ -5,52 +5,53 @@ class GameRow<C : ItemClass<C>>(
|
|||||||
val options: List<Item<C>>,
|
val options: List<Item<C>>,
|
||||||
val cells: List<GameCell<C>>
|
val cells: List<GameCell<C>>
|
||||||
) : List<GameCell<C>> by cells {
|
) : List<GameCell<C>> by cells {
|
||||||
|
var isSolved = false
|
||||||
|
private set
|
||||||
|
|
||||||
fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element }
|
fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element }
|
||||||
|
|
||||||
fun cleanupOptions() {
|
fun cleanupOptions() {
|
||||||
cells.forEach {
|
if (
|
||||||
cleanupOptions(it)
|
isSolved && all {
|
||||||
}
|
it.solution != null
|
||||||
// do {
|
&& it.options.size == 1
|
||||||
// var selectedSingleOption = false
|
&& it.options.single() == it.selection
|
||||||
// cells.forEach { cleanupOptions(it) }
|
|
||||||
// val selections = cells.mapNotNull { it.selection }
|
|
||||||
// options
|
|
||||||
// .filter { !selections.contains(it) }
|
|
||||||
// .forEach { option ->
|
|
||||||
// if (cells.count { cell -> cell.options.contains(option) } == 1) {
|
|
||||||
// cells
|
|
||||||
// .filter { it.selection == null }
|
|
||||||
// .first { cell -> cell.options.contains(option) }
|
|
||||||
// .let { it.selection = option }
|
|
||||||
// selectedSingleOption = true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } while (selectedSingleOption)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanupOptions(cell: GameCell<C>, justSelected: Boolean = true) {
|
|
||||||
if ((justSelected && cell.selection != null) || (cell.options.size == 1 && cell.selection == null)) {
|
|
||||||
val selection = cell.selection
|
|
||||||
if (selection == null) {
|
|
||||||
cell.selection = cell.options.first()
|
|
||||||
} else {
|
|
||||||
cell.options.clear()
|
|
||||||
cell.options.add(selection)
|
|
||||||
}
|
}
|
||||||
filter { otherCell -> otherCell != cell && otherCell.hasNoSelection() }.forEach { otherCell ->
|
) return
|
||||||
otherCell.options.remove(cell.selection)
|
do {
|
||||||
cleanupOptions(otherCell, false)
|
var changed = false
|
||||||
|
val selections = filter { it.selection != null }.let { cellsWithSelection ->
|
||||||
|
cellsWithSelection
|
||||||
|
.filter { it.options.size != 1 }
|
||||||
|
.forEach {
|
||||||
|
it.options.clear()
|
||||||
|
it.options.add(it.selection!!)
|
||||||
|
}
|
||||||
|
cellsWithSelection.mapNotNull { it.selection }.toMutableSet()
|
||||||
}
|
}
|
||||||
filter { it.selection == null }
|
filter { it.selection == null }
|
||||||
.flatMap { c -> c.options.map { o -> o to c } }
|
.forEach { it.options.removeAll(selections) }
|
||||||
.groupBy { it.first }
|
filter { it.selection == null && it.options.size == 1 }
|
||||||
.filter { it.value.size == 1 }
|
|
||||||
.forEach {
|
.forEach {
|
||||||
val c = it.value.single().second
|
changed = true
|
||||||
c.selection = it.key
|
it.selection = it.options.single()
|
||||||
cleanupOptions(c, true)
|
|
||||||
}
|
}
|
||||||
|
if (!changed) {
|
||||||
|
filter { it.selection == null }
|
||||||
|
.flatMap { c -> c.options.map { o -> o to c } }
|
||||||
|
.groupBy { it.first }
|
||||||
|
.filter { it.value.size == 1 }
|
||||||
|
.forEach {
|
||||||
|
val c = it.value.single().second
|
||||||
|
c.selection = it.key
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (changed)
|
||||||
|
isSolved = all {
|
||||||
|
it.solution != null
|
||||||
|
&& it.options.size == 1
|
||||||
|
&& it.options.single() == it.selection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,26 @@ sealed class Clue {
|
|||||||
* @return `true` if any option was removed
|
* @return `true` if any option was removed
|
||||||
*/
|
*/
|
||||||
abstract fun removeForbiddenOptions(grid: Grid): Boolean
|
abstract fun removeForbiddenOptions(grid: Grid): Boolean
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T : ItemClass<T>> parse(line: String, mapper: (ItemClass<*>) -> Item<T>): Clue {
|
||||||
|
return NeighbourClue.parse(line, mapper)
|
||||||
|
?: OrderClue.parse(line, mapper)
|
||||||
|
?: TripletClue.parse(line, mapper)
|
||||||
|
?: SameColumnClue.parse(line, mapper)
|
||||||
|
?: PositionClue.parse(line, mapper)
|
||||||
|
?: throw IllegalStateException("Unknown clue: $line")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ClueParser {
|
||||||
|
/**
|
||||||
|
* Parses a string description of a clue into a clue.
|
||||||
|
*
|
||||||
|
* Returns `null` if `line` isn't a representation of that clue.
|
||||||
|
*/
|
||||||
|
fun <T : ItemClass<T>> parse(line: String, mapper: (ItemClass<*>) -> Item<T>): Clue?
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class HorizontalClue : Clue()
|
sealed class HorizontalClue : Clue()
|
||||||
@@ -66,6 +86,21 @@ class NeighbourClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: I
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString() = "$aType is next to $bType"
|
override fun toString() = "$aType is next to $bType"
|
||||||
|
|
||||||
|
companion object : ClueParser {
|
||||||
|
override fun <T : ItemClass<T>> parse(
|
||||||
|
line: String,
|
||||||
|
mapper: (ItemClass<*>) -> Item<T>
|
||||||
|
): Clue? {
|
||||||
|
val regex = Regex("^(\\* )?(?<a>[A-Z_]+) is next to (?<b>[A-Z_]+)$")
|
||||||
|
val matchResult = regex.matchEntire(line) ?: return null
|
||||||
|
|
||||||
|
val a = ItemClass.parse(matchResult.groups["a"]!!.value)
|
||||||
|
val b = ItemClass.parse(matchResult.groups["b"]!!.value)
|
||||||
|
|
||||||
|
return NeighbourClue(mapper(a), mapper(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OrderClue<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, val right: Item<R>) :
|
class OrderClue<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, val right: Item<R>) :
|
||||||
@@ -113,6 +148,21 @@ class OrderClue<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, val right
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString() = "$leftType is left of $rightType"
|
override fun toString() = "$leftType is left of $rightType"
|
||||||
|
|
||||||
|
companion object : ClueParser {
|
||||||
|
override fun <T : ItemClass<T>> parse(
|
||||||
|
line: String,
|
||||||
|
mapper: (ItemClass<*>) -> Item<T>
|
||||||
|
): Clue? {
|
||||||
|
val regex = Regex("^(\\* )?(?<left>[A-Z_]+) is left of (?<right>[A-Z_]+)$")
|
||||||
|
val matchResult = regex.matchEntire(line) ?: return null
|
||||||
|
|
||||||
|
val left = ItemClass.parse(matchResult.groups["left"]!!.value)
|
||||||
|
val right = ItemClass.parse(matchResult.groups["right"]!!.value)
|
||||||
|
|
||||||
|
return OrderClue(mapper(left), mapper(right))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
|
class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
|
||||||
@@ -242,6 +292,23 @@ class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
|
|||||||
|
|
||||||
override fun toString(): String =
|
override fun toString(): String =
|
||||||
"$bType is between the neighbours $aType and $cType to both sides"
|
"$bType is between the neighbours $aType and $cType to both sides"
|
||||||
|
|
||||||
|
companion object : ClueParser {
|
||||||
|
override fun <T : ItemClass<T>> parse(
|
||||||
|
line: String,
|
||||||
|
mapper: (ItemClass<*>) -> Item<T>
|
||||||
|
): Clue? {
|
||||||
|
val regex =
|
||||||
|
Regex("^(\\* )?(?<b>[A-Z_]+) is between the neighbours (?<a>[A-Z_]+) and (?<c>[A-Z_]+) to both sides$")
|
||||||
|
val matchResult = regex.matchEntire(line) ?: return null
|
||||||
|
|
||||||
|
val a = ItemClass.parse(matchResult.groups["a"]!!.value)
|
||||||
|
val b = ItemClass.parse(matchResult.groups["b"]!!.value)
|
||||||
|
val c = ItemClass.parse(matchResult.groups["c"]!!.value)
|
||||||
|
|
||||||
|
return TripletClue(mapper(a), mapper(b), mapper(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) : Clue() {
|
class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) : Clue() {
|
||||||
@@ -291,6 +358,22 @@ class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b:
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "$aType and $bType are in the same column"
|
override fun toString(): String = "$aType and $bType are in the same column"
|
||||||
|
|
||||||
|
companion object : ClueParser {
|
||||||
|
override fun <T : ItemClass<T>> parse(
|
||||||
|
line: String,
|
||||||
|
mapper: (ItemClass<*>) -> Item<T>
|
||||||
|
): Clue? {
|
||||||
|
val regex = Regex("^(\\* )?(?<a>[A-Z_]+) and (?<b>[A-Z_]+) are in the same column$")
|
||||||
|
val matchResult = regex.matchEntire(line) ?: return null
|
||||||
|
|
||||||
|
val a = ItemClass.parse(matchResult.groups["a"]!!.value)
|
||||||
|
val b = ItemClass.parse(matchResult.groups["b"]!!.value)
|
||||||
|
|
||||||
|
return SameColumnClue(mapper(a), mapper(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PositionClue<C : ItemClass<C>>(val item: Item<C>, val index: Int) : Clue() {
|
class PositionClue<C : ItemClass<C>>(val item: Item<C>, val index: Int) : Clue() {
|
||||||
@@ -307,14 +390,31 @@ class PositionClue<C : ItemClass<C>>(val item: Item<C>, val index: Int) : Clue()
|
|||||||
val row = grid[itemType.companion]
|
val row = grid[itemType.companion]
|
||||||
var removed = false
|
var removed = false
|
||||||
row.forEachIndexed { i, cell ->
|
row.forEachIndexed { i, cell ->
|
||||||
if (i == index) {
|
if (cell.hasNoSelection()) {
|
||||||
removed = cell.options.retainAll { it == item } || removed
|
removed = if (i == index) {
|
||||||
} else {
|
cell.options.retainAll { it == item } || removed
|
||||||
removed = cell.options.remove(item) || removed
|
} else {
|
||||||
|
cell.options.remove(item) || removed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return removed
|
return removed
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString() = "$itemType is at position $index"
|
override fun toString() = "$itemType is at position $index"
|
||||||
|
|
||||||
|
companion object : ClueParser {
|
||||||
|
override fun <T : ItemClass<T>> parse(
|
||||||
|
line: String,
|
||||||
|
mapper: (ItemClass<*>) -> Item<T>
|
||||||
|
): Clue? {
|
||||||
|
val regex = Regex("^(\\* )?(?<type>[A-Z_]+) is at position (?<pos>\\d)$")
|
||||||
|
val matchResult = regex.matchEntire(line) ?: return null
|
||||||
|
|
||||||
|
val type = ItemClass.parse(matchResult.groups["type"]!!.value)
|
||||||
|
val pos = matchResult.groups["pos"]!!.value.toInt()
|
||||||
|
|
||||||
|
return PositionClue(mapper(type), pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ private fun idic(symbol: String): Array<String> = Array(GENDERS.size * SKIN_TONE
|
|||||||
sealed interface ItemClass<out SELF : ItemClass<SELF>> {
|
sealed interface ItemClass<out SELF : ItemClass<SELF>> {
|
||||||
val symbols: Array<String>
|
val symbols: Array<String>
|
||||||
|
|
||||||
|
val name: String
|
||||||
|
|
||||||
val companion: ItemClassCompanion<SELF>
|
val companion: ItemClassCompanion<SELF>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -168,6 +170,10 @@ sealed interface ItemClass<out SELF : ItemClass<SELF>> {
|
|||||||
fun randomClasses(n: Int): List<ItemClassCompanion<*>> {
|
fun randomClasses(n: Int): List<ItemClassCompanion<*>> {
|
||||||
return classes.shuffled().take(n)
|
return classes.shuffled().take(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(name: String): ItemClass<*> {
|
||||||
|
return classes.mapNotNull { it.parse(name) }.single()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,4 +183,8 @@ sealed interface ItemClassCompanion<out C : ItemClass<C>> {
|
|||||||
fun randomItems(n: Int): List<C> {
|
fun randomItems(n: Int): List<C> {
|
||||||
return items.shuffled().take(n)
|
return items.shuffled().take(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(name: String): C? {
|
||||||
|
return items.firstOrNull { it.name == name }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ class GameTest {
|
|||||||
game = generateGame()
|
game = generateGame()
|
||||||
}
|
}
|
||||||
println("Generated game #$i in ${time.inWholeMilliseconds}ms")
|
println("Generated game #$i in ${time.inWholeMilliseconds}ms")
|
||||||
expect(solve(game.grid, game.clues)).toEqual(SOLVABLE)
|
val solvable = solve(game.grid, game.clues)
|
||||||
|
if (solvable != SOLVABLE) {
|
||||||
|
println("Puzzle:\n$game")
|
||||||
|
}
|
||||||
|
expect(solvable).toEqual(SOLVABLE)
|
||||||
expect(time).toBeLessThan(500.milliseconds)
|
expect(time).toBeLessThan(500.milliseconds)
|
||||||
if (time < fastest) {
|
if (time < fastest) {
|
||||||
fastest = time
|
fastest = time
|
||||||
@@ -67,8 +71,8 @@ class GameTest {
|
|||||||
feature(Game::areCategoriesValid).toEqual(true)
|
feature(Game::areCategoriesValid).toEqual(true)
|
||||||
feature(Game::isValid).toEqual(true)
|
feature(Game::isValid).toEqual(true)
|
||||||
feature(Game::clues) {
|
feature(Game::clues) {
|
||||||
feature(List<Clue>::size).toBeGreaterThan(5)
|
feature(Collection<Clue>::size).toBeGreaterThan(5)
|
||||||
feature(List<Clue>::size).toBeLessThan(30)
|
feature(Collection<Clue>::size).toBeLessThan(30)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println("Clues: ${game.clues.size}")
|
println("Clues: ${game.clues.size}")
|
||||||
@@ -124,4 +128,42 @@ class GameTest {
|
|||||||
expect(solve(game.grid, game.clues)).toEqual(SOLVABLE)
|
expect(solve(game.grid, game.clues)).toEqual(SOLVABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ensure specific game is solvable`() {
|
||||||
|
val game = Game.parse("""
|
||||||
|
👩🏿⚕️👨🏽🎤👩🏿⚕️ 👩🏾🚀🧑🏿🏫
|
||||||
|
🐜🐕 🐐 🐐
|
||||||
|
🍉🥭🍐🍇🍍
|
||||||
|
🧁🍨🍩🍰🥧
|
||||||
|
🇨🇭🇬🇧🇯🇵🇺🇦🇬🇧🇨🇦
|
||||||
|
🍷🧃🍺🧃
|
||||||
|
|
||||||
|
* ZEBRA is between the neighbours PIE and PEAR to both sides
|
||||||
|
* WINE is at position 0
|
||||||
|
* SLOTH is between the neighbours ZEBRA and COFFEE to both sides
|
||||||
|
* ICE_CREAM is left of MANGO
|
||||||
|
* SWITZERLAND is at position 0
|
||||||
|
* PIE is at position 4
|
||||||
|
* SCIENTIST is between the neighbours ASTRONAUT and PEAR to both sides
|
||||||
|
* ROCK_STAR is between the neighbours SNAIL and ANT to both sides
|
||||||
|
* SNAIL is between the neighbours ROCK_STAR and TEA to both sides
|
||||||
|
* SOFTWARE_DEV is left of SLOTH
|
||||||
|
* SOFTWARE_DEV is left of HEALTH_WORKER
|
||||||
|
* MILK is between the neighbours CUSTARD and ZEBRA to both sides
|
||||||
|
* SLOTH is between the neighbours CUSTARD and CAKE to both sides
|
||||||
|
* SPAIN is between the neighbours CUSTARD and GRAPES to both sides
|
||||||
|
* SCIENTIST is between the neighbours SNAIL and SLOTH to both sides
|
||||||
|
* DOG is between the neighbours CUPCAKE and BEER to both sides
|
||||||
|
* SNAIL is between the neighbours BANANA and GRAPES to both sides
|
||||||
|
* SLOTH is between the neighbours GRAPES and CANADA to both sides
|
||||||
|
* UKRAINE and SCIENTIST are in the same column
|
||||||
|
* DOG is between the neighbours JAPAN and SWITZERLAND to both sides
|
||||||
|
* SLOTH is between the neighbours GOAT and TEA to both sides
|
||||||
|
* ROCK_STAR and ENGLAND are in the same column
|
||||||
|
* ROCK_STAR is next to DOUGHNUT
|
||||||
|
* PINEAPPLE is between the neighbours TEACHER and GRAPES to both sides
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
expect(solve(game.grid, game.clues)).toEqual(SOLVABLE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ class NeighbourClueTest : ClueTest() {
|
|||||||
val a = grid[ia][j - 1]
|
val a = grid[ia][j - 1]
|
||||||
val b = grid[ib][j]
|
val b = grid[ib][j]
|
||||||
|
|
||||||
expect(NeighbourClue(a.solution, b.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)
|
expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,8 +35,8 @@ class NeighbourClueTest : ClueTest() {
|
|||||||
val a = grid[ia][ja]
|
val a = grid[ia][ja]
|
||||||
val b = grid[ib][jb]
|
val b = grid[ib][jb]
|
||||||
|
|
||||||
expect(NeighbourClue(a.solution, b.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)
|
expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,32 +58,32 @@ class NeighbourClueTest : ClueTest() {
|
|||||||
rowB.forEach { it.selection = null; it.options.clear() }
|
rowB.forEach { it.selection = null; it.options.clear() }
|
||||||
|
|
||||||
a.selection = null
|
a.selection = null
|
||||||
a.options.add(a.solution)
|
a.options.add(a.solution!!)
|
||||||
b.selection = b.solution
|
b.selection = b.solution
|
||||||
b.options.clear()
|
b.options.clear()
|
||||||
|
|
||||||
expect(NeighbourClue(a.solution, b.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)
|
expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
|
||||||
|
|
||||||
a.selection = a.solution
|
a.selection = a.solution
|
||||||
a.options.clear()
|
a.options.clear()
|
||||||
b.selection = null
|
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(a.solution!!, b.solution!!).isValid(grid)).toEqual(true)
|
||||||
expect(NeighbourClue(b.solution, a.solution).isValid(grid)).toEqual(true)
|
expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
|
||||||
|
|
||||||
if (j < size - 1) {
|
if (j < size - 1) {
|
||||||
val notA = rowA[j + 1]
|
val notA = rowA[j + 1]
|
||||||
|
|
||||||
a.selection = null
|
a.selection = null
|
||||||
a.options.clear()
|
a.options.clear()
|
||||||
notA.options.add(a.solution)
|
notA.options.add(a.solution!!)
|
||||||
b.selection = b.solution
|
b.selection = b.solution
|
||||||
b.options.clear()
|
b.options.clear()
|
||||||
|
|
||||||
expect(NeighbourClue(a.solution, b.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)
|
expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,8 +102,8 @@ class NeighbourClueTest : ClueTest() {
|
|||||||
|
|
||||||
rowB[3].selection = b.solution
|
rowB[3].selection = b.solution
|
||||||
|
|
||||||
expect(NeighbourClue(a.solution, b.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)
|
expect(NeighbourClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class OrderClueTest : ClueTest() {
|
|||||||
val a = grid[ia][ja]
|
val a = grid[ia][ja]
|
||||||
val b = grid[ib][jb]
|
val b = grid[ib][jb]
|
||||||
|
|
||||||
expect(OrderClue(a.solution, b.solution).isValid(grid)).toEqual(true)
|
expect(OrderClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ class OrderClueTest : ClueTest() {
|
|||||||
val a = grid[ia][ja]
|
val a = grid[ia][ja]
|
||||||
val b = grid[ib][jb]
|
val b = grid[ib][jb]
|
||||||
|
|
||||||
expect(OrderClue(b.solution, a.solution).isValid(grid)).toEqual(false)
|
expect(OrderClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ class OrderClueTest : ClueTest() {
|
|||||||
for (rowA in grid.rows) {
|
for (rowA in grid.rows) {
|
||||||
for (rowB in grid.rows) {
|
for (rowB in grid.rows) {
|
||||||
for (i in 0 until size) {
|
for (i in 0 until size) {
|
||||||
expect(OrderClue(rowA[i].solution, rowB[i].solution).isValid(grid)).toEqual(false)
|
expect(OrderClue(rowA[i].solution!!, rowB[i].solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class SameColumnClueTest : ClueTest() {
|
|||||||
val a = grid[ia][j]
|
val a = grid[ia][j]
|
||||||
val b = grid[ib][j]
|
val b = grid[ib][j]
|
||||||
|
|
||||||
expect(SameColumnClue(a.solution, b.solution).isValid(grid)).toEqual(true)
|
expect(SameColumnClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(true)
|
||||||
expect(SameColumnClue(b.solution, a.solution).isValid(grid)).toEqual(true)
|
expect(SameColumnClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,8 @@ class SameColumnClueTest : ClueTest() {
|
|||||||
val a = grid[ia][ja]
|
val a = grid[ia][ja]
|
||||||
val b = grid[ib][jb]
|
val b = grid[ib][jb]
|
||||||
|
|
||||||
expect(SameColumnClue(a.solution, b.solution).isValid(grid)).toEqual(false)
|
expect(SameColumnClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false)
|
||||||
expect(SameColumnClue(b.solution, a.solution).isValid(grid)).toEqual(false)
|
expect(SameColumnClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,8 @@ class SameColumnClueTest : ClueTest() {
|
|||||||
a.selection = a.solution
|
a.selection = a.solution
|
||||||
b.selection = b.solution
|
b.selection = b.solution
|
||||||
|
|
||||||
expect(SameColumnClue(a.solution, b.solution).isValid(grid)).toEqual(false)
|
expect(SameColumnClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false)
|
||||||
expect(SameColumnClue(b.solution, a.solution).isValid(grid)).toEqual(false)
|
expect(SameColumnClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,8 +87,8 @@ class SameColumnClueTest : ClueTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(SameColumnClue(a.solution, b.solution).isValid(grid)).toEqual(false)
|
expect(SameColumnClue(a.solution!!, b.solution!!).isValid(grid)).toEqual(false)
|
||||||
expect(SameColumnClue(b.solution, a.solution).isValid(grid)).toEqual(false)
|
expect(SameColumnClue(b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ class TripletClueTest : ClueTest() {
|
|||||||
val b = grid[ib][j - 1]
|
val b = grid[ib][j - 1]
|
||||||
val c = grid[ic][j]
|
val c = grid[ic][j]
|
||||||
|
|
||||||
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(true)
|
expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(true)
|
||||||
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(true)
|
expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,14 +38,14 @@ class TripletClueTest : ClueTest() {
|
|||||||
|
|
||||||
rowB[0].selection = b.solution
|
rowB[0].selection = b.solution
|
||||||
|
|
||||||
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(false)
|
expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(false)
|
||||||
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(false)
|
expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
|
|
||||||
rowB[0].selection = null
|
rowB[0].selection = null
|
||||||
rowB[grid.size - 1].selection = b.solution
|
rowB[grid.size - 1].selection = b.solution
|
||||||
|
|
||||||
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(false)
|
expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(false)
|
||||||
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(false)
|
expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -61,10 +61,10 @@ class TripletClueTest : ClueTest() {
|
|||||||
val rowC = grid[1]
|
val rowC = grid[1]
|
||||||
val c = rowC[ic]
|
val c = rowC[ic]
|
||||||
|
|
||||||
val clue = TripletClue(a.solution, b.solution, c.solution)
|
val clue = TripletClue(a.solution!!, b.solution!!, c.solution!!)
|
||||||
|
|
||||||
b.selection = b.solution
|
b.selection = b.solution
|
||||||
c.options.add(c.solution)
|
c.options.add(c.solution!!)
|
||||||
|
|
||||||
|
|
||||||
rowA.forEachIndexed { index, notA ->
|
rowA.forEachIndexed { index, notA ->
|
||||||
@@ -75,7 +75,7 @@ class TripletClueTest : ClueTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
index == ic -> {
|
index == ic -> {
|
||||||
rowC[ia].options.add(c.solution)
|
rowC[ia].options.add(c.solution!!)
|
||||||
expect(clue.isValid(grid)).toEqual(true)
|
expect(clue.isValid(grid)).toEqual(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +97,8 @@ class TripletClueTest : ClueTest() {
|
|||||||
a.selection = a.solution
|
a.selection = a.solution
|
||||||
grid[1][4].selection = c.solution
|
grid[1][4].selection = c.solution
|
||||||
|
|
||||||
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(false)
|
expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(false)
|
||||||
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(false)
|
expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -115,11 +115,11 @@ class TripletClueTest : ClueTest() {
|
|||||||
b.options.clear()
|
b.options.clear()
|
||||||
c.options.clear()
|
c.options.clear()
|
||||||
|
|
||||||
rowB[4].options.add(b.solution)
|
rowB[4].options.add(b.solution!!)
|
||||||
rowC[5].options.add(c.solution)
|
rowC[5].options.add(c.solution!!)
|
||||||
|
|
||||||
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(true)
|
expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(true)
|
||||||
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(true)
|
expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -136,7 +136,7 @@ class TripletClueTest : ClueTest() {
|
|||||||
rowB[4].selection = rowC[3].solution
|
rowB[4].selection = rowC[3].solution
|
||||||
c.selection = rowC[3].solution
|
c.selection = rowC[3].solution
|
||||||
|
|
||||||
expect(TripletClue(a.solution, b.solution, c.solution).isValid(grid)).toEqual(false)
|
expect(TripletClue(a.solution!!, b.solution!!, c.solution!!).isValid(grid)).toEqual(false)
|
||||||
expect(TripletClue(c.solution, b.solution, a.solution).isValid(grid)).toEqual(false)
|
expect(TripletClue(c.solution!!, b.solution!!, a.solution!!).isValid(grid)).toEqual(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user