Add tests

This commit is contained in:
Christian Basler
2024-06-24 18:42:39 +02:00
parent f76be158a0
commit 4c7cc68024
5 changed files with 66 additions and 33 deletions

View File

@@ -10,9 +10,14 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import domain.Clue
import domain.Grid import domain.Grid
import domain.HorizontalClue import domain.HorizontalClue
import domain.ItemClass import domain.ItemClass
@@ -28,12 +33,18 @@ import yaep.composeapp.generated.resources.Res
import yaep.composeapp.generated.resources.neighbour import yaep.composeapp.generated.resources.neighbour
import yaep.composeapp.generated.resources.order import yaep.composeapp.generated.resources.order
class DisplayClue<C : Clue>(val clue: C) {
var isActive by mutableStateOf(true)
}
@Composable @Composable
fun App(modifier: Modifier = Modifier) { fun App(modifier: Modifier = Modifier) {
val game = generateGame() val game = remember { generateGame() }
val horizontalClues = remember { game.horizontalClues.map { DisplayClue(it) } }
val verticalClues = remember { game.verticalClues.map { DisplayClue(it) } }
Row(modifier = modifier) { Row(modifier = modifier) {
PuzzleGrid(modifier = Modifier.weight(1f), game.grid) PuzzleGrid(modifier = Modifier.weight(1f), game.grid)
PuzzleClues(modifier = Modifier.weight(1f), game.horizontalClues, game.verticalClues) PuzzleClues(modifier = Modifier.weight(1f), horizontalClues, verticalClues)
} }
} }
@@ -68,8 +79,8 @@ fun PuzzleGrid(
@Composable @Composable
fun PuzzleClues( fun PuzzleClues(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
horizontalClues: List<HorizontalClue>, horizontalClues: List<DisplayClue<HorizontalClue>>,
verticalClues: List<SameRowClue<ItemClass<*>>> verticalClues: List<DisplayClue<SameRowClue<ItemClass<*>>>>
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
LazyVerticalGrid( LazyVerticalGrid(
@@ -80,7 +91,7 @@ fun PuzzleClues(
item { item {
HorizontalClue( HorizontalClue(
modifier = Modifier.clickable { clue.isActive = false }, modifier = Modifier.clickable { clue.isActive = false },
clue = clue clue = clue.clue
) )
} }
} }
@@ -90,7 +101,7 @@ fun PuzzleClues(
modifier = Modifier modifier = Modifier
.alpha(0.5f) .alpha(0.5f)
.clickable { clue.isActive = true }, .clickable { clue.isActive = true },
clue = clue clue = clue.clue
) )
} }
} }
@@ -104,7 +115,7 @@ fun PuzzleClues(
item { item {
VerticalClue( VerticalClue(
modifier = Modifier.clickable { clue.isActive = false }, modifier = Modifier.clickable { clue.isActive = false },
clue = clue clue = clue.clue
) )
} }
} }
@@ -114,7 +125,7 @@ fun PuzzleClues(
modifier = Modifier modifier = Modifier
.alpha(0.5f) .alpha(0.5f)
.clickable { clue.isActive = true }, .clickable { clue.isActive = true },
clue = clue clue = clue.clue
) )
} }
} }

View File

@@ -1,12 +1,7 @@
package domain package domain
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
sealed class Clue { sealed class Clue {
abstract fun isRuleViolated(grid: Grid): Boolean abstract fun isRuleViolated(grid: Grid): Boolean
var isActive: Boolean by mutableStateOf(true)
} }
sealed class HorizontalClue : Clue() sealed class HorizontalClue : Clue()
@@ -58,6 +53,10 @@ class TripletClue<C : ItemClass<C>>(val a: Item<C>, val b: Item<C>, val c: Item<
val rowC = grid[cType.companion] val rowC = grid[cType.companion]
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)

View File

