Fix tests & clues (WIP)

This commit is contained in:
2024-06-25 09:49:44 +02:00
parent 4c7cc68024
commit 593939a082
8 changed files with 175 additions and 54 deletions

View File

@@ -23,7 +23,7 @@ import domain.HorizontalClue
import domain.ItemClass import domain.ItemClass
import domain.NeighbourClue import domain.NeighbourClue
import domain.OrderClue import domain.OrderClue
import domain.SameRowClue import domain.SameColumnClue
import domain.TripletClue import domain.TripletClue
import domain.generateGame import domain.generateGame
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@@ -80,7 +80,7 @@ fun PuzzleGrid(
fun PuzzleClues( fun PuzzleClues(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
horizontalClues: List<DisplayClue<HorizontalClue>>, horizontalClues: List<DisplayClue<HorizontalClue>>,
verticalClues: List<DisplayClue<SameRowClue<ItemClass<*>>>> verticalClues: List<DisplayClue<SameColumnClue<ItemClass<*>, ItemClass<*>>>>
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
LazyVerticalGrid( LazyVerticalGrid(
@@ -137,7 +137,7 @@ fun PuzzleClues(
fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue) { fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue) {
Column { Column {
when (clue) { when (clue) {
is NeighbourClue<*> -> { is NeighbourClue<*, *> -> {
DrawItem(modifier = Modifier.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), clue.a)
OutlinedCard(modifier = modifier.aspectRatio(1f).weight(1f)) { OutlinedCard(modifier = modifier.aspectRatio(1f).weight(1f)) {
Image( Image(
@@ -148,7 +148,7 @@ fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue) {
DrawItem(modifier = Modifier.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), clue.b)
} }
is OrderClue<*> -> { is OrderClue<*, *> -> {
DrawItem(modifier = Modifier.weight(1f), clue.left) DrawItem(modifier = Modifier.weight(1f), clue.left)
OutlinedCard(modifier = modifier.aspectRatio(1f).weight(1f)) { OutlinedCard(modifier = modifier.aspectRatio(1f).weight(1f)) {
Image(painter = painterResource(Res.drawable.order), contentDescription = null) 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) DrawItem(modifier = Modifier.weight(1f), clue.right)
} }
is TripletClue<*> -> { is TripletClue<*, *, *> -> {
DrawItem(modifier = Modifier.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), clue.a)
DrawItem(modifier = Modifier.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), clue.b)
DrawItem(modifier = Modifier.weight(1f), clue.c) DrawItem(modifier = Modifier.weight(1f), clue.c)
@@ -166,7 +166,7 @@ fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue) {
} }
@Composable @Composable
fun VerticalClue(modifier: Modifier = Modifier, clue: SameRowClue<*>) { fun VerticalClue(modifier: Modifier = Modifier, clue: SameColumnClue<*, *>) {
Column(modifier = modifier) { Column(modifier = modifier) {
DrawItem(modifier = Modifier.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), clue.a)
DrawItem(modifier = Modifier.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), clue.b)

View File

@@ -1,13 +1,11 @@
package domain package domain
import androidx.compose.ui.util.fastAny
class Game( class Game(
val grid: Grid, val grid: Grid,
val clues: List<Clue> val clues: List<Clue>
) { ) {
val horizontalClues = clues.filterIsInstance<HorizontalClue>() val horizontalClues = clues.filterIsInstance<HorizontalClue>()
val verticalClues = clues.filterIsInstance<SameRowClue<ItemClass<*>>>() val verticalClues = clues.filterIsInstance<SameColumnClue<ItemClass<*>, ItemClass<*>>>()
val positionalClues = clues.filterIsInstance<PositionClue<ItemClass<*>>>() val positionalClues = clues.filterIsInstance<PositionClue<ItemClass<*>>>()
init { init {

View File

@@ -6,7 +6,8 @@ sealed class Clue {
sealed class HorizontalClue : Clue() sealed class HorizontalClue : Clue()
class NeighbourClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>) : HorizontalClue() { class NeighbourClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) :
HorizontalClue() {
private val aType = a.itemType private val aType = a.itemType
private val bType = b.itemType private val bType = b.itemType
@@ -14,6 +15,19 @@ class NeighbourClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>) : Horizont
val rowA = grid[aType.companion] val rowA = grid[aType.companion]
val rowB = grid[bType.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) { for (i in 1 until grid.size) {
if (rowA[i - 1].mayBe(a) && if (rowA[i - 1].mayBe(a) &&
rowB[i - 0].mayBe(b) rowB[i - 0].mayBe(b)
@@ -29,7 +43,8 @@ class NeighbourClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>) : Horizont
} }
} }
class OrderClue<C : ItemClass<C>>(val left: Item<C>, val right: Item<C>) : HorizontalClue() { class OrderClue<L : ItemClass<L>, R : ItemClass<R>>(val left: Item<L>, val right: Item<R>) :
HorizontalClue() {
private val leftType = left.itemType private val leftType = left.itemType
private val rightType = right.itemType private val rightType = right.itemType
@@ -37,26 +52,92 @@ class OrderClue<C : ItemClass<C>>(val left: Item<C>, val right: Item<C>) : Horiz
val rowLeft = grid[leftType.companion] val rowLeft = grid[leftType.companion]
val rowRight = grid[rightType.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) } return rowLeft.indexOfFirst { it.mayBe(left) } >= rowRight.indexOfLast { it.mayBe(right) }
} }
} }
class TripletClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>, val c: Item<C>) : class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
val a: Item<A>,
val b: Item<B>,
val c: Item<C>
) :
HorizontalClue() { HorizontalClue() {
private val aType = a.itemType private val aType = a.itemType
private val bType = b.itemType private val bType = b.itemType
private val cType = c.itemType private val cType = c.itemType
override fun isRuleViolated(grid: Grid): Boolean { override fun isRuleViolated(grid: Grid): Boolean {
val rowA = grid[aType.companion] val rowA by lazy { grid[aType.companion] }
val rowB = grid[bType.companion] val rowB by lazy { grid[bType.companion] }
val rowC = grid[cType.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) { 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) && if (rowA[i - 2].mayBe(a) &&
rowB[i - 1].mayBe(b) && rowB[i - 1].mayBe(b) &&
rowC[i - 0].mayBe(c) rowC[i - 0].mayBe(c)
@@ -73,7 +154,7 @@ class TripletClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>, val c: Item<
} }
} }
class SameRowClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>) : Clue() { class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: Item<B>) : Clue() {
private val aType = a.itemType private val aType = a.itemType
private val bType = b.itemType private val bType = b.itemType
@@ -81,8 +162,23 @@ class SameRowClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>) : Clue() {
val rowA = grid[aType.companion] val rowA = grid[aType.companion]
val rowB = grid[bType.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) { 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 return false
} }
} }
@@ -92,6 +188,9 @@ class SameRowClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>) : Clue() {
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() {
override fun isRuleViolated(grid: Grid): Boolean { override fun isRuleViolated(grid: Grid): Boolean {
val i = grid.indexOf(item.itemType)
if (i != -1) return i != index
return grid[item].mayBe(item) return grid[item].mayBe(item)
} }
} }

View File

@@ -167,7 +167,7 @@ private fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableSet<Clue>
// Clue: Same Column // Clue: Same Column
rows.map { it[j] }.forEach { rows.map { it[j] }.forEach {
if (it != item) { if (it != item) {
clues.add(SameRowClue(item, it)) clues.add(SameColumnClue(item, it))
} }
} }
} }

View File

@@ -10,6 +10,8 @@ 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 {
fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element }
fun updateOptions() { fun updateOptions() {
val selections = mapNotNull { it.selection } val selections = mapNotNull { it.selection }
forEach { it.options.removeAll(selections) } forEach { it.options.removeAll(selections) }
@@ -64,9 +66,10 @@ 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 || (selection == null && options.contains(item))
} }
fun <C : ItemClass<C>> GameCell<C>?.mayBe(item: Item<C>) = this != null && (selection == item || (selection == null && options.contains(item)))
class Item<C : ItemClass<C>>( class Item<C : ItemClass<C>>(
val itemType: C, val itemType: C,
val symbol: String = itemType.symbols.random() val symbol: String = itemType.symbols.random()

View File

@@ -53,8 +53,13 @@ class NeighbourClueTest : ClueTest() {
for (ia in 0 until size) { for (ia in 0 until size) {
for (ib in 0 until size) { for (ib in 0 until size) {
for (j in 1 until size) { for (j in 1 until size) {
val a = grid[ia][j - 1] val rowA = grid[ia]
val b = grid[ib][j] 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.selection = null
a.options.add(a.solution) a.options.add(a.solution)
@@ -77,7 +82,7 @@ class NeighbourClueTest : ClueTest() {
.toEqual(false) .toEqual(false)
if (j < size - 1) { if (j < size - 1) {
val notA = grid[ia][j + 1] val notA = rowA[j + 1]
a.selection = null a.selection = null
a.options.clear() a.options.clear()
@@ -98,11 +103,14 @@ class NeighbourClueTest : ClueTest() {
@Test @Test
fun `ensure grid with a and c more than one cell between is not considered valid`() { fun `ensure grid with a and c more than one cell between is not considered valid`() {
val grid = createGrid { null } val grid = createGrid { null }
val a = grid[2][1] val rowA = grid.random()
val b = grid[0][2] val rowB = grid.random()
val a = rowA[1]
val b = rowB[2]
a.selection = a.solution a.selection = a.solution
grid[0][3].selection = b.solution
rowB[3].selection = b.solution
expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid)) expect(NeighbourClue(a.solution, b.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)

View File

@@ -4,9 +4,9 @@ import ch.tutteli.atrium.api.fluent.en_GB.toEqual
import ch.tutteli.atrium.api.verbs.expect import ch.tutteli.atrium.api.verbs.expect
import kotlin.test.Test import kotlin.test.Test
class SameRowClueTest : ClueTest() { class SameColumnClueTest : ClueTest() {
@Test @Test
fun `ensure fields in the same row are considered valid`() { fun `ensure fields in the same column are considered valid`() {
val grid = createGrid() val grid = createGrid()
for (ia in 0 until size - 1) { for (ia in 0 until size - 1) {
for (ib in ia + 1 until size) { for (ib in ia + 1 until size) {
@@ -14,9 +14,9 @@ class SameRowClueTest : ClueTest() {
val a = grid[ia][j] val a = grid[ia][j]
val b = grid[ib][j] val b = grid[ib][j]
expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid)) expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid))
.toEqual(false) .toEqual(false)
expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid)) expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid))
.toEqual(false) .toEqual(false)
} }
} }
@@ -25,7 +25,7 @@ class SameRowClueTest : ClueTest() {
} }
@Test @Test
fun `ensure fields in different rows are considered invalid`() { fun `ensure fields in different columns are considered invalid`() {
val grid = createGrid() val grid = createGrid()
for (ia in 0 until size - 1) { for (ia in 0 until size - 1) {
for (ib in ia + 1 until size) { for (ib in ia + 1 until size) {
@@ -34,9 +34,9 @@ class SameRowClueTest : ClueTest() {
val a = grid[ia][ja] val a = grid[ia][ja]
val b = grid[ib][jb] val b = grid[ib][jb]
expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid)) expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)
expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid)) expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)
} }
} }
@@ -45,7 +45,7 @@ class SameRowClueTest : ClueTest() {
} }
@Test @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 } val grid = createGrid { null }
for (ia in 0 until size - 1) { for (ia in 0 until size - 1) {
@@ -59,9 +59,9 @@ class SameRowClueTest : ClueTest() {
b.selection = null b.selection = null
b.options.remove(b.solution) b.options.remove(b.solution)
expect(SameRowClue(a.solution, b.solution).isRuleViolated(grid)) expect(SameColumnClue(a.solution, b.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)
expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid)) expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)
} }
} }
@@ -70,7 +70,7 @@ class SameRowClueTest : ClueTest() {
} }
@Test @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 grid = createGrid { null }
val rowA = grid.random() val rowA = grid.random()
@@ -79,14 +79,24 @@ class SameRowClueTest : ClueTest() {
for (i in 0 until size) { for (i in 0 until size) {
val a = rowA[i] val a = rowA[i]
val b = rowB[i] val b = rowB[i]
rowA.take(i).forEach { it.options.remove(a.solution) } rowA.forEachIndexed { index, gameCell ->
rowB.take(i).forEach { it.options.add(b.solution) } if (index < i) {
rowA.takeLast(size - i - 1).forEach { it.options.add(a.solution) } gameCell.options.remove(a.solution)
rowA.takeLast(size - i - 1).forEach { it.options.remove(b.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) .toEqual(true)
expect(SameRowClue(b.solution, a.solution).isRuleViolated(grid)) expect(SameColumnClue(b.solution, a.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)
} }
} }

View File

@@ -112,16 +112,19 @@ class TripletClueTest : ClueTest() {
@Test @Test
fun `grid with a set and b and c as option on the same side is considered valid`() { fun `grid with a set and b and c as option on the same side is considered valid`() {
val grid = createGrid { null } val grid = createGrid { null }
val a = grid[2][3] val rowA = grid.rows.random()
val b = grid[0][2] val rowB = grid.rows.random()
val c = grid[1][1] val rowC = grid.rows.random()
val a = rowA[3]
val b = rowB[2]
val c = rowC[1]
a.selection = a.solution a.selection = a.solution
b.options.remove(b.solution) b.options.clear()
c.options.remove(c.solution) c.options.clear()
grid[0][4].options.add(b.solution) rowB[4].options.add(b.solution)
grid[1][5].options.add(c.solution) rowC[5].options.add(c.solution)
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
.toEqual(false) .toEqual(false)