diff --git a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt index d3ed892..5040319 100644 --- a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt +++ b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/App.kt @@ -1,21 +1,45 @@ package ch.dissem.yaep.ui.common import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.PointerMatcher +import androidx.compose.foundation.PointerMatcher.Companion.mouse +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.onClick import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.draw.alpha import androidx.compose.ui.draw.shadow +import androidx.compose.ui.input.pointer.PointerButton.Companion.Secondary import androidx.compose.ui.unit.dp -import ch.dissem.yaep.domain.* +import ch.dissem.yaep.domain.Clue +import ch.dissem.yaep.domain.Game +import ch.dissem.yaep.domain.Grid +import ch.dissem.yaep.domain.HorizontalClue +import ch.dissem.yaep.domain.ItemClass +import ch.dissem.yaep.domain.NeighbourClue +import ch.dissem.yaep.domain.OrderClue +import ch.dissem.yaep.domain.SameColumnClue +import ch.dissem.yaep.domain.TripletClue +import ch.dissem.yaep.domain.generateGame import org.jetbrains.compose.resources.painterResource import yaep.commonui.generated.resources.Res import yaep.commonui.generated.resources.neighbour @@ -93,10 +117,15 @@ fun PuzzleGrid( item.options.remove(it) row.cleanupOptions() }, + onOptionAdded = { + item.options.add(it) + }, selectedItem = selection, onSelectItem = { item.selection = it - row.cleanupOptions() + if (it != null) { + row.cleanupOptions() + } } ) } @@ -147,10 +176,11 @@ fun PuzzleClues( } } +@OptIn(ExperimentalFoundationApi::class) private fun Modifier.forClue(clue: DisplayClue) = this .alpha(if (clue.isActive) 1f else 0.2f) .padding(8.dp) - .clickable { clue.isActive = false } + .onClick(matcher = PointerMatcher.Primary + mouse(Secondary)) { clue.isActive = !clue.isActive } @Composable fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue, isClueViolated: Boolean) { @@ -191,7 +221,11 @@ fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue, isClueVi } @Composable -fun VerticalClue(modifier: Modifier = Modifier, clue: SameColumnClue<*, *>, isClueViolated: Boolean = false) { +fun VerticalClue( + modifier: Modifier = Modifier, + clue: SameColumnClue<*, *>, + isClueViolated: Boolean = false +) { ClueCard( modifier = modifier.aspectRatio(0.5f), isClueViolated = isClueViolated @@ -204,11 +238,20 @@ fun VerticalClue(modifier: Modifier = Modifier, clue: SameColumnClue<*, *>, isCl } @Composable -fun ClueCard(modifier: Modifier = Modifier, isClueViolated: Boolean, content: @Composable () -> Unit) { +fun ClueCard( + modifier: Modifier = Modifier, + isClueViolated: Boolean, + content: @Composable () -> Unit +) { val colors = MaterialTheme.colorScheme OutlinedCard( modifier = if (isClueViolated) { - modifier.shadow(8.dp, shape = CardDefaults.outlinedShape, ambientColor = colors.error, spotColor = colors.error) + modifier.shadow( + 8.dp, + shape = CardDefaults.outlinedShape, + ambientColor = colors.error, + spotColor = colors.error + ) } else { modifier }, diff --git a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/selector.kt b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/selector.kt index ab80877..54cfcd7 100644 --- a/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/selector.kt +++ b/commonUI/src/main/kotlin/ch/dissem/yaep/ui/common/selector.kt @@ -28,7 +28,8 @@ import kotlin.math.min fun > Selector( modifier: Modifier = Modifier, options: List>>, - onOptionRemoved: (Item?) -> Unit, + onOptionRemoved: (Item) -> Unit, + onOptionAdded: (Item) -> Unit, selectedItem: Item?, onSelectItem: (Item?) -> Unit, ) { @@ -54,8 +55,13 @@ fun > Selector( .onClick( matcher = PointerMatcher.mouse(PointerButton.Secondary), onClick = { - option.enabled = false - onOptionRemoved(option.item) + if (option.enabled) { + option.enabled = false + onOptionRemoved(option.item) + } else { + option.enabled = true + onOptionAdded(option.item) + } } ) ) diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/GameRow.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/GameRow.kt index 074ebaa..23d3893 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/GameRow.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/GameRow.kt @@ -8,18 +8,49 @@ class GameRow>( fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element } fun cleanupOptions() { - cells.forEach { cleanupOptions(it) } + cells.forEach { + cleanupOptions(it) + } +// do { +// var selectedSingleOption = false +// cells.forEach { cleanupOptions(it) } +// val selections = cells.mapNotNull { it.selection } +// options +// .filter { !selections.contains(it) } +// .forEach { option -> +// if (cells.count { cell -> cell.options.contains(option) } == 1) { +// cells +// .filter { it.selection == null } +// .first { cell -> cell.options.contains(option) } +// .let { it.selection = option } +// selectedSingleOption = true +// } +// } +// } while (selectedSingleOption) } - private fun cleanupOptions(cell: GameCell, firstCall: Boolean = true) { - if ((firstCall && cell.selection != null) || (cell.options.size == 1 && cell.selection == null)) { - if (cell.selection == null) { + private fun cleanupOptions(cell: GameCell, justSelected: Boolean = true) { + if ((justSelected && cell.selection != null) || (cell.options.size == 1 && cell.selection == null)) { + val selection = cell.selection + if (selection == null) { cell.selection = cell.options.first() + } else { + cell.options.clear() + cell.options.add(selection) } - filter { otherCell -> otherCell != cell && otherCell.selection == null }.forEach { otherCell -> + filter { otherCell -> otherCell != cell && otherCell.hasNoSelection() }.forEach { otherCell -> otherCell.options.remove(cell.selection) cleanupOptions(otherCell, false) } + filter { it.selection == null } + .flatMap { c -> c.options.map { o -> o to c } } + .groupBy { it.first } + .filter { it.value.size == 1 } + .forEach { + val c = it.value.single().second + c.selection = it.key + cleanupOptions(c, true) + } } } diff --git a/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt b/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt index 41f5f11..fa75ffd 100644 --- a/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt +++ b/domain/src/main/kotlin/ch/dissem/yaep/domain/clues.kt @@ -55,10 +55,9 @@ class NeighbourClue, B : ItemClass>(val a: Item, val b: I for (iX in rowX.indices) { val cellX = rowX[iX] - if (cellX.options.contains(x)) { + if (cellX.mayBe(x, mayHaveSelection = false)) { if (!rowY.getOrNull(iX - 1).mayBe(y) && !rowY.getOrNull(iX + 1).mayBe(y)) { - cellX.options.remove(x) - removed = true + removed = cellX.options.remove(x) || removed } } } @@ -102,8 +101,11 @@ class OrderClue, R : ItemClass>(val left: Item, val right var removed = false try { rowL.takeLast(rowL.size - lastR) + .filter { it.selection == null } .forEach { removed = it.options.remove(left) || removed } - rowR.take(firstL + 1).forEach { removed = it.options.remove(right) || removed } + rowR.take(firstL + 1) + .filter { it.selection == null } + .forEach { removed = it.options.remove(right) || removed } } catch (e: IllegalArgumentException) { throw UnsolvablePuzzleException(e) } @@ -274,11 +276,15 @@ class SameColumnClue, B : ItemClass>(val a: Item, val b: val cellA = rowA[i] val cellB = rowB[i] - if (!cellA.mayBe(a)) { - removed = cellB.options.remove(b) || removed + if (cellB.hasNoSelection()) { + if (!cellA.mayBe(a)) { + removed = cellB.options.remove(b) || removed + } } - if (!cellB.mayBe(b)) { - removed = cellA.options.remove(a) || removed + if (cellA.hasNoSelection()) { + if (!cellB.mayBe(b)) { + removed = cellA.options.remove(a) || removed + } } } return removed diff --git a/domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt b/domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt index 93fbe81..4a0c647 100644 --- a/domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt +++ b/domain/src/test/kotlin/ch/dissem/yaep/domain/GameTest.kt @@ -29,6 +29,7 @@ class GameTest { val time = measureTime { game = generateGame() } + println("Generated game #$i in ${time.inWholeMilliseconds}ms") expect(solve(game.grid, game.clues)).toEqual(SOLVABLE) expect(time).toBeLessThan(500.milliseconds) if (time < fastest) {