From 593939a082404a640ba89ded007406c37962143b Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Tue, 25 Jun 2024 09:49:44 +0200 Subject: [PATCH] Fix tests & clues (WIP) --- composeApp/src/commonMain/kotlin/App.kt | 12 +- .../src/commonMain/kotlin/domain/Game.kt | 4 +- .../src/commonMain/kotlin/domain/clues.kt | 123 ++++++++++++++++-- .../src/commonMain/kotlin/domain/generator.kt | 2 +- .../src/commonMain/kotlin/domain/grid.kt | 5 +- .../kotlin/domain/NeighbourClueTest.kt | 22 +++- ...meRowClueTest.kt => SameColumnClueTest.kt} | 44 ++++--- .../kotlin/domain/TripletClueTest.kt | 17 ++- 8 files changed, 175 insertions(+), 54 deletions(-) rename composeApp/src/commonTest/kotlin/domain/{SameRowClueTest.kt => SameColumnClueTest.kt} (54%) diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index 3456daf..842bfcf 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -23,7 +23,7 @@ import domain.HorizontalClue import domain.ItemClass import domain.NeighbourClue import domain.OrderClue -import domain.SameRowClue +import domain.SameColumnClue import domain.TripletClue import domain.generateGame import org.jetbrains.compose.resources.painterResource @@ -80,7 +80,7 @@ fun PuzzleGrid( fun PuzzleClues( modifier: Modifier = Modifier, horizontalClues: List>, - verticalClues: List>>> + verticalClues: List, ItemClass<*>>>> ) { Column(modifier = modifier) { LazyVerticalGrid( @@ -137,7 +137,7 @@ fun PuzzleClues( fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue) { Column { when (clue) { - is NeighbourClue<*> -> { + is NeighbourClue<*, *> -> { DrawItem(modifier = Modifier.weight(1f), clue.a) OutlinedCard(modifier = modifier.aspectRatio(1f).weight(1f)) { Image( @@ -148,7 +148,7 @@ fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue) { DrawItem(modifier = Modifier.weight(1f), clue.b) } - is OrderClue<*> -> { + is OrderClue<*, *> -> { DrawItem(modifier = Modifier.weight(1f), clue.left) OutlinedCard(modifier = modifier.aspectRatio(1f).weight(1f)) { Image(painter = painterResource(Res.drawable.order), contentDescription = null) @@ -156,7 +156,7 @@ fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue) { DrawItem(modifier = Modifier.weight(1f), clue.right) } - is TripletClue<*> -> { + is TripletClue<*, *, *> -> { DrawItem(modifier = Modifier.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), clue.c) @@ -166,7 +166,7 @@ fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue) { } @Composable -fun VerticalClue(modifier: Modifier = Modifier, clue: SameRowClue<*>) { +fun VerticalClue(modifier: Modifier = Modifier, clue: SameColumnClue<*, *>) { Column(modifier = modifier) { DrawItem(modifier = Modifier.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), clue.b) diff --git a/composeApp/src/commonMain/kotlin/domain/Game.kt b/composeApp/src/commonMain/kotlin/domain/Game.kt index 222af70..d033cfe 100644 --- a/composeApp/src/commonMain/kotlin/domain/Game.kt +++ b/composeApp/src/commonMain/kotlin/domain/Game.kt @@ -1,13 +1,11 @@ package domain -import androidx.compose.ui.util.fastAny - class Game( val grid: Grid, val clues: List ) { val horizontalClues = clues.filterIsInstance() - val verticalClues = clues.filterIsInstance>>() + val verticalClues = clues.filterIsInstance, ItemClass<*>>>() val positionalClues = clues.filterIsInstance>>() init { diff --git a/composeApp/src/commonMain/kotlin/domain/clues.kt b/composeApp/src/commonMain/kotlin/domain/clues.kt index a65558f..dd9a8ed 100644 --- a/composeApp/src/commonMain/kotlin/domain/clues.kt +++ b/composeApp/src/commonMain/kotlin/domain/clues.kt @@ -6,7 +6,8 @@ sealed class Clue { sealed class HorizontalClue : Clue() -class NeighbourClue>(val a: Item, val b: Item) : HorizontalClue() { +class NeighbourClue, B : ItemClass>(val a: Item, val b: Item) : + HorizontalClue() { private val aType = a.itemType private val bType = b.itemType @@ -14,6 +15,19 @@ class NeighbourClue>(val a: Item, val b: Item) : Horizont val rowA = grid[aType.companion] val rowB = grid[bType.companion] + val ia = rowA.indexOf(aType) + val ib by lazy { rowB.indexOf(bType) } + if (ia != -1) { + if (ib != -1) return !(ib == ia - 1 || ib == ia + 1) + return !(rowB.getOrNull(ia - 1).mayBe(b) || + rowB.getOrNull(ia + 1).mayBe(b)) + } + + if (ib != -1) { + return !(rowA.getOrNull(ib - 1).mayBe(a) || + rowA.getOrNull(ib + 1).mayBe(a)) + } + for (i in 1 until grid.size) { if (rowA[i - 1].mayBe(a) && rowB[i - 0].mayBe(b) @@ -29,7 +43,8 @@ class NeighbourClue>(val a: Item, val b: Item) : Horizont } } -class OrderClue>(val left: Item, val right: Item) : HorizontalClue() { +class OrderClue, R : ItemClass>(val left: Item, val right: Item) : + HorizontalClue() { private val leftType = left.itemType private val rightType = right.itemType @@ -37,26 +52,92 @@ class OrderClue>(val left: Item, val right: Item) : Horiz val rowLeft = grid[leftType.companion] val rowRight = grid[rightType.companion] + val iLeft = rowLeft.indexOf(leftType) + val iRight by lazy { rowRight.indexOf(rightType) } + if (iLeft != -1) { + if (iRight != -1) return iRight <= iLeft + + return rowRight.indexOfLast { it.mayBe(right) } > iLeft + } + + if (iRight != -1) { + return rowLeft.indexOfFirst { it.mayBe(left) } in 0 until iRight + } + return rowLeft.indexOfFirst { it.mayBe(left) } >= rowRight.indexOfLast { it.mayBe(right) } } } -class TripletClue>(val a: Item, val b: Item, val c: Item) : +class TripletClue, B : ItemClass, C : ItemClass>( + val a: Item, + val b: Item, + val c: Item +) : HorizontalClue() { private val aType = a.itemType private val bType = b.itemType private val cType = c.itemType override fun isRuleViolated(grid: Grid): Boolean { - val rowA = grid[aType.companion] - val rowB = grid[bType.companion] - val rowC = grid[cType.companion] + val rowA by lazy { grid[aType.companion] } + val rowB by lazy { grid[bType.companion] } + val rowC by lazy { grid[cType.companion] } + + val ia = rowA.indexOf(aType) + val ib by lazy { rowB.indexOf(bType) } + val ic by lazy { rowC.indexOf(cType) } + + if (ia != -1) { + if (ib != -1) { + when (ib) { + ia - 1 -> { + return if (ic != -1) { + ic != ia - 2 + } else { + !rowC[ia - 2].mayBe(c) + } + } + + ia + 1 -> { + return if (ic != -1) { + ic != ia + 2 + } else { + !rowC[ia + 2].mayBe(c) + } + } + + else -> return true + } + } + return when (ic) { + ia - 2 -> !rowB[ia - 1].mayBe(b) + ia + 2 -> !rowB[ia + 1].mayBe(b) + -1 -> !(rowB.getOrNull(ia - 1).mayBe(b) && rowC.getOrNull(ia - 2).mayBe(c)) && + !(rowB.getOrNull(ia + 1).mayBe(b) && rowC.getOrNull(ia + 2).mayBe(c)) + + else -> true + } + } + + if (ib != -1) { + if (ib == 0 || ib == rowB.size - 1) return true + + return when (ic) { + -1 -> !(rowA.getOrNull(ib - 1).mayBe(a) && rowC.getOrNull(ia - 2).mayBe(c)) && + !(rowA.getOrNull(ib + 1).mayBe(a) && rowC.getOrNull(ia + 2).mayBe(c)) + + ib - 1 -> rowA[ib + 1].mayBe(a) + ib + 1 -> rowA[ib - 1].mayBe(a) + else -> false + } + } + + if (ic != -1) { + return !(rowB.getOrNull(ic - 1).mayBe(b) && rowA.getOrNull(ic - 2).mayBe(a)) && + !(rowB.getOrNull(ic + 1).mayBe(b) && rowA.getOrNull(ic + 2).mayBe(a)) + } for (i in 2 until grid.size) { - if (rowA[i-2].selection == a){ - return rowB[i - 1].mayBe(b) && - rowC[i - 0].mayBe(c) - } if (rowA[i - 2].mayBe(a) && rowB[i - 1].mayBe(b) && rowC[i - 0].mayBe(c) @@ -73,7 +154,7 @@ class TripletClue>(val a: Item, val b: Item, val c: Item< } } -class SameRowClue>(val a: Item, val b: Item) : Clue() { +class SameColumnClue, B : ItemClass>(val a: Item, val b: Item) : Clue() { private val aType = a.itemType private val bType = b.itemType @@ -81,8 +162,23 @@ class SameRowClue>(val a: Item, val b: Item) : Clue() { val rowA = grid[aType.companion] val rowB = grid[bType.companion] + val ia = rowA.indexOf(aType) + val ib by lazy { rowB.indexOf(bType) } + + if (ia != -1) { + return if (ib != -1) { + ib != ia + } else { + !rowB[ia].mayBe(b) + } + } + + if (ib != -1) { + return !rowA[ib].mayBe(a) + } + for (i in 0 until grid.size) { - if (rowA[i].mayBe(a) && rowB[i].mayBe(b)) { + if (!rowA[i].mayBe(a) && !rowB[i].mayBe(b)) { return false } } @@ -92,6 +188,9 @@ class SameRowClue>(val a: Item, val b: Item) : Clue() { class PositionClue>(val item: Item, val index: Int) : Clue() { override fun isRuleViolated(grid: Grid): Boolean { + val i = grid.indexOf(item.itemType) + if (i != -1) return i != 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 7169e05..794dfd5 100644 --- a/composeApp/src/commonMain/kotlin/domain/generator.kt +++ b/composeApp/src/commonMain/kotlin/domain/generator.kt @@ -167,7 +167,7 @@ private fun getAllClues(rows: List>>>): MutableSet // Clue: Same Column rows.map { it[j] }.forEach { if (it != item) { - clues.add(SameRowClue(item, it)) + clues.add(SameColumnClue(item, it)) } } } diff --git a/composeApp/src/commonMain/kotlin/domain/grid.kt b/composeApp/src/commonMain/kotlin/domain/grid.kt index e01fe7b..19701bf 100644 --- a/composeApp/src/commonMain/kotlin/domain/grid.kt +++ b/composeApp/src/commonMain/kotlin/domain/grid.kt @@ -10,6 +10,8 @@ class GameRow>( val options: List>, val cells: List> ) : List> by cells { + fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element } + fun updateOptions() { val selections = mapNotNull { it.selection } forEach { it.options.removeAll(selections) } @@ -64,9 +66,10 @@ class GameCell>( val options = options.toMutableStateList() var selection by mutableStateOf(selection) - fun mayBe(item: Item) = selection == item || (selection == null && options.contains(item)) } +fun > GameCell?.mayBe(item: Item) = this != null && (selection == item || (selection == null && options.contains(item))) + class Item>( val itemType: C, val symbol: String = itemType.symbols.random() diff --git a/composeApp/src/commonTest/kotlin/domain/NeighbourClueTest.kt b/composeApp/src/commonTest/kotlin/domain/NeighbourClueTest.kt index 30ce3f6..aedbcf7 100644 --- a/composeApp/src/commonTest/kotlin/domain/NeighbourClueTest.kt +++ b/composeApp/src/commonTest/kotlin/domain/NeighbourClueTest.kt @@ -53,8 +53,13 @@ class NeighbourClueTest : ClueTest() { for (ia in 0 until size) { for (ib in 0 until size) { for (j in 1 until size) { - val a = grid[ia][j - 1] - val b = grid[ib][j] + val rowA = grid[ia] + val rowB = grid[ib] + val a = rowA[j - 1] + val b = rowB[j] + + rowA.forEach { it.selection = null; it.options.clear() } + rowB.forEach { it.selection = null; it.options.clear() } a.selection = null a.options.add(a.solution) @@ -76,8 +81,8 @@ class NeighbourClueTest : ClueTest() { expect(NeighbourClue(b.solution, a.solution).isRuleViolated(grid)) .toEqual(false) - if (j < size-1) { - val notA = grid[ia][j + 1] + if (j < size - 1) { + val notA = rowA[j + 1] a.selection = null a.options.clear() @@ -98,11 +103,14 @@ class NeighbourClueTest : ClueTest() { @Test fun `ensure grid with a and c more than one cell between is not considered valid`() { val grid = createGrid { null } - val a = grid[2][1] - val b = grid[0][2] + val rowA = grid.random() + val rowB = grid.random() + val a = rowA[1] + val b = rowB[2] a.selection = a.solution - grid[0][3].selection = b.solution + + rowB[3].selection = b.solution expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid)) .toEqual(true) diff --git a/composeApp/src/commonTest/kotlin/domain/SameRowClueTest.kt b/composeApp/src/commonTest/kotlin/domain/SameColumnClueTest.kt similarity index 54% rename from composeApp/src/commonTest/kotlin/domain/SameRowClueTest.kt rename to composeApp/src/commonTest/kotlin/domain/SameColumnClueTest.kt index 5506457..12d5db0 100644 --- a/composeApp/src/commonTest/kotlin/domain/SameRowClueTest.kt +++ b/composeApp/src/commonTest/kotlin/domain/SameColumnClueTest.kt @@ -4,9 +4,9 @@ import ch.tutteli.atrium.api.fluent.en_GB.toEqual import ch.tutteli.atrium.api.verbs.expect import kotlin.test.Test -class SameRowClueTest : ClueTest() { +class SameColumnClueTest : ClueTest() { @Test - fun `ensure fields in the same row are considered valid`() { + fun `ensure fields in the same column are considered valid`() { val grid = createGrid() for (ia in 0 until size - 1) { for (ib in ia + 1 until size) { @@ -14,9 +14,9 @@ class SameRowClueTest : ClueTest() { val a = grid[ia][j] val b = grid[ib][j] - expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid)) + expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid)) .toEqual(false) - expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid)) + expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid)) .toEqual(false) } } @@ -25,7 +25,7 @@ class SameRowClueTest : ClueTest() { } @Test - fun `ensure fields in different rows are considered invalid`() { + fun `ensure fields in different columns are considered invalid`() { val grid = createGrid() for (ia in 0 until size - 1) { for (ib in ia + 1 until size) { @@ -34,9 +34,9 @@ class SameRowClueTest : ClueTest() { val a = grid[ia][ja] val b = grid[ib][jb] - expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid)) + expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid)) .toEqual(true) - expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid)) + expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid)) .toEqual(true) } } @@ -45,7 +45,7 @@ class SameRowClueTest : ClueTest() { } @Test - fun `if a is set, but b is not an option in the same row, it's considered invalid`() { + fun `if a is set, but b is not an option in the same column, it's considered invalid`() { val grid = createGrid { null } for (ia in 0 until size - 1) { @@ -59,9 +59,9 @@ class SameRowClueTest : ClueTest() { b.selection = null b.options.remove(b.solution) - expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid)) + expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid)) .toEqual(true) - expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid)) + expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid)) .toEqual(true) } } @@ -70,7 +70,7 @@ class SameRowClueTest : ClueTest() { } @Test - fun `if there are no options for a and b in the same row, it's considered invalid`() { + fun `if there are no options for a and b in the same column, it's considered invalid`() { val grid = createGrid { null } val rowA = grid.random() @@ -79,14 +79,24 @@ class SameRowClueTest : ClueTest() { 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) } + rowA.forEachIndexed { index, gameCell -> + if (index < i) { + gameCell.options.remove(a.solution) + } else { + gameCell.options.add(a.solution) + } + } + rowB.forEachIndexed { index, gameCell -> + if (index < i) { + gameCell.options.add(b.solution) + } else { + gameCell.options.remove(b.solution) + } + } - expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid)) + expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid)) .toEqual(true) - expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid)) + expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid)) .toEqual(true) } } diff --git a/composeApp/src/commonTest/kotlin/domain/TripletClueTest.kt b/composeApp/src/commonTest/kotlin/domain/TripletClueTest.kt index bc349cf..ee01b33 100644 --- a/composeApp/src/commonTest/kotlin/domain/TripletClueTest.kt +++ b/composeApp/src/commonTest/kotlin/domain/TripletClueTest.kt @@ -112,16 +112,19 @@ class TripletClueTest : ClueTest() { @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] + val rowA = grid.rows.random() + val rowB = grid.rows.random() + val rowC = grid.rows.random() + val a = rowA[3] + val b = rowB[2] + val c = rowC[1] a.selection = a.solution - b.options.remove(b.solution) - c.options.remove(c.solution) + b.options.clear() + c.options.clear() - grid[0][4].options.add(b.solution) - grid[1][5].options.add(c.solution) + rowB[4].options.add(b.solution) + rowC[5].options.add(c.solution) expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) .toEqual(false)