Add improvements

almost playable
This commit is contained in:
Christian Basler
2024-07-24 17:37:44 +02:00
parent 630d24f3c4
commit 3cae93e5bf
7 changed files with 131 additions and 53 deletions

View File

@@ -2,34 +2,16 @@ package ch.dissem.yaep.ui.common
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.OutlinedCard
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.Clue
import ch.dissem.yaep.domain.Grid
import ch.dissem.yaep.domain.HorizontalClue
import ch.dissem.yaep.domain.ItemClass
import ch.dissem.yaep.domain.NeighbourClue
import ch.dissem.yaep.domain.OrderClue
import ch.dissem.yaep.domain.SameColumnClue
import ch.dissem.yaep.domain.TripletClue
import ch.dissem.yaep.domain.generateGame
import ch.dissem.yaep.domain.*
import org.jetbrains.compose.resources.painterResource
import yaep.commonui.generated.resources.Res
import yaep.commonui.generated.resources.neighbour
@@ -66,15 +48,30 @@ fun PuzzleGrid(
.fillMaxWidth()
.wrapContentHeight()
) {
val allOptions = row.options
for (item in row) {
var selection by remember { mutableStateOf(item.selection) }
val options = remember { allOptions.map { Toggleable(it, item.options.contains(it)) } }
LaunchedEffect(item) {
item.removedListeners.add { removed ->
options
.filter { removed.contains(it.item) }
.forEach { it.enabled = false }
}
item.selectionChangedListeners.add {
selection = it
}
}
Selector(
modifier = Modifier
.padding(8.dp)
.weight(1f),
category = row.category,
options = item.options,
selectedItem = item.selection,
onSelectItem = { item.selection = it }
options = options,
selectedItem = selection,
onSelectItem = {
item.selection = it
grid.cleanupOptions()
}
)
}
}
@@ -169,4 +166,4 @@ fun VerticalClue(modifier: Modifier = Modifier, clue: SameColumnClue<*, *>) {
DrawItem(modifier = Modifier.weight(1f), clue.b)
}
}
}
}

View File

@@ -1,7 +1,6 @@
package ch.dissem.yaep.ui.common
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
@@ -9,24 +8,28 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.OutlinedCard
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
import ch.dissem.yaep.domain.Item
import ch.dissem.yaep.domain.ItemClass
import ch.dissem.yaep.domain.ItemClassCompanion
import ch.dissem.yaep.ui.common.theme.emojiFontFamily
import kotlin.math.min
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun <C : ItemClass<C>> Selector(
modifier: Modifier = Modifier,
category: ItemClassCompanion<C>,
options: List<Item<C>>,
options: List<Toggleable<Item<C>>>,
selectedItem: Item<C>?,
onSelectItem: (Item<C>?) -> Unit
onSelectItem: (Item<C>?) -> Unit,
) {
if (selectedItem != null) {
DrawItem(item = selectedItem, modifier = modifier.clickable { onSelectItem(null) })
@@ -40,8 +43,19 @@ fun <C : ItemClass<C>> Selector(
for (option in options) {
item {
DrawItem(
item = option,
modifier = Modifier.clickable { onSelectItem(option) }
item = option.item,
modifier = Modifier
.alpha(if (option.enabled) 1f else 0.1f)
.combinedClickable(
onClick = { onSelectItem(option.item) },
onLongClick = { option.enabled = false }
)
.onClick(
matcher = PointerMatcher.mouse(PointerButton.Secondary),
onClick = {
option.enabled = false
}
)
)
}
}
@@ -81,7 +95,10 @@ fun <C : ItemClass<C>> DrawItem(
drawText(
textMeasurer = textMeasurer,
text = emoji,
style = TextStyle(fontSize = fontSize, fontFamily = emojiFontFamily ?: TextStyle.Default.fontFamily),
style = TextStyle(
fontSize = fontSize,
fontFamily = emojiFontFamily ?: TextStyle.Default.fontFamily
),
topLeft = offset
)
}
@@ -89,3 +106,23 @@ fun <C : ItemClass<C>> DrawItem(
}
}
class Toggleable<T>(val item: T, enabled: Boolean = true) {
var enabled: Boolean by mutableStateOf(enabled)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Toggleable<*>) return false
if (item != other.item) return false
if (enabled != other.enabled) return false
return true
}
override fun hashCode(): Int {
var result = item.hashCode()
result = 31 * result + enabled.hashCode()
return result
}
}

