diff --git a/composeApp/src/commonMain/kotlin/domain/clues.kt b/composeApp/src/commonMain/kotlin/domain/clues.kt index bed6e28..e34ce49 100644 --- a/composeApp/src/commonMain/kotlin/domain/clues.kt +++ b/composeApp/src/commonMain/kotlin/domain/clues.kt @@ -3,7 +3,6 @@ package domain import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import kotlin.math.abs sealed class Clue { abstract fun isRuleViolated(grid: Grid): Boolean @@ -17,12 +16,21 @@ class NeighbourClue>(val a: Item, val b: Item) : Horizont private val bType = b.itemType override fun isRuleViolated(grid: Grid): Boolean { - val ia = grid.indexOf(aType) - val ib = grid.indexOf(bType) + val rowA = grid[aType.companion] + 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>(val left: Item, val right: Item) : Horiz private val rightType = right.itemType override fun isRuleViolated(grid: Grid): Boolean { - val il = grid.indexOf(leftType) - val ir = grid.indexOf(rightType) + val rowLeft = grid[leftType.companion] + val rowRight = grid[rightType.companion] - if (il == -1 || ir == -1) return false - - return ir <= il + return rowLeft.indexOfFirst { it.mayBe(left) } >= rowRight.indexOfLast { it.mayBe(right) } } } @@ -46,33 +52,25 @@ class TripletClue>(val a: Item, val b: Item, val c: Item< private val bType = b.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 { - val ia = grid.indexOf(aType) - val ib = grid.indexOf(bType) - val ic = grid.indexOf(cType) + val rowA = grid[aType.companion] + val rowB = grid[bType.companion] + val rowC = grid[cType.companion] - if (ib == 0 || ib == grid.size) { - return true + for (i in 2 until grid.size) { + 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 } - - 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 + return true } } @@ -81,23 +79,20 @@ class SameRowClue>(val a: Item, val b: Item) : Clue() { private val bType = b.itemType override fun isRuleViolated(grid: Grid): Boolean { - val ia = grid.indexOf(aType) - val ib = grid.indexOf(bType) + val rowA = grid[aType.companion] + val rowB = grid[bType.companion] - if (ia == -1 || ib == -1) return false - - return ia != ib + for (i in 0 until grid.size) { + if (rowA[i].mayBe(a) && rowB[i].mayBe(b)) { + return false + } + } + return true } } class PositionClue>(val item: Item, val index: Int) : Clue() { - private val aType = item.itemType - override fun isRuleViolated(grid: Grid): Boolean { - val ia = grid.indexOf(aType) - - if (ia == -1) return false - - return ia != index + return grid[item].mayBe(item) } } diff --git a/composeApp/src/commonMain/kotlin/domain/generator.kt b/composeApp/src/commonMain/kotlin/domain/generator.kt index daec5a9..06a5852 100644 --- a/composeApp/src/commonMain/kotlin/domain/generator.kt +++ b/composeApp/src/commonMain/kotlin/domain/generator.kt @@ -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.) } +// FIXME: I need to better include the options into the solver (rule violations checks) private fun solve( grid: Grid, clues: Collection @@ -121,13 +122,11 @@ fun > GameRow.cleanupOptions(cell: GameCell) { private fun getAllClues(rows: List>>>): MutableSet { val clues = mutableSetOf() -// rows.forEach { row -> -// row.forEachIndexed { i, item -> -// clues.add(PositionClue(item, i)) -// } -// } - clues.add(PositionClue(rows.random().first(), 0)) - clues.add(PositionClue(rows.random()[3], 3)) + rows.forEach { row -> + row.forEachIndexed { i, item -> + clues.add(PositionClue(item, i)) + } + } rows.forEach { columns -> columns.forEachIndexed { j, item -> diff --git a/composeApp/src/commonMain/kotlin/domain/grid.kt b/composeApp/src/commonMain/kotlin/domain/grid.kt index b98733d..e345b67 100644 --- a/composeApp/src/commonMain/kotlin/domain/grid.kt +++ b/composeApp/src/commonMain/kotlin/domain/grid.kt @@ -9,7 +9,12 @@ class GameRow>( val category: ItemClassCompanion, val options: List>, val cells: List> -) : List> by cells +) : List> by cells { + fun updateOptions() { + val selections = mapNotNull { it.selection } + forEach { it.options.removeAll(selections) } + } +} class Grid( val rows: List>> @@ -25,6 +30,14 @@ class Grid( return rows.first { it.category == itemType } as GameRow } + operator fun > get(item: Item): GameCell { + 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>>>.toGrid() = Grid( @@ -50,6 +63,8 @@ class GameCell>( ) { val options = options.toMutableStateList() var selection by mutableStateOf(selection) + + fun mayBe(item: Item) = selection == item || options.contains(item) } class Item>( diff --git a/composeApp/src/commonTest/kotlin/domain/ClueTest.kt b/composeApp/src/commonTest/kotlin/domain/ClueTest.kt index 253c520..44674a6 100644 --- a/composeApp/src/commonTest/kotlin/domain/ClueTest.kt +++ b/composeApp/src/commonTest/kotlin/domain/ClueTest.kt @@ -3,7 +3,9 @@ package domain abstract class ClueTest { protected val size = 6 - protected fun createGrid(selection: (Item>) -> Item>? = { it }) = Grid( + protected fun createGrid( + selection: (Item>) -> Item>? = { it } + ) = Grid( ItemClass.randomClasses(size) .map { it.randomItems(size).map { item -> Item(item) } diff --git a/composeApp/src/commonTest/kotlin/domain/NeighbourClueTest.kt b/composeApp/src/commonTest/kotlin/domain/NeighbourClueTest.kt index 10b3893..30ce3f6 100644 --- a/composeApp/src/commonTest/kotlin/domain/NeighbourClueTest.kt +++ b/composeApp/src/commonTest/kotlin/domain/NeighbourClueTest.kt @@ -48,7 +48,7 @@ class NeighbourClueTest : ClueTest() { } @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() for (ia in 0 until size) { for (ib in 0 until size) { @@ -57,7 +57,9 @@ class NeighbourClueTest : ClueTest() { val b = grid[ib][j] a.selection = null + a.options.add(a.solution) b.selection = b.solution + b.options.clear() expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid)) .toEqual(false) @@ -65,12 +67,29 @@ class NeighbourClueTest : ClueTest() { .toEqual(false) a.selection = a.solution + a.options.clear() b.selection = null + b.options.add(b.solution) expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid)) .toEqual(false) expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid)) .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) + } } } } diff --git a/composeApp/src/commonTest/kotlin/domain/SameRowClueTest.kt b/composeApp/src/commonTest/kotlin/domain/SameRowClueTest.kt new file mode 100644 index 0000000..5506457 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/domain/SameRowClueTest.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonTest/kotlin/domain/TripletClueTest.kt b/composeApp/src/commonTest/kotlin/domain/TripletClueTest.kt index 765e0cc..8793de4 100644 --- a/composeApp/src/commonTest/kotlin/domain/TripletClueTest.kt +++ b/composeApp/src/commonTest/kotlin/domain/TripletClueTest.kt @@ -28,7 +28,46 @@ class TripletClueTest : ClueTest() { } @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 a = grid[2][1] val b = grid[0][2] @@ -43,4 +82,43 @@ class TripletClueTest : ClueTest() { .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) + } } \ No newline at end of file