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" }