Sonar fixes
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 3m58s

This commit is contained in:
2025-07-19 00:04:19 +02:00
committed by Christian Basler
parent 1527ab0cb0
commit 6d58b93477
15 changed files with 339 additions and 335 deletions

View File

@@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -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">
<activity
android:exported="true"

View File

@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:scaleX="2.8"
android:scaleY="2.8"
android:translateX="18"
android:translateY="18"
>
<path
android:fillColor="#464c4f"
android:pathData="m15.848,20.078c-0.34,2.82 -0.74,5.473 -1.078,8.299 -0.082,0.685 12.73,0.217 13.129,-0.182 0.22,-0.22 0.288,-1.278 0.143,-2.785C30.72,18.261 26.815,5.37 17.301,3.861 14.319,3.388 10.718,4.395 9.119,5.395c-0.432,0.27 2.131,2.26 3.264,3.248 -1.393,0.789 -2.973,2.554 -5.869,6.248 -0.435,0.555 -0.754,1.179 -1.18,1.586 -1.503,1.437 -2.891,3.476 -1.77,5.533 0.602,1.105 1.972,1.791 3.6,1.221 1.235,-0.432 1.905,-1.018 2.204,-1.54 3.218,0.401 5.569,-0.974 6.48,-1.613zM14.951,3.875 L15.354,5.457c-0.339,0.555 -0.651,1.288 -0.883,2.252L12.234,4.313c0.909,-0.242 1.814,-0.387 2.717,-0.438zM18.176,4.141c0.839,0.198 1.62,0.476 2.342,0.836L19.123,7.887C18.933,6.332 18.452,5.269 18.014,4.617ZM17.191,4.691c0.227,-0.012 1.371,1.696 1.283,3.701 -0.061,1.383 -0.51,1.544 -0.885,2.045 -0.702,0.936 -0.712,2.299 -0.305,2.594 0.972,0.194 1.433,-1.881 1.758,-2.504 0.679,0.254 0.832,0.325 1.209,0.662 -0.972,2.388 -2.752,7.249 -2.604,7.424 0.1,0.117 1.808,-0.833 5.93,-3.836 0.2,0.224 0.427,0.598 0.539,0.873C21.871,17.673 19.505,19.528 16.797,20.9 17.605,16.232 16.188,16.421 15.975,14.545 15.839,13.35 16.679,11.953 15.818,11.922c-0.999,0.288 -1.077,1.635 -1.291,2.48 -0.341,1.732 1.656,1.887 0.977,5.033l-0.744,0.346c0,0 1.139,-2.838 -1.576,-3.803 -2.015,-0.716 -3.539,0.345 -4.195,0.941 0.132,-0.444 0.169,-0.925 -0.062,-1.357 0.483,-2.284 2.414,-3.303 2.543,-4.719 0.153,-0.7 0.422,-1.248 1.127,-1.504 0,0 -0.042,0.871 0.07,1.719 0.087,0.646 0.9,0.764 1.398,0.387 0.498,-0.377 0.707,-2.645 0.707,-2.645l0.438,-0.162c0.207,-1.525 0.587,-3.062 1.982,-3.947zM22.289,6.064c0.432,0.325 0.825,0.657 1.178,0.996L20.771,10.627c-0.404,-0.283 -0.835,-0.531 -1.279,-0.758zM17.01,6.15c-0.528,-0.057 -1.844,3.645 -0.291,3.645 1.319,0 0.734,-3.596 0.291,-3.645zM24.773,8.498c0.466,0.59 0.887,1.208 1.266,1.854L23.252,12.949C22.867,12.483 22.46,12.039 22.02,11.631ZM9.289,12.07c0.365,0.524 -0.197,1.322 -1.295,2.693 -0.243,-0.105 -0.495,-0.164 -0.732,-0.162zM27.061,12.361c0.182,0.421 0.344,0.836 0.488,1.244l-2.729,1.611c-0.197,-0.338 -0.399,-0.672 -0.617,-0.996zM21.586,12.43c0.165,0.089 0.447,0.271 1.1,1.08 0,0 -2.18,1.749 -2.217,1.617 -0.037,-0.132 1.117,-2.697 1.117,-2.697zM12.967,12.664c-0.509,0 -0.92,0.493 -0.92,1.1 0,0.607 0.419,1.103 0.926,1.148 0.681,0.061 0.914,-0.542 0.914,-1.148 0,-0.607 -0.411,-1.1 -0.92,-1.1zM28.035,15.168c0.121,0.452 0.226,0.906 0.316,1.363l-2.301,1.162c-0.165,-0.389 -0.34,-0.773 -0.527,-1.156zM24.896,17.115c0.284,0.448 0.487,0.87 0.611,1.33 -0.278,0.41 -3.469,4.963 -9.748,7.367l0.182,-1.662C19.922,22.311 20.378,21.914 24.896,17.115ZM13.398,20.525c-0.448,0.227 -0.729,0.283 -1.18,0.361C12.542,18.162 11.139,18.184 9.018,19.371 8.838,19.169 8.657,19.017 8.514,18.885 11.008,16.534 13.703,16.865 13.398,20.525ZM28.65,18.596c0.041,0.457 0.065,0.882 0.074,1.277l-1.672,0.613c-0.114,-0.387 -0.243,-0.775 -0.379,-1.168zM5.965,19.559c0.242,0.744 -0.264,1.527 -0.66,1.455 -0.397,-0.072 -0.041,-0.684 -0.193,-0.998 -0.153,-0.315 -0.609,-0.253 -0.57,-0.59 0.066,-0.57 1.248,-0.409 1.424,0.133zM26.191,20.109c0.103,0.508 0.238,0.831 0.318,1.252 -1.821,2.749 -3.408,4.952 -6.684,6.637 -1.046,0.056 -2.087,0.012 -3.197,-0.078 1.058,-1.058 2.314,0.688 9.562,-7.811zM10.959,21.084c-0.512,0.033 -0.922,0.016 -1.449,0 0.017,-0.251 0.01,-0.473 -0.018,-0.666 0.872,-0.527 1.112,-0.04 1.467,0.666zM28.598,22.441c-0.101,0.856 -0.321,1.712 -0.617,2.568 -0.086,-0.749 -0.206,-1.559 -0.4,-2.461zM26.939,23.785c0.298,1.288 0.41,2.528 0.336,3.723 -0.776,0.117 -1.691,0.189 -2.746,0.213 1.162,-1.58 1.291,-1.953 2.41,-3.936z" />
</group>
</vector>

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

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

View File

@@ -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<C : Clue>(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<ItemClass<*>>,
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<ItemClass<*>>,
cell: GameCell<ItemClass<*>>,
options: List<Toggleable<Item<ItemClass<*>>>>,
selectedItem: Item<ItemClass<*>>?,
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(

View File

@@ -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
)
}

View File

@@ -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()) {

View File

@@ -19,7 +19,7 @@ class GameCell<C : ItemClass<C>>(
selectionChangedListeners.forEach { listener -> listener(value) }
}
}
val options: ObservableSet<Item<C>> = ObservableSet(options) { before, after ->
val options: ObservableSet<Item<C>> = ObservableSet(options) { _, after ->
optionsChangedListeners.forEach { listener ->
listener(after)
}

View File

@@ -37,7 +37,7 @@ class GameRow<C : ItemClass<C>>(
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) }

View File

@@ -75,10 +75,12 @@ class NeighbourClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, 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<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
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<B>,
rowC: GameRow<C>
): 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<A>, rowC: GameRow<C>): 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<A>, rowB: GameRow<B>): 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<A>, rowB: GameRow<B>, rowC: GameRow<C>): 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<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
(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<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
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 <X : ItemClass<X>, Y : ItemClass<Y>, Z : ItemClass<Z>> removeForbiddenOptionsGivenX(
i: Int,
x: Group<X>,
y: Group<Y>,
z: Group<Z>
): 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<T : ItemClass<T>>(
val item: Item<T>,
val row: GameRow<T>,
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<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, 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

View File

@@ -14,7 +14,7 @@ class GameSolverTest {
fun `ensure there are no unnecessary clues`() {
var game: Game
var neighbours: List<NeighbourClue<*, *>>
repeat(100) {
repeat(10) {
game = generateGame()
val triplets = game.horizontalClues.filterIsInstance<TripletClue<*, *, *>>()
neighbours = game.horizontalClues.filterIsInstance<NeighbourClue<*, *>>()

View File

@@ -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

View File

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