Add tests
This commit is contained in:
@@ -3,7 +3,6 @@ package domain
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
sealed class Clue {
|
sealed class Clue {
|
||||||
abstract fun isRuleViolated(grid: Grid): Boolean
|
abstract fun isRuleViolated(grid: Grid): Boolean
|
||||||
@@ -17,12 +16,21 @@ class NeighbourClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>) : Horizont
|
|||||||
private val bType = b.itemType
|
private val bType = b.itemType
|
||||||
|
|
||||||
override fun isRuleViolated(grid: Grid): Boolean {
|
override fun isRuleViolated(grid: Grid): Boolean {
|
||||||
val ia = grid.indexOf(aType)
|
val rowA = grid[aType.companion]
|
||||||
val ib = grid.indexOf(bType)
|
val rowB = grid[bType.companion]
|
||||||
|
|
||||||
if (ia == -1 || ib == -1) return false
|
for (i in 1 until grid.size) {
|
||||||
|
if (rowA[i - 1].mayBe(a) &&
|
||||||
|
rowB[i - 0].mayBe(b)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
return abs(ia - ib) != 1
|
if (rowA[i - 0].mayBe(a) &&
|
||||||
|
rowB[i - 1].mayBe(b)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,12 +39,10 @@ class OrderClue<C : ItemClass<C>>(val left: Item<C>, val right: Item<C>) : Horiz
|
|||||||
private val rightType = right.itemType
|
private val rightType = right.itemType
|
||||||
|
|
||||||
override fun isRuleViolated(grid: Grid): Boolean {
|
override fun isRuleViolated(grid: Grid): Boolean {
|
||||||
val il = grid.indexOf(leftType)
|
val rowLeft = grid[leftType.companion]
|
||||||
val ir = grid.indexOf(rightType)
|
val rowRight = grid[rightType.companion]
|
||||||
|
|
||||||
if (il == -1 || ir == -1) return false
|
return rowLeft.indexOfFirst { it.mayBe(left) } >= rowRight.indexOfLast { it.mayBe(right) }
|
||||||
|
|
||||||
return ir <= il
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,33 +52,25 @@ class TripletClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>, val c: Item<
|
|||||||
private val bType = b.itemType
|
private val bType = b.itemType
|
||||||
private val cType = c.itemType
|
private val cType = c.itemType
|
||||||
|
|
||||||
private fun isNeighbourRuleViolated(ix: Int, iy: Int): Boolean {
|
|
||||||
if (ix == -1 || iy == -1) return false
|
|
||||||
return abs(ix - iy) != 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isRuleViolated(grid: Grid): Boolean {
|
override fun isRuleViolated(grid: Grid): Boolean {
|
||||||
val ia = grid.indexOf(aType)
|
val rowA = grid[aType.companion]
|
||||||
val ib = grid.indexOf(bType)
|
val rowB = grid[bType.companion]
|
||||||
val ic = grid.indexOf(cType)
|
val rowC = grid[cType.companion]
|
||||||
|
|
||||||
if (ib == 0 || ib == grid.size) {
|
for (i in 2 until grid.size) {
|
||||||
return true
|
if (rowA[i - 2].mayBe(a) &&
|
||||||
|
rowB[i - 1].mayBe(b) &&
|
||||||
|
rowC[i - 0].mayBe(c)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (rowA[i - 0].mayBe(a) &&
|
||||||
|
rowB[i - 1].mayBe(b) &&
|
||||||
|
rowC[i - 2].mayBe(c)
|
||||||
|
)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
if (ia == -1 && ic == -1) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ia != -1 && ic != -1) {
|
|
||||||
return !(ia + 2 == ic || ia == ic + 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNeighbourRuleViolated(ia, ib) || isNeighbourRuleViolated(ib, ic)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,23 +79,20 @@ class SameRowClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>) : Clue() {
|
|||||||
private val bType = b.itemType
|
private val bType = b.itemType
|
||||||
|
|
||||||
override fun isRuleViolated(grid: Grid): Boolean {
|
override fun isRuleViolated(grid: Grid): Boolean {
|
||||||
val ia = grid.indexOf(aType)
|
val rowA = grid[aType.companion]
|
||||||
val ib = grid.indexOf(bType)
|
val rowB = grid[bType.companion]
|
||||||
|
|
||||||
if (ia == -1 || ib == -1) return false
|
for (i in 0 until grid.size) {
|
||||||
|
if (rowA[i].mayBe(a) && rowB[i].mayBe(b)) {
|
||||||
return ia != ib
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
||||||
private val aType = item.itemType
|
|
||||||
|
|
||||||
override fun isRuleViolated(grid: Grid): Boolean {
|
override fun isRuleViolated(grid: Grid): Boolean {
|
||||||
val ia = grid.indexOf(aType)
|
return grid[item].mayBe(item)
|
||||||
|
|
||||||
if (ia == -1) return false
|
|
||||||
|
|
||||||
return ia != index
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,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.)
|
// (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.)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: I need to better include the options into the solver (rule violations checks)
|
||||||
private fun solve(
|
private fun solve(
|
||||||
grid: Grid,
|
grid: Grid,
|
||||||
clues: Collection<Clue>
|
clues: Collection<Clue>
|
||||||
@@ -121,13 +122,11 @@ fun <C : ItemClass<C>> GameRow<C>.cleanupOptions(cell: GameCell<C>) {
|
|||||||
|
|
||||||
private fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableSet<Clue> {
|
private fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableSet<Clue> {
|
||||||
val clues = mutableSetOf<Clue>()
|
val clues = mutableSetOf<Clue>()
|
||||||
// rows.forEach { row ->
|
rows.forEach { row ->
|
||||||
// row.forEachIndexed { i, item ->
|
row.forEachIndexed { i, item ->
|
||||||
// clues.add(PositionClue(item, i))
|
clues.add(PositionClue(item, i))
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
clues.add(PositionClue(rows.random().first(), 0))
|
|
||||||
clues.add(PositionClue(rows.random()[3], 3))
|
|
||||||
|
|
||||||
rows.forEach { columns ->
|
rows.forEach { columns ->
|
||||||
columns.forEachIndexed { j, item ->
|
columns.forEachIndexed { j, item ->
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ class GameRow<C : ItemClass<C>>(
|
|||||||
val category: ItemClassCompanion<C>,
|
val category: ItemClassCompanion<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 {
|
||||||
|
fun updateOptions() {
|
||||||
|
val selections = mapNotNull { it.selection }
|
||||||
|
forEach { it.options.removeAll(selections) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Grid(
|
class Grid(
|
||||||
val rows: List<GameRow<ItemClass<*>>>
|
val rows: List<GameRow<ItemClass<*>>>
|
||||||
@@ -25,6 +30,14 @@ class Grid(
|
|||||||
return rows.first { it.category == itemType } as GameRow<C>
|
return rows.first { it.category == itemType } as GameRow<C>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operator fun <C : ItemClass<C>> get(item: Item<C>): GameCell<C> {
|
||||||
|
return this[item.itemType.companion].first { it.selection == item }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return rows.map { row -> row.map { it.selection?.symbol ?: " " }.joinToString("") }
|
||||||
|
.joinToString("\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid(
|
fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid(
|
||||||
@@ -50,6 +63,8 @@ class GameCell<C : ItemClass<C>>(
|
|||||||
) {
|
) {
|
||||||
val options = options.toMutableStateList()
|
val options = options.toMutableStateList()
|
||||||
var selection by mutableStateOf(selection)
|
var selection by mutableStateOf(selection)
|
||||||
|
|
||||||
|
fun mayBe(item: Item<C>) = selection == item || options.contains(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Item<C : ItemClass<C>>(
|
class Item<C : ItemClass<C>>(
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package domain
|
|||||||
abstract class ClueTest {
|
abstract class ClueTest {
|
||||||
protected val size = 6
|
protected val size = 6
|
||||||
|
|
||||||
protected fun createGrid(selection: (Item<ItemClass<*>>) -> Item<ItemClass<*>>? = { it }) = Grid(
|
protected fun createGrid(
|
||||||
|
selection: (Item<ItemClass<*>>) -> Item<ItemClass<*>>? = { it }
|
||||||
|
) = Grid(
|
||||||
ItemClass.randomClasses(size)
|
ItemClass.randomClasses(size)
|
||||||
.map {
|
.map {
|
||||||
it.randomItems(size).map { item -> Item(item) }
|
it.randomItems(size).map { item -> Item(item) }
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class NeighbourClueTest : ClueTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ensure grid with one neighbour not set is considered valid`() {
|
fun `ensure grid with one neighbour not set is considered valid if a neighbour is in the options`() {
|
||||||
val grid = createGrid()
|
val grid = createGrid()
|
||||||
for (ia in 0 until size) {
|
for (ia in 0 until size) {
|
||||||
for (ib in 0 until size) {
|
for (ib in 0 until size) {
|
||||||
@@ -57,7 +57,9 @@ class NeighbourClueTest : ClueTest() {
|
|||||||
val b = grid[ib][j]
|
val b = grid[ib][j]
|
||||||
|
|
||||||
a.selection = null
|
a.selection = null
|
||||||
|
a.options.add(a.solution)
|
||||||
b.selection = b.solution
|
b.selection = b.solution
|
||||||
|
b.options.clear()
|
||||||
|
|
||||||
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
||||||
.toEqual(false)
|
.toEqual(false)
|
||||||
@@ -65,12 +67,29 @@ class NeighbourClueTest : ClueTest() {
|
|||||||
.toEqual(false)
|
.toEqual(false)
|
||||||
|
|
||||||
a.selection = a.solution
|
a.selection = a.solution
|
||||||
|
a.options.clear()
|
||||||
b.selection = null
|
b.selection = null
|
||||||
|
b.options.add(b.solution)
|
||||||
|
|
||||||
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
||||||
.toEqual(false)
|
.toEqual(false)
|
||||||
expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid))
|
expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid))
|
||||||
.toEqual(false)
|
.toEqual(false)
|
||||||
|
|
||||||
|
if (j < size-1) {
|
||||||
|
val notA = grid[ia][j + 1]
|
||||||
|
|
||||||
|
a.selection = null
|
||||||
|
a.options.clear()
|
||||||
|
notA.options.add(a.solution)
|
||||||
|
b.selection = b.solution
|
||||||
|
b.options.clear()
|
||||||
|
|
||||||
|
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(false)
|
||||||
|
expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
93
composeApp/src/commonTest/kotlin/domain/SameRowClueTest.kt
Normal file
93
composeApp/src/commonTest/kotlin/domain/SameRowClueTest.kt
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
|
||||||
|
import ch.tutteli.atrium.api.verbs.expect
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class SameRowClueTest : ClueTest() {
|
||||||
|
@Test
|
||||||
|
fun `ensure fields in the same row are considered valid`() {
|
||||||
|
val grid = createGrid()
|
||||||
|
for (ia in 0 until size - 1) {
|
||||||
|
for (ib in ia + 1 until size) {
|
||||||
|
for (j in 0 until size) {
|
||||||
|
val a = grid[ia][j]
|
||||||
|
val b = grid[ib][j]
|
||||||
|
|
||||||
|
expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(false)
|
||||||
|
expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ensure fields in different rows are considered invalid`() {
|
||||||
|
val grid = createGrid()
|
||||||
|
for (ia in 0 until size - 1) {
|
||||||
|
for (ib in ia + 1 until size) {
|
||||||
|
for (ja in 0 until size - 1) {
|
||||||
|
for (jb in ja + 1 until size) {
|
||||||
|
val a = grid[ia][ja]
|
||||||
|
val b = grid[ib][jb]
|
||||||
|
|
||||||
|
expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(true)
|
||||||
|
expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `if a is set, but b is not an option in the same row, it's considered invalid`() {
|
||||||
|
val grid = createGrid { null }
|
||||||
|
|
||||||
|
for (ia in 0 until size - 1) {
|
||||||
|
for (ib in ia + 1 until size) {
|
||||||
|
for (ja in 0 until size - 1) {
|
||||||
|
for (jb in ja + 1 until size) {
|
||||||
|
val a = grid[ia][ja]
|
||||||
|
val b = grid[ib][jb]
|
||||||
|
|
||||||
|
a.selection = a.solution
|
||||||
|
b.selection = null
|
||||||
|
b.options.remove(b.solution)
|
||||||
|
|
||||||
|
expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(true)
|
||||||
|
expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `if there are no options for a and b in the same row, it's considered invalid`() {
|
||||||
|
val grid = createGrid { null }
|
||||||
|
|
||||||
|
val rowA = grid.random()
|
||||||
|
val rowB = grid.filter { it != rowA }.random()
|
||||||
|
|
||||||
|
for (i in 0 until size) {
|
||||||
|
val a = rowA[i]
|
||||||
|
val b = rowB[i]
|
||||||
|
rowA.take(i).forEach { it.options.remove(a.solution) }
|
||||||
|
rowB.take(i).forEach { it.options.add(b.solution) }
|
||||||
|
rowA.takeLast(size - i - 1).forEach { it.options.add(a.solution) }
|
||||||
|
rowA.takeLast(size - i - 1).forEach { it.options.remove(b.solution) }
|
||||||
|
|
||||||
|
expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(true)
|
||||||
|
expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,46 @@ class TripletClueTest : ClueTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ensure grid with a and c more than one cell between is not considered valid`() {
|
fun `ensure grid where a or c and b are not neighbours is invalid`() {
|
||||||
|
val grid = createGrid { null }
|
||||||
|
val ia = 1
|
||||||
|
val rowA = grid[2]
|
||||||
|
val a = rowA[ia]
|
||||||
|
val ib = 2
|
||||||
|
val rowB = grid[0]
|
||||||
|
val b = rowB[ib]
|
||||||
|
val ic = 3
|
||||||
|
val rowC = grid[1]
|
||||||
|
val c = rowC[ic]
|
||||||
|
|
||||||
|
val clue = TripletClue(a.solution, b.solution, c.solution)
|
||||||
|
|
||||||
|
b.selection = b.solution
|
||||||
|
c.options.add(c.solution)
|
||||||
|
|
||||||
|
|
||||||
|
rowA.forEachIndexed { index, notA ->
|
||||||
|
notA.selection = a.solution
|
||||||
|
when {
|
||||||
|
notA == a -> {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
index == ic -> {
|
||||||
|
rowC[ia].options.add(c.solution)
|
||||||
|
expect(clue.isRuleViolated(grid)).toEqual(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
expect(clue.isRuleViolated(grid)).toEqual(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notA.selection = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ensure grid with a and c more than one cell between is considered invalid`() {
|
||||||
val grid = createGrid { null }
|
val grid = createGrid { null }
|
||||||
val a = grid[2][1]
|
val a = grid[2][1]
|
||||||
val b = grid[0][2]
|
val b = grid[0][2]
|
||||||
@@ -43,4 +82,43 @@ class TripletClueTest : ClueTest() {
|
|||||||
.toEqual(true)
|
.toEqual(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `grid with a set and b and c as option on the same side is considered valid`() {
|
||||||
|
val grid = createGrid { null }
|
||||||
|
val a = grid[2][3]
|
||||||
|
val b = grid[0][2]
|
||||||
|
val c = grid[1][1]
|
||||||
|
|
||||||
|
a.selection = a.solution
|
||||||
|
b.options.remove(b.solution)
|
||||||
|
c.options.remove(c.solution)
|
||||||
|
|
||||||
|
grid[0][4].options.add(b.solution)
|
||||||
|
grid[1][5].options.add(c.solution)
|
||||||
|
|
||||||
|
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(false)
|
||||||
|
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `grid with a set and no b and c as option on the same side is considered invalid`() {
|
||||||
|
val grid = createGrid { null }
|
||||||
|
val a = grid[2][3]
|
||||||
|
val b = grid[0][2]
|
||||||
|
val c = grid[1][1]
|
||||||
|
|
||||||
|
a.selection = a.solution
|
||||||
|
b.options.remove(b.solution)
|
||||||
|
c.options.add(c.solution)
|
||||||
|
|
||||||
|
grid[0][4].options.add(b.solution)
|
||||||
|
grid[1][5].options.remove(c.solution)
|
||||||
|
|
||||||
|
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(true)
|
||||||
|
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
|
||||||
|
.toEqual(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user