@@ -18,10 +18,6 @@ fun generateGame(size: Int = 6): Game {
// if there are 5 houses, there are 5 possible clues of the form "Person A lives in // if there are 5 houses, there are 5 possible clues of the form "Person A lives in
// house B", 8 possible clues of the form "Person A lives next to house B", and so on.) // house B", 8 possible clues of the form "Person A lives next to house B", and so on.)
var clues = getAllClues(grid).shuffled() var clues = getAllClues(grid).shuffled()
// var positionClues: MutableSet<PositionClue<out ItemClass<*>>> = grid.flatMap { row ->
// row.mapIndexed { i, item -> PositionClue(item, i) }
// }.toMutableSet()
var i = 0 var i = 0
@@ -51,13 +47,14 @@ private fun solve(
// First, set the positions of the items that are already known. // First, set the positions of the items that are already known.
clues.filterIsInstance<PositionClue<ItemClass<*>>>().forEach { position -> clues.filterIsInstance<PositionClue<ItemClass<*>>>().forEach { position ->
val row = grid[position.item.itemType.companion] val row = grid[position.item.itemType.companion]
val newSelections = mutableListOf<GameCell<ItemClass<*>>>()
row.forEachIndexed { index, gameCell -> row.forEachIndexed { index, gameCell ->
if (index == position.index) { if (index == position.index) {
gameCell.selection = position.item gameCell.selection = position.item
} else { newSelections.add(gameCell)
gameCell.options.remove(position.item)
} }
} }
newSelections.forEach { row.cleanupOptions(it) }
} }
// For each clue, remove any items that violate the clue. // For each clue, remove any items that violate the clue.
@@ -73,11 +70,10 @@ private fun solve(
} while (removedOptions) } while (removedOptions)
// If any cell has no items left, the puzzle has no solution. // If any cell has no items left, the puzzle has no solution.
grid.flatMap { it }.forEach { cell -> if (grid.flatMap { it }.any { cell -> cell.options.isEmpty() }) {
if (cell.options.isEmpty()) {
return NO_SOLUTION return NO_SOLUTION
} }
}
// If all cells have exactly one item left, the puzzle is solved. // If all cells have exactly one item left, the puzzle is solved.
if (grid.flatMap { it }.all { it.selection != null }) return SOLVABLE if (grid.flatMap { it }.all { it.selection != null }) return SOLVABLE
@@ -122,11 +118,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))
} // }
} // }
rows.forEach { columns -> rows.forEach { columns ->
columns.forEachIndexed { j, item -> columns.forEachIndexed { j, item ->

View File

@@ -64,7 +64,7 @@ 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) fun mayBe(item: Item<C>) = selection == item || (selection == null && options.contains(item))
} }
class Item<C : ItemClass<C>>( class Item<C : ItemClass<C>>(

View File

@@ -19,7 +19,7 @@ class TripletClueTest : ClueTest() {
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
.toEqual(false) .toEqual(false)
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
.toEqual(false) .toEqual(false)
} }
} }
@@ -27,6 +27,33 @@ class TripletClueTest : ClueTest() {
} }
} }
@Test
fun `ensure that middle might not be at the corners`() {
val grid = createGrid { null }
val rowA = grid.random()
val rowB = grid.random()
val rowC = grid.random()
val a = rowA[1]
val b = rowB[2]
val c = rowC[3]
rowB[0].selection = b.solution
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
.toEqual(true)
expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
.toEqual(true)
rowB[0].selection = null
rowB[grid.size - 1].selection = b.solution
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
.toEqual(true)
expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
.toEqual(true)
}
@Test @Test
fun `ensure grid where a or c and b are not neighbours is invalid`() { fun `ensure grid where a or c and b are not neighbours is invalid`() {
val grid = createGrid { null } val grid = createGrid { null }
@@ -78,7 +105,7 @@ class TripletClueTest : ClueTest() {
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)
} }
@@ -98,7 +125,7 @@ class TripletClueTest : ClueTest() {
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
.toEqual(false) .toEqual(false)
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
.toEqual(false) .toEqual(false)
} }
@@ -118,7 +145,7 @@ class TripletClueTest : ClueTest() {
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)
expect(TripletClue(a.solution, b.solution, c.solution).isRuleViolated(grid)) expect(TripletClue(c.solution, b.solution, a.solution).isRuleViolated(grid))
.toEqual(true) .toEqual(true)
} }
} }