View File

@@ -1,10 +1,20 @@
package ch.dissem.yaep.domain
class GameCell<C : ItemClass<C>>(
var selection: Item<C>?,
selection: Item<C>?,
val solution: Item<C>,
val options: MutableList<Item<C>>
)
options: Collection<Item<C>>
) {
val selectionChangedListeners = mutableListOf<(Item<C>?) -> Unit>()
val removedListeners = mutableListOf<(Collection<Item<C>>) -> Unit>()
var selection: Item<C>? = selection
set(value) {
field = value
selectionChangedListeners.forEach { listener -> listener(value) }
}
val options = ObservableSet(options.toMutableSet()) { removedListeners.forEach { listener -> listener(it) } }
}
fun <C : ItemClass<C>> GameCell<C>?.mayBe(item: Item<C>, mayHaveSelection: Boolean = true) =
this != null &&
@@ -14,3 +24,4 @@ fun <C : ItemClass<C>> GameCell<C>?.isA(item: Item<C>) =
this != null && selection == item
fun <C : ItemClass<C>> GameCell<C>?.hasNoSelection() = this != null && this.selection == null

View File

@@ -7,8 +7,18 @@ class GameRow<C : ItemClass<C>>(
) : List<GameCell<C>> by cells {
fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element }
fun updateOptions() {
val selections = mapNotNull { it.selection }
forEach { it.options.removeAll(selections) }
fun cleanupOptions() {
cells.forEach { cleanupOptions(it) }
}
private fun cleanupOptions(cell: GameCell<C>) {
if (cell.options.size == 1 && cell.selection == null) {
cell.selection = cell.options.first()
filter { otherCell -> otherCell != cell && otherCell.selection == null }.forEach { otherCell ->
otherCell.options.remove(cell.selection)
cleanupOptions(otherCell)
}
}
}
}

View File

@@ -6,7 +6,7 @@ class Grid(
) : List<GameRow<ItemClass<*>>> by rows as List<GameRow<ItemClass<*>>> {
val cells: List<GameCell<*>>
get() = rows.flatten() as List<GameCell<*>>
get() = rows.flatten()
fun <C : ItemClass<C>> indexOf(element: C): Int {
return this[element.companion]
@@ -27,6 +27,10 @@ class Grid(
row.joinToString("") { it.selection?.symbol ?: " " }
}
}
fun cleanupOptions() {
forEach { row -> row.cleanupOptions() }
}
}
fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid(

View File

@@ -0,0 +1,29 @@
package ch.dissem.yaep.domain
class ObservableSet<E> private constructor(
private val mutableSet: MutableSet<E>,
private val onElementRemoved: (Set<E>) -> Unit
) : MutableSet<E> by mutableSet {
constructor(elements: Collection<E>, onElementRemoved: (Set<E>) -> Unit) : this(
elements.toMutableSet(),
onElementRemoved
)
override fun remove(element: E): Boolean {
return observeRemoval(setOf(element)) { mutableSet.remove(element) }
}
override fun removeAll(elements: Collection<E>): Boolean =
observeRemoval(elements.toSet(), mutableSet::removeAll)
override fun retainAll(elements: Collection<E>): Boolean =
observeRemoval(elements.toSet(), mutableSet::retainAll)
private fun observeRemoval(values: Set<E>, operation: (Set<E>) -> Boolean): Boolean {
val removed = operation(values)
if (removed) {
onElementRemoved(values)
}
return removed
}
}

View File

@@ -74,7 +74,7 @@ internal fun solve(
// .forEach { it.options.removeAll(groupOptions) }
// }
// }
grid.forEach { row -> row.forEach { cell -> row.cleanupOptions(cell) } }
grid.cleanupOptions()
} while (removedOptions)
// If any cell has no items left, the puzzle has no solution.
@@ -92,16 +92,6 @@ internal enum class PuzzleSolution {
MULTIPLE_SOLUTIONS
}
fun <C : ItemClass<C>> GameRow<C>.cleanupOptions(cell: GameCell<C>) {
if (cell.options.size == 1 && cell.selection == null) {
cell.selection = cell.options.first()
filter { otherCell -> otherCell != cell && otherCell.selection == null }.forEach { otherCell ->
otherCell.options.remove(cell.selection)
cleanupOptions(otherCell)
}
}
}
fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableSet<Clue> {
val clues = mutableSetOf<Clue>()
rows.forEach { row ->