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: env:
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST }} 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:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:usesCleartextTraffic="false"
android:theme="@android:style/Theme.Material.Light.NoActionBar"> android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity <activity
android:exported="true" 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"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon> </adaptive-icon>

View File

@@ -3,9 +3,13 @@ package ch.dissem.yaep.ui.common
import ch.dissem.yaep.domain.Game import ch.dissem.yaep.domain.Game
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
actual fun CoroutineScope.logGame(game: Game) { actual fun CoroutineScope.logGame(game: Game, dispatcher: CoroutineContext) {
launch(dispatcher) {
log.debug { "Game: $game" } log.debug { "Game: $game" }
} }
}

View File

@@ -31,17 +31,23 @@ import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.Clue import ch.dissem.yaep.domain.Clue
import ch.dissem.yaep.domain.Game 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.Grid
import ch.dissem.yaep.domain.HorizontalClue 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.NeighbourClue
import ch.dissem.yaep.domain.OrderClue import ch.dissem.yaep.domain.OrderClue
import ch.dissem.yaep.domain.SameColumnClue import ch.dissem.yaep.domain.SameColumnClue
import ch.dissem.yaep.domain.TripletClue import ch.dissem.yaep.domain.TripletClue
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import yaep.commonui.generated.resources.Res import yaep.commonui.generated.resources.Res
import yaep.commonui.generated.resources.neighbour import yaep.commonui.generated.resources.neighbour
import yaep.commonui.generated.resources.order import yaep.commonui.generated.resources.order
import kotlin.coroutines.CoroutineContext
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
class DisplayClue<C : Clue>(val clue: C) { class DisplayClue<C : Clue>(val clue: C) {
@@ -144,22 +150,43 @@ fun PuzzleGrid(
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
for (row in grid) { for (row in grid) {
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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
) { ) {
val allOptions = row.options val allOptions = row.options
for (item in row) { for (cell in row) {
var selection by remember(item) { mutableStateOf(item.selection) } var selection by remember(cell) { mutableStateOf(cell.selection) }
val options = remember(item) { val options = remember(cell) {
allOptions.map { Toggleable(it, item.options.contains(it)) } allOptions.map { Toggleable(it, cell.options.contains(it)) }
} }
LaunchedEffect(item) { LaunchedEffect(cell) {
item.optionsChangedListeners.add { enabled -> cell.optionsChangedListeners.add { enabled ->
options.forEach { it.enabled = enabled.contains(it.item) } options.forEach { it.enabled = enabled.contains(it.item) }
} }
item.selectionChangedListeners.add { cell.selectionChangedListeners.add {
selection = it selection = it
onUpdate() onUpdate()
} }
@@ -172,32 +199,41 @@ fun PuzzleGrid(
selectDirectly = selectDirectly, selectDirectly = selectDirectly,
options = options, options = options,
onOptionRemoved = { onOptionRemoved = {
grid.snapshot() onSnapshot()
item.options.remove(it) cell.options.remove(it)
row.cleanupOptions() row.cleanupOptions()
}, },
onOptionAdded = { onOptionAdded = {
item.options.add(it) cell.options.add(it)
}, },
selectedItem = selection, selectedItem = selection,
onSelectItem = { selectedItem -> onSelectItem = { selectedItem ->
if (selectedItem != null) { onSelectItem(row, cell, options, selectedItem, onSnapshot, onUndo)
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)
}
}
} }
) )
} }
} }
} }
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 @Composable
fun ClueCard( fun ClueCard(

View File

@@ -1,11 +1,10 @@
package ch.dissem.yaep.ui.common.theme package ch.dissem.yaep.ui.common.theme
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
private val lightScheme = lightColorScheme( private val lightScheme = lightColorScheme(
primary = primaryLight, primary = primaryLight,
@@ -83,170 +82,6 @@ private val darkScheme = darkColorScheme(
surfaceContainerHighest = surfaceContainerHighestDark, 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 @Composable
fun AppTheme( fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
@@ -256,18 +91,9 @@ fun AppTheme(
darkTheme -> darkScheme darkTheme -> darkScheme
else -> lightScheme 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
// }
// }
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
// typography = AppTypography,
content = content content = content
) )
} }

View File

@@ -3,8 +3,8 @@ package ch.dissem.yaep.ui.common
import ch.dissem.yaep.domain.Game import ch.dissem.yaep.domain.Game
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
import kotlin.io.path.Path import kotlin.io.path.Path
import kotlin.io.path.createFile import kotlin.io.path.createFile
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory
@@ -15,8 +15,8 @@ import kotlin.time.ExperimentalTime
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
actual fun CoroutineScope.logGame(game: Game) { actual fun CoroutineScope.logGame(game: Game, dispatcher: CoroutineContext) {
launch(Dispatchers.IO) { launch(dispatcher) {
val dirName = """${System.getProperty("user.home")}/.yaep""" val dirName = """${System.getProperty("user.home")}/.yaep"""
val dir = Path(dirName) val dir = Path(dirName)
if (dir.isDirectory()) { if (dir.isDirectory()) {

View File

@@ -19,7 +19,7 @@ class GameCell<C : ItemClass<C>>(
selectionChangedListeners.forEach { listener -> listener(value) } 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 -> optionsChangedListeners.forEach { listener ->
listener(after) listener(after)
} }

View File

@@ -37,7 +37,7 @@ class GameRow<C : ItemClass<C>>(
it.options.clear() it.options.clear()
it.options.add(it.selection!!) it.options.add(it.selection!!)
} }
cellsWithSelection.mapNotNull { it.selection }.toMutableSet() cellsWithSelection.mapNotNull { it.selection }.toSet()
} }
filter { it.selection == null } filter { it.selection == null }
.forEach { it.options.removeAll(selections) } .forEach { it.options.removeAll(selections) }

View File

@@ -75,12 +75,14 @@ class NeighbourClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b: I
for (iX in rowX.indices) { for (iX in rowX.indices) {
val cellX = rowX[iX] val cellX = rowX[iX]
if (cellX.mayBe(x, mayHaveSelection = false)) { if (
if (!rowY.getOrNull(iX - 1).mayBe(y) && !rowY.getOrNull(iX + 1).mayBe(y)) { cellX.mayBe(x, mayHaveSelection = false)
&& !rowY.getOrNull(iX - 1).mayBe(y)
&& !rowY.getOrNull(iX + 1).mayBe(y)
) {
removed = cellX.options.remove(x) || removed removed = cellX.options.remove(x) || removed
} }
} }
}
return removed return removed
} }
@@ -185,7 +187,25 @@ class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
val ic by lazy { rowC.indexOf(cType) } val ic by lazy { rowC.indexOf(cType) }
if (ia != -1) { if (ia != -1) {
return when (ib) { return isValidWithASet(ia, ib, ic, rowB, rowC)
}
if (ib != -1) {
return isValidWithBSet(ib, ic, rowA, rowC)
}
if (ic != -1) {
return isValidWithCSet(ic, rowA, rowB)
}
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 -> when (ic) {
-1 -> (rowB.getOrNull(ia - 1).hasNoSelection() && rowC.getOrNull(ia - 2) -1 -> (rowB.getOrNull(ia - 1).hasNoSelection() && rowC.getOrNull(ia - 2)
.hasNoSelection()) || .hasNoSelection()) ||
@@ -211,25 +231,27 @@ class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
else -> false else -> false
} }
}
if (ib != -1) { private fun isValidWithBSet(ib: Int, ic: Int, rowA: GameRow<A>, rowC: GameRow<C>): Boolean =
when (ic) { when (ic) {
-1 -> return (rowA.getOrNull(ib - 1).hasNoSelection() && rowC.getOrNull(ib + 1) -1 -> (rowA.getOrNull(ib - 1).hasNoSelection() && rowC.getOrNull(ib + 1)
.hasNoSelection()) || .hasNoSelection()) ||
(rowA.getOrNull(ib + 1).hasNoSelection() && rowC.getOrNull(ib - 1) (rowA.getOrNull(ib + 1).hasNoSelection() && rowC.getOrNull(ib - 1)
.hasNoSelection()) .hasNoSelection())
ib - 1 -> return rowA.getOrNull(ib + 1).hasNoSelection() ib - 1 -> rowA.getOrNull(ib + 1).hasNoSelection()
ib + 1 -> return rowA.getOrNull(ib - 1).hasNoSelection() ib + 1 -> rowA.getOrNull(ib - 1).hasNoSelection()
else -> false
} }
}
if (ic != -1) { private fun isValidWithCSet(ic: Int, rowA: GameRow<A>, rowB: GameRow<B>): Boolean =
return (rowB.getOrNull(ic - 1).hasNoSelection() && rowA.getOrNull(ic - 2) (rowB.getOrNull(ic - 1).hasNoSelection() && rowA.getOrNull(ic - 2)
.hasNoSelection()) || .hasNoSelection()) ||
(rowB.getOrNull(ic + 1).hasNoSelection() && rowA.getOrNull(ic + 2) (rowB.getOrNull(ic + 1).hasNoSelection() && rowA.getOrNull(ic + 2)
.hasNoSelection()) .hasNoSelection())
}
return rowA.mapIndexed { index, gameCell -> if (gameCell.hasNoSelection()) index else null } private fun isValidWithNoneSet(rowA: GameRow<A>, rowB: GameRow<B>, rowC: GameRow<C>): Boolean =
rowA.mapIndexed { index, gameCell -> if (gameCell.hasNoSelection()) index else null }
.filterNotNull() .filterNotNull()
.any { index -> .any { index ->
(rowB.getOrNull(index - 1).hasNoSelection() && rowC.getOrNull(index - 2) (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) (rowB.getOrNull(index + 1).hasNoSelection() && rowC.getOrNull(index + 2)
.hasNoSelection()) .hasNoSelection())
} }
}
override fun removeForbiddenOptions(grid: Grid): Boolean { override fun removeForbiddenOptions(grid: Grid): Boolean {
val rowA = grid[aType.companion] val rowA = grid[aType.companion]
@@ -247,49 +268,62 @@ class TripletClue<A : ItemClass<A>, B : ItemClass<B>, C : ItemClass<C>>(
var removed = false var removed = false
for (i in rowA.indices) { for (i in rowA.indices) {
val cellA = rowA[i] removed = removeForbiddenOptionsGivenX(
if (cellA.mayBe(a, mayHaveSelection = false)) { i = i,
val cellBR = rowB.getOrNull(i + 1) x = Group(a, rowA),
val cellCR = rowC.getOrNull(i + 2) y = Group(b, rowB, 1),
val cellBL = rowB.getOrNull(i - 1) z = Group(c, rowC, 2)
val cellCL = rowC.getOrNull(i - 2) ) || removed
if (!(cellBR.mayBe(b) && cellCR.mayBe(c)) && !(cellBL.mayBe(b) && cellCL.mayBe(c))) {
removed = cellA.options.remove(a) || removed
}
}
} }
for (i in rowB.indices) { for (i in rowB.indices) {
val cellB = rowB[i] removed = removeForbiddenOptionsGivenX(
if (cellB.mayBe(b, mayHaveSelection = false)) { i = i,
val cellAL = rowA.getOrNull(i - 1) x = Group(b, rowB),
val cellAR = rowA.getOrNull(i + 1) y = Group(a, rowA, 1),
val cellCL = rowC.getOrNull(i - 1) z = Group(c, rowC, -1)
val cellCR = rowC.getOrNull(i + 1) ) || removed
if (!(cellAL.mayBe(a) && cellCR.mayBe(c)) && !(cellCL.mayBe(c) && cellAR.mayBe(a))) {
removed = cellB.options.remove(b) || removed
}
}
} }
for (i in rowC.indices) { for (i in rowC.indices) {
val cellC = rowC[i] removed = removeForbiddenOptionsGivenX(
if (cellC.mayBe(c, mayHaveSelection = false)) { i = i,
val cellBR = rowB.getOrNull(i + 1) x = Group(c, rowC),
val cellAR = rowA.getOrNull(i + 2) y = Group(b, rowB, 1),
val cellBL = rowB.getOrNull(i - 1) z = Group(a, rowA, 2)
val cellAL = rowA.getOrNull(i - 2) ) || removed
if (!(cellBR.mayBe(b) && cellAR.mayBe(a)) && !(cellBL.mayBe(b) && cellAL.mayBe(a))) {
removed = cellC.options.remove(c) || removed
} }
}
}
return 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 = override fun toString(): String =
"$bType is between the neighbours $aType and $cType to both sides" "$bType is between the neighbours $aType and $cType to both sides"
@@ -343,17 +377,13 @@ class SameColumnClue<A : ItemClass<A>, B : ItemClass<B>>(val a: Item<A>, val b:
val cellA = rowA[i] val cellA = rowA[i]
val cellB = rowB[i] val cellB = rowB[i]
if (cellB.hasNoSelection()) { if (cellB.hasNoSelection() && !cellA.mayBe(a)) {
if (!cellA.mayBe(a)) {
removed = cellB.options.remove(b) || removed removed = cellB.options.remove(b) || removed
} }
} if (cellA.hasNoSelection() && !cellB.mayBe(b)) {
if (cellA.hasNoSelection()) {
if (!cellB.mayBe(b)) {
removed = cellA.options.remove(a) || removed removed = cellA.options.remove(a) || removed
} }
} }
}
return removed return removed
} }

View File

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

View File

@@ -19,7 +19,7 @@ class GameTest {
@Test @Test
fun `ensure generated games are solvable`() { fun `ensure generated games are solvable`() {
val tries = 1000 val tries = 100
var fastest = 500.milliseconds var fastest = 500.milliseconds
var slowest = 0.milliseconds var slowest = 0.milliseconds
var total = 0.milliseconds var total = 0.milliseconds

View File

@@ -1,7 +1,7 @@
[versions] [versions]
app-version-code = "1" app-version-code = "1"
app-version-name = "1.0.0" app-version-name = "1.0.0"
agp = "8.11.1" agp = "8.12.0"
jdk = "21" jdk = "21"
android-compileSdk = "36" android-compileSdk = "36"
android-minSdk = "26" 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" } atrium = { module = "ch.tutteli.atrium:atrium-fluent", version.ref = "atrium" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.2" } 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" } logging-slf4j = { module = "org.slf4j:slf4j-simple", version = "2.0.17" }
[bundles] [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-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", 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" } compose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
sonarqube = { id = "org.sonarqube", version = "6.2.0.5505" }