diff --git a/.gitea/workflows/sonar.yaml b/.gitea/workflows/sonar.yaml index ce7dc5f..82c06d1 100644 --- a/.gitea/workflows/sonar.yaml +++ b/.gitea/workflows/sonar.yaml @@ -39,4 +39,4 @@ jobs: env: SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST }} - run: ./gradlew build sonar --info --build-cache + run: ./gradlew sonar diff --git a/android/ic_launcher_monochrome.svg b/android/ic_launcher_monochrome.svg new file mode 100644 index 0000000..4c3fa22 --- /dev/null +++ b/android/ic_launcher_monochrome.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 5a8fa95..7a14ae4 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" + android:usesCleartextTraffic="false" android:theme="@android:style/Theme.Material.Light.NoActionBar"> + + + + diff --git a/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd..1084c24 100644 --- a/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/commonUI/src/androidMain/kotlin/ch/dissem/yaep/ui/common/App.android.kt b/commonUI/src/androidMain/kotlin/ch/dissem/yaep/ui/common/App.android.kt index a0d195f..21afd4f 100644 --- a/commonUI/src/androidMain/kotlin/ch/dissem/yaep/ui/common/App.android.kt +++ b/commonUI/src/androidMain/kotlin/ch/dissem/yaep/ui/common/App.android.kt @@ -3,9 +3,13 @@ package ch.dissem.yaep.ui.common import ch.dissem.yaep.domain.Game import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext private val log = KotlinLogging.logger {} -actual fun CoroutineScope.logGame(game: Game) { - log.debug { "Game: $game" } +actual fun CoroutineScope.logGame(game: Game, dispatcher: CoroutineContext) { + launch(dispatcher) { + log.debug { "Game: $game" } + } } \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt index ae202a6..fc9143b 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt @@ -31,17 +31,23 @@ import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import ch.dissem.yaep.domain.Clue import ch.dissem.yaep.domain.Game +import ch.dissem.yaep.domain.GameCell +import ch.dissem.yaep.domain.GameRow import ch.dissem.yaep.domain.Grid import ch.dissem.yaep.domain.HorizontalClue +import ch.dissem.yaep.domain.Item +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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import org.jetbrains.compose.resources.painterResource import yaep.commonui.generated.resources.Res import yaep.commonui.generated.resources.neighbour import yaep.commonui.generated.resources.order +import kotlin.coroutines.CoroutineContext import kotlin.time.ExperimentalTime class DisplayClue(val clue: C) { @@ -144,59 +150,89 @@ fun PuzzleGrid( ) { Column(modifier = modifier) { for (row in grid) { - Row( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - ) { - val allOptions = row.options - for (item in row) { - var selection by remember(item) { mutableStateOf(item.selection) } - val options = remember(item) { - allOptions.map { Toggleable(it, item.options.contains(it)) } - } - LaunchedEffect(item) { - item.optionsChangedListeners.add { enabled -> - options.forEach { it.enabled = enabled.contains(it.item) } - } - item.selectionChangedListeners.add { - selection = it - onUpdate() - } - } - Selector( - modifier = Modifier - .padding(spacing) - .weight(1f), - spacing, - selectDirectly = selectDirectly, - options = options, - onOptionRemoved = { - grid.snapshot() - item.options.remove(it) - row.cleanupOptions() - }, - onOptionAdded = { - item.options.add(it) - }, - selectedItem = selection, - onSelectItem = { selectedItem -> - if (selectedItem != null) { - grid.snapshot() - item.selection = selectedItem - row.cleanupOptions() - } else { - while (item.selection != null) { - if (!grid.undo()) break - } - options.forEach { option -> - option.enabled = item.options.contains(option.item) - } - } - } - ) + PuzzleRow( + row = row, + onUpdate = onUpdate, + onSnapshot = { grid.snapshot() }, + onUndo = { grid.undo() }, + spacing = spacing, + selectDirectly = selectDirectly + ) + } + } +} + +@Composable +private fun PuzzleRow( + row: GameRow>, + onUpdate: () -> Unit, + onSnapshot: () -> Unit, + onUndo: () -> Boolean, + spacing: Dp, + selectDirectly: Boolean +) { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + val allOptions = row.options + for (cell in row) { + var selection by remember(cell) { mutableStateOf(cell.selection) } + val options = remember(cell) { + allOptions.map { Toggleable(it, cell.options.contains(it)) } + } + LaunchedEffect(cell) { + cell.optionsChangedListeners.add { enabled -> + options.forEach { it.enabled = enabled.contains(it.item) } + } + cell.selectionChangedListeners.add { + selection = it + onUpdate() } } + Selector( + modifier = Modifier + .padding(spacing) + .weight(1f), + spacing, + selectDirectly = selectDirectly, + options = options, + onOptionRemoved = { + onSnapshot() + cell.options.remove(it) + row.cleanupOptions() + }, + onOptionAdded = { + cell.options.add(it) + }, + selectedItem = selection, + onSelectItem = { selectedItem -> + onSelectItem(row, cell, options, selectedItem, onSnapshot, onUndo) + } + ) + } + } +} + +private fun onSelectItem( + row: GameRow>, + cell: GameCell>, + options: List>>>, + selectedItem: Item>?, + onSnapshot: () -> Unit, + onUndo: () -> Boolean +) { + if (selectedItem != null) { + onSnapshot() + cell.selection = selectedItem + row.cleanupOptions() + } else { + while (cell.selection != null) { + if (!onUndo()) break + } + options.forEach { option -> + option.enabled = cell.options.contains(option.item) } } } @@ -269,7 +305,7 @@ fun VerticalClue( } } -expect fun CoroutineScope.logGame(game: Game) +expect fun CoroutineScope.logGame(game: Game, dispatcher: CoroutineContext = Dispatchers.IO) @Composable fun ClueCard( diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/theme/Theme.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/theme/Theme.kt index 42f6d8f..aa81928 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/theme/Theme.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/theme/Theme.kt @@ -1,11 +1,10 @@ package ch.dissem.yaep.ui.common.theme + import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color private val lightScheme = lightColorScheme( primary = primaryLight, @@ -83,192 +82,19 @@ private val darkScheme = darkColorScheme( surfaceContainerHighest = surfaceContainerHighestDark, ) -private val mediumContrastLightColorScheme = lightColorScheme( - primary = primaryLightMediumContrast, - onPrimary = onPrimaryLightMediumContrast, - primaryContainer = primaryContainerLightMediumContrast, - onPrimaryContainer = onPrimaryContainerLightMediumContrast, - secondary = secondaryLightMediumContrast, - onSecondary = onSecondaryLightMediumContrast, - secondaryContainer = secondaryContainerLightMediumContrast, - onSecondaryContainer = onSecondaryContainerLightMediumContrast, - tertiary = tertiaryLightMediumContrast, - onTertiary = onTertiaryLightMediumContrast, - tertiaryContainer = tertiaryContainerLightMediumContrast, - onTertiaryContainer = onTertiaryContainerLightMediumContrast, - error = errorLightMediumContrast, - onError = onErrorLightMediumContrast, - errorContainer = errorContainerLightMediumContrast, - onErrorContainer = onErrorContainerLightMediumContrast, - background = backgroundLightMediumContrast, - onBackground = onBackgroundLightMediumContrast, - surface = surfaceLightMediumContrast, - onSurface = onSurfaceLightMediumContrast, - surfaceVariant = surfaceVariantLightMediumContrast, - onSurfaceVariant = onSurfaceVariantLightMediumContrast, - outline = outlineLightMediumContrast, - outlineVariant = outlineVariantLightMediumContrast, - scrim = scrimLightMediumContrast, - inverseSurface = inverseSurfaceLightMediumContrast, - inverseOnSurface = inverseOnSurfaceLightMediumContrast, - inversePrimary = inversePrimaryLightMediumContrast, - surfaceDim = surfaceDimLightMediumContrast, - surfaceBright = surfaceBrightLightMediumContrast, - surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, - surfaceContainerLow = surfaceContainerLowLightMediumContrast, - surfaceContainer = surfaceContainerLightMediumContrast, - surfaceContainerHigh = surfaceContainerHighLightMediumContrast, - surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, -) - -private val highContrastLightColorScheme = lightColorScheme( - primary = primaryLightHighContrast, - onPrimary = onPrimaryLightHighContrast, - primaryContainer = primaryContainerLightHighContrast, - onPrimaryContainer = onPrimaryContainerLightHighContrast, - secondary = secondaryLightHighContrast, - onSecondary = onSecondaryLightHighContrast, - secondaryContainer = secondaryContainerLightHighContrast, - onSecondaryContainer = onSecondaryContainerLightHighContrast, - tertiary = tertiaryLightHighContrast, - onTertiary = onTertiaryLightHighContrast, - tertiaryContainer = tertiaryContainerLightHighContrast, - onTertiaryContainer = onTertiaryContainerLightHighContrast, - error = errorLightHighContrast, - onError = onErrorLightHighContrast, - errorContainer = errorContainerLightHighContrast, - onErrorContainer = onErrorContainerLightHighContrast, - background = backgroundLightHighContrast, - onBackground = onBackgroundLightHighContrast, - surface = surfaceLightHighContrast, - onSurface = onSurfaceLightHighContrast, - surfaceVariant = surfaceVariantLightHighContrast, - onSurfaceVariant = onSurfaceVariantLightHighContrast, - outline = outlineLightHighContrast, - outlineVariant = outlineVariantLightHighContrast, - scrim = scrimLightHighContrast, - inverseSurface = inverseSurfaceLightHighContrast, - inverseOnSurface = inverseOnSurfaceLightHighContrast, - inversePrimary = inversePrimaryLightHighContrast, - surfaceDim = surfaceDimLightHighContrast, - surfaceBright = surfaceBrightLightHighContrast, - surfaceContainerLowest = surfaceContainerLowestLightHighContrast, - surfaceContainerLow = surfaceContainerLowLightHighContrast, - surfaceContainer = surfaceContainerLightHighContrast, - surfaceContainerHigh = surfaceContainerHighLightHighContrast, - surfaceContainerHighest = surfaceContainerHighestLightHighContrast, -) - -private val mediumContrastDarkColorScheme = darkColorScheme( - primary = primaryDarkMediumContrast, - onPrimary = onPrimaryDarkMediumContrast, - primaryContainer = primaryContainerDarkMediumContrast, - onPrimaryContainer = onPrimaryContainerDarkMediumContrast, - secondary = secondaryDarkMediumContrast, - onSecondary = onSecondaryDarkMediumContrast, - secondaryContainer = secondaryContainerDarkMediumContrast, - onSecondaryContainer = onSecondaryContainerDarkMediumContrast, - tertiary = tertiaryDarkMediumContrast, - onTertiary = onTertiaryDarkMediumContrast, - tertiaryContainer = tertiaryContainerDarkMediumContrast, - onTertiaryContainer = onTertiaryContainerDarkMediumContrast, - error = errorDarkMediumContrast, - onError = onErrorDarkMediumContrast, - errorContainer = errorContainerDarkMediumContrast, - onErrorContainer = onErrorContainerDarkMediumContrast, - background = backgroundDarkMediumContrast, - onBackground = onBackgroundDarkMediumContrast, - surface = surfaceDarkMediumContrast, - onSurface = onSurfaceDarkMediumContrast, - surfaceVariant = surfaceVariantDarkMediumContrast, - onSurfaceVariant = onSurfaceVariantDarkMediumContrast, - outline = outlineDarkMediumContrast, - outlineVariant = outlineVariantDarkMediumContrast, - scrim = scrimDarkMediumContrast, - inverseSurface = inverseSurfaceDarkMediumContrast, - inverseOnSurface = inverseOnSurfaceDarkMediumContrast, - inversePrimary = inversePrimaryDarkMediumContrast, - surfaceDim = surfaceDimDarkMediumContrast, - surfaceBright = surfaceBrightDarkMediumContrast, - surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, - surfaceContainerLow = surfaceContainerLowDarkMediumContrast, - surfaceContainer = surfaceContainerDarkMediumContrast, - surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, - surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, -) - -private val highContrastDarkColorScheme = darkColorScheme( - primary = primaryDarkHighContrast, - onPrimary = onPrimaryDarkHighContrast, - primaryContainer = primaryContainerDarkHighContrast, - onPrimaryContainer = onPrimaryContainerDarkHighContrast, - secondary = secondaryDarkHighContrast, - onSecondary = onSecondaryDarkHighContrast, - secondaryContainer = secondaryContainerDarkHighContrast, - onSecondaryContainer = onSecondaryContainerDarkHighContrast, - tertiary = tertiaryDarkHighContrast, - onTertiary = onTertiaryDarkHighContrast, - tertiaryContainer = tertiaryContainerDarkHighContrast, - onTertiaryContainer = onTertiaryContainerDarkHighContrast, - error = errorDarkHighContrast, - onError = onErrorDarkHighContrast, - errorContainer = errorContainerDarkHighContrast, - onErrorContainer = onErrorContainerDarkHighContrast, - background = backgroundDarkHighContrast, - onBackground = onBackgroundDarkHighContrast, - surface = surfaceDarkHighContrast, - onSurface = onSurfaceDarkHighContrast, - surfaceVariant = surfaceVariantDarkHighContrast, - onSurfaceVariant = onSurfaceVariantDarkHighContrast, - outline = outlineDarkHighContrast, - outlineVariant = outlineVariantDarkHighContrast, - scrim = scrimDarkHighContrast, - inverseSurface = inverseSurfaceDarkHighContrast, - inverseOnSurface = inverseOnSurfaceDarkHighContrast, - inversePrimary = inversePrimaryDarkHighContrast, - surfaceDim = surfaceDimDarkHighContrast, - surfaceBright = surfaceBrightDarkHighContrast, - surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, - surfaceContainerLow = surfaceContainerLowDarkHighContrast, - surfaceContainer = surfaceContainerDarkHighContrast, - surfaceContainerHigh = surfaceContainerHighDarkHighContrast, - surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, -) - -@Immutable -data class ColorFamily( - val color: Color, - val onColor: Color, - val colorContainer: Color, - val onColorContainer: Color -) - -val unspecified_scheme = ColorFamily( - Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified -) - @Composable fun AppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit ) { - val colorScheme = when { - darkTheme -> darkScheme - else -> lightScheme - } -// val view = LocalView.current -// if (!view.isInEditMode) { -// SideEffect { -// val window = (view.context as Activity).window -// window.statusBarColor = colorScheme.primary.toArgb() -// WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme -// } -// } + val colorScheme = when { + darkTheme -> darkScheme + else -> lightScheme + } - MaterialTheme( - colorScheme = colorScheme, -// typography = AppTypography, - content = content - ) + MaterialTheme( + colorScheme = colorScheme, + content = content + ) } diff --git a/commonUI/src/jvmMain/kotlin/ch/dissem/yaep/ui/common/App.jvm.kt b/commonUI/src/jvmMain/kotlin/ch/dissem/yaep/ui/common/App.jvm.kt index e1ea328..f30abc2 100644 --- a/commonUI/src/jvmMain/kotlin/ch/dissem/yaep/ui/common/App.jvm.kt +++ b/commonUI/src/jvmMain/kotlin/ch/dissem/yaep/ui/common/App.jvm.kt @@ -3,8 +3,8 @@ package ch.dissem.yaep.ui.common import ch.dissem.yaep.domain.Game import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext import kotlin.io.path.Path import kotlin.io.path.createFile import kotlin.io.path.isDirectory @@ -15,8 +15,8 @@ import kotlin.time.ExperimentalTime private val log = KotlinLogging.logger {} @OptIn(ExperimentalTime::class) -actual fun CoroutineScope.logGame(game: Game) { - launch(Dispatchers.IO) { +actual fun CoroutineScope.logGame(game: Game, dispatcher: CoroutineContext) { + launch(dispatcher) { val dirName = """${System.getProperty("user.home")}/.yaep""" val dir = Path(dirName) if (dir.isDirectory()) { diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameCell.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameCell.kt index e5aada4..dca85dd 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameCell.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameCell.kt @@ -19,7 +19,7 @@ class GameCell>( selectionChangedListeners.forEach { listener -> listener(value) } } } - val options: ObservableSet> = ObservableSet(options) { before, after -> + val options: ObservableSet> = ObservableSet(options) { _, after -> optionsChangedListeners.forEach { listener -> listener(after) } diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt index 3befc53..b87e9c7 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt @@ -37,7 +37,7 @@ class GameRow>( it.options.clear() it.options.add(it.selection!!) } - cellsWithSelection.mapNotNull { it.selection }.toMutableSet() + cellsWithSelection.mapNotNull { it.selection }.toSet() } filter { it.selection == null } .forEach { it.options.removeAll(selections) } diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/clues.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/clues.kt index 0f19db0..b1c0e88 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/clues.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/clues.kt @@ -75,10 +75,12 @@ class NeighbourClue, B : ItemClass>(val a: Item, val b: I for (iX in rowX.indices) { val cellX = rowX[iX] - if (cellX.mayBe(x, mayHaveSelection = false)) { - if (!rowY.getOrNull(iX - 1).mayBe(y) && !rowY.getOrNull(iX + 1).mayBe(y)) { - removed = cellX.options.remove(x) || removed - } + if ( + cellX.mayBe(x, mayHaveSelection = false) + && !rowY.getOrNull(iX - 1).mayBe(y) + && !rowY.getOrNull(iX + 1).mayBe(y) + ) { + removed = cellX.options.remove(x) || removed } } @@ -185,51 +187,71 @@ class TripletClue, B : ItemClass, C : ItemClass>( val ic by lazy { rowC.indexOf(cType) } if (ia != -1) { - return when (ib) { - -1 -> when (ic) { - -1 -> (rowB.getOrNull(ia - 1).hasNoSelection() && rowC.getOrNull(ia - 2) - .hasNoSelection()) || - (rowB.getOrNull(ia + 1).hasNoSelection() && rowC.getOrNull(ia + 2) - .hasNoSelection()) - - ia - 2 -> rowB.getOrNull(ia - 1).hasNoSelection() - ia + 2 -> rowB.getOrNull(ia + 1).hasNoSelection() - else -> false - } - - ia - 1 -> when (ic) { - -1 -> rowC.getOrNull(ia - 2).hasNoSelection() - ia - 2 -> true - else -> false - } - - ia + 1 -> when (ic) { - -1 -> rowC.getOrNull(ia + 2).hasNoSelection() - ia + 2 -> true - else -> false - } - - else -> false - } + return isValidWithASet(ia, ib, ic, rowB, rowC) } if (ib != -1) { - when (ic) { - -1 -> return (rowA.getOrNull(ib - 1).hasNoSelection() && rowC.getOrNull(ib + 1) - .hasNoSelection()) || - (rowA.getOrNull(ib + 1).hasNoSelection() && rowC.getOrNull(ib - 1) - .hasNoSelection()) - - ib - 1 -> return rowA.getOrNull(ib + 1).hasNoSelection() - ib + 1 -> return rowA.getOrNull(ib - 1).hasNoSelection() - } + return isValidWithBSet(ib, ic, rowA, rowC) } if (ic != -1) { - return (rowB.getOrNull(ic - 1).hasNoSelection() && rowA.getOrNull(ic - 2) - .hasNoSelection()) || - (rowB.getOrNull(ic + 1).hasNoSelection() && rowA.getOrNull(ic + 2) - .hasNoSelection()) + return isValidWithCSet(ic, rowA, rowB) } - return rowA.mapIndexed { index, gameCell -> if (gameCell.hasNoSelection()) index else null } + return isValidWithNoneSet(rowA, rowB, rowC) + } + + private fun isValidWithASet( + ia: Int, + ib: Int, + ic: Int, + rowB: GameRow, + rowC: GameRow + ): Boolean = + when (ib) { + -1 -> when (ic) { + -1 -> (rowB.getOrNull(ia - 1).hasNoSelection() && rowC.getOrNull(ia - 2) + .hasNoSelection()) || + (rowB.getOrNull(ia + 1).hasNoSelection() && rowC.getOrNull(ia + 2) + .hasNoSelection()) + + ia - 2 -> rowB.getOrNull(ia - 1).hasNoSelection() + ia + 2 -> rowB.getOrNull(ia + 1).hasNoSelection() + else -> false + } + + ia - 1 -> when (ic) { + -1 -> rowC.getOrNull(ia - 2).hasNoSelection() + ia - 2 -> true + else -> false + } + + ia + 1 -> when (ic) { + -1 -> rowC.getOrNull(ia + 2).hasNoSelection() + ia + 2 -> true + else -> false + } + + else -> false + } + + private fun isValidWithBSet(ib: Int, ic: Int, rowA: GameRow, rowC: GameRow): Boolean = + when (ic) { + -1 -> (rowA.getOrNull(ib - 1).hasNoSelection() && rowC.getOrNull(ib + 1) + .hasNoSelection()) || + (rowA.getOrNull(ib + 1).hasNoSelection() && rowC.getOrNull(ib - 1) + .hasNoSelection()) + + ib - 1 -> rowA.getOrNull(ib + 1).hasNoSelection() + ib + 1 -> rowA.getOrNull(ib - 1).hasNoSelection() + else -> false + } + + private fun isValidWithCSet(ic: Int, rowA: GameRow, rowB: GameRow): Boolean = + (rowB.getOrNull(ic - 1).hasNoSelection() && rowA.getOrNull(ic - 2) + .hasNoSelection()) || + (rowB.getOrNull(ic + 1).hasNoSelection() && rowA.getOrNull(ic + 2) + .hasNoSelection()) + + private fun isValidWithNoneSet(rowA: GameRow, rowB: GameRow, rowC: GameRow): Boolean = + rowA.mapIndexed { index, gameCell -> if (gameCell.hasNoSelection()) index else null } .filterNotNull() .any { index -> (rowB.getOrNull(index - 1).hasNoSelection() && rowC.getOrNull(index - 2) @@ -237,7 +259,6 @@ class TripletClue, B : ItemClass, C : ItemClass>( (rowB.getOrNull(index + 1).hasNoSelection() && rowC.getOrNull(index + 2) .hasNoSelection()) } - } override fun removeForbiddenOptions(grid: Grid): Boolean { val rowA = grid[aType.companion] @@ -247,49 +268,62 @@ class TripletClue, B : ItemClass, C : ItemClass>( var removed = false for (i in rowA.indices) { - val cellA = rowA[i] - if (cellA.mayBe(a, mayHaveSelection = false)) { - val cellBR = rowB.getOrNull(i + 1) - val cellCR = rowC.getOrNull(i + 2) - val cellBL = rowB.getOrNull(i - 1) - val cellCL = rowC.getOrNull(i - 2) - - if (!(cellBR.mayBe(b) && cellCR.mayBe(c)) && !(cellBL.mayBe(b) && cellCL.mayBe(c))) { - removed = cellA.options.remove(a) || removed - } - } + removed = removeForbiddenOptionsGivenX( + i = i, + x = Group(a, rowA), + y = Group(b, rowB, 1), + z = Group(c, rowC, 2) + ) || removed } for (i in rowB.indices) { - val cellB = rowB[i] - if (cellB.mayBe(b, mayHaveSelection = false)) { - val cellAL = rowA.getOrNull(i - 1) - val cellAR = rowA.getOrNull(i + 1) - val cellCL = rowC.getOrNull(i - 1) - val cellCR = rowC.getOrNull(i + 1) - - if (!(cellAL.mayBe(a) && cellCR.mayBe(c)) && !(cellCL.mayBe(c) && cellAR.mayBe(a))) { - removed = cellB.options.remove(b) || removed - } - } + removed = removeForbiddenOptionsGivenX( + i = i, + x = Group(b, rowB), + y = Group(a, rowA, 1), + z = Group(c, rowC, -1) + ) || removed } for (i in rowC.indices) { - val cellC = rowC[i] - if (cellC.mayBe(c, mayHaveSelection = false)) { - val cellBR = rowB.getOrNull(i + 1) - val cellAR = rowA.getOrNull(i + 2) - val cellBL = rowB.getOrNull(i - 1) - val cellAL = rowA.getOrNull(i - 2) - - if (!(cellBR.mayBe(b) && cellAR.mayBe(a)) && !(cellBL.mayBe(b) && cellAL.mayBe(a))) { - removed = cellC.options.remove(c) || removed - } - } + removed = removeForbiddenOptionsGivenX( + i = i, + x = Group(c, rowC), + y = Group(b, rowB, 1), + z = Group(a, rowA, 2) + ) || removed } - return removed } + private fun , Y : ItemClass, Z : ItemClass> removeForbiddenOptionsGivenX( + i: Int, + x: Group, + y: Group, + z: Group + ): Boolean { + val cellX = x.row[i] + if (cellX.mayBe(x.item, mayHaveSelection = false)) { + val cellYR = y.row.getOrNull(i + y.offset) + val cellZR = z.row.getOrNull(i + z.offset) + val cellYL = y.row.getOrNull(i - y.offset) + val cellZL = z.row.getOrNull(i - z.offset) + + if ( + !(cellYR.mayBe(y.item) && cellZR.mayBe(z.item)) + && !(cellYL.mayBe(y.item) && cellZL.mayBe(z.item)) + ) { + return cellX.options.remove(x.item) + } + } + return false + } + + private class Group>( + val item: Item, + val row: GameRow, + val offset: Int = 0 + ) + override fun toString(): String = "$bType is between the neighbours $aType and $cType to both sides" @@ -343,15 +377,11 @@ class SameColumnClue, B : ItemClass>(val a: Item, val b: val cellA = rowA[i] val cellB = rowB[i] - if (cellB.hasNoSelection()) { - if (!cellA.mayBe(a)) { - removed = cellB.options.remove(b) || removed - } + if (cellB.hasNoSelection() && !cellA.mayBe(a)) { + removed = cellB.options.remove(b) || removed } - if (cellA.hasNoSelection()) { - if (!cellB.mayBe(b)) { - removed = cellA.options.remove(a) || removed - } + if (cellA.hasNoSelection() && !cellB.mayBe(b)) { + removed = cellA.options.remove(a) || removed } } return removed diff --git a/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameSolverTest.kt b/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameSolverTest.kt index 3adf179..9cce6a4 100644 --- a/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameSolverTest.kt +++ b/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameSolverTest.kt @@ -14,7 +14,7 @@ class GameSolverTest { fun `ensure there are no unnecessary clues`() { var game: Game var neighbours: List> - repeat(100) { + repeat(10) { game = generateGame() val triplets = game.horizontalClues.filterIsInstance>() neighbours = game.horizontalClues.filterIsInstance>() diff --git a/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameTest.kt b/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameTest.kt index 112ad68..d7df4c5 100644 --- a/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameTest.kt +++ b/domain/src/commonTest/kotlin/ch/dissem/yaep/domain/GameTest.kt @@ -19,7 +19,7 @@ class GameTest { @Test fun `ensure generated games are solvable`() { - val tries = 1000 + val tries = 100 var fastest = 500.milliseconds var slowest = 0.milliseconds var total = 0.milliseconds diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5650832..54f0fd5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] app-version-code = "1" app-version-name = "1.0.0" -agp = "8.11.1" +agp = "8.12.0" jdk = "21" android-compileSdk = "36" android-minSdk = "26" @@ -22,7 +22,7 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl atrium = { module = "ch.tutteli.atrium:atrium-fluent", version.ref = "atrium" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.2" } -logging-jvm = { module = "io.github.oshai:kotlin-logging-jvm", version = "7.0.7" } +logging-jvm = { module = "io.github.oshai:kotlin-logging-jvm", version = "7.0.11" } logging-slf4j = { module = "org.slf4j:slf4j-simple", version = "2.0.17" } [bundles] @@ -35,6 +35,6 @@ android-library = { id = "com.android.library", version.ref = "agp" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -sonarqube = { id = "org.sonarqube", version = "6.2.0.5505" } compose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +sonarqube = { id = "org.sonarqube", version = "6.2.0.5505" }