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.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
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.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.Clue import ch.dissem.yaep.domain.*
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 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
@@ -66,15 +48,30 @@ fun PuzzleGrid(
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
) { ) {
val allOptions = row.options
for (item in row) { 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( Selector(
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.weight(1f), .weight(1f),
category = row.category, options = options,
options = item.options, selectedItem = selection,
selectedItem = item.selection, onSelectItem = {
onSelectItem = { item.selection = it } item.selection = it
grid.cleanupOptions()
}
) )
} }
} }

View File

@@ -1,7 +1,6 @@
package ch.dissem.yaep.ui.common package ch.dissem.yaep.ui.common
import androidx.compose.foundation.Canvas import androidx.compose.foundation.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize 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.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import ch.dissem.yaep.domain.Item import ch.dissem.yaep.domain.Item
import ch.dissem.yaep.domain.ItemClass import ch.dissem.yaep.domain.ItemClass
import ch.dissem.yaep.domain.ItemClassCompanion
import ch.dissem.yaep.ui.common.theme.emojiFontFamily import ch.dissem.yaep.ui.common.theme.emojiFontFamily
import kotlin.math.min import kotlin.math.min
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun <C : ItemClass<C>> Selector( fun <C : ItemClass<C>> Selector(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
category: ItemClassCompanion<C>, options: List<Toggleable<Item<C>>>,
options: List<Item<C>>,
selectedItem: Item<C>?, selectedItem: Item<C>?,
onSelectItem: (Item<C>?) -> Unit onSelectItem: (Item<C>?) -> Unit,
) { ) {
if (selectedItem != null) { if (selectedItem != null) {
DrawItem(item = selectedItem, modifier = modifier.clickable { onSelectItem(null) }) DrawItem(item = selectedItem, modifier = modifier.clickable { onSelectItem(null) })
@@ -40,8 +43,19 @@ fun <C : ItemClass<C>> Selector(
for (option in options) { for (option in options) {
item { item {
DrawItem( DrawItem(
item = option, item = option.item,
modifier = Modifier.clickable { onSelectItem(option) } 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( drawText(
textMeasurer = textMeasurer, textMeasurer = textMeasurer,
text = emoji, text = emoji,
style = TextStyle(fontSize = fontSize, fontFamily = emojiFontFamily ?: TextStyle.Default.fontFamily), style = TextStyle(
fontSize = fontSize,
fontFamily = emojiFontFamily ?: TextStyle.Default.fontFamily
),
topLeft = offset 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 package ch.dissem.yaep.domain
class GameCell<C : ItemClass<C>>( class GameCell<C : ItemClass<C>>(
var selection: Item<C>?, selection: Item<C>?,
val solution: 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) = fun <C : ItemClass<C>> GameCell<C>?.mayBe(item: Item<C>, mayHaveSelection: Boolean = true) =
this != null && this != null &&
@@ -14,3 +24,4 @@ fun <C : ItemClass<C>> GameCell<C>?.isA(item: Item<C>) =
this != null && selection == item this != null && selection == item
fun <C : ItemClass<C>> GameCell<C>?.hasNoSelection() = this != null && this.selection == null 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 { ) : List<GameCell<C>> by cells {
fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element } fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element }
fun updateOptions() { fun cleanupOptions() {
val selections = mapNotNull { it.selection } cells.forEach { cleanupOptions(it) }
forEach { it.options.removeAll(selections) } }
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<*>>> { ) : List<GameRow<ItemClass<*>>> by rows as List<GameRow<ItemClass<*>>> {
val cells: List<GameCell<*>> val cells: List<GameCell<*>>
get() = rows.flatten() as List<GameCell<*>> get() = rows.flatten()
fun <C : ItemClass<C>> indexOf(element: C): Int { fun <C : ItemClass<C>> indexOf(element: C): Int {
return this[element.companion] return this[element.companion]
@@ -27,6 +27,10 @@ class Grid(
row.joinToString("") { it.selection?.symbol ?: " " } row.joinToString("") { it.selection?.symbol ?: " " }
} }
} }
fun cleanupOptions() {
forEach { row -> row.cleanupOptions() }
}
} }
fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid( 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) } // .forEach { it.options.removeAll(groupOptions) }
// } // }
// } // }
grid.forEach { row -> row.forEach { cell -> row.cleanupOptions(cell) } } grid.cleanupOptions()
} while (removedOptions) } while (removedOptions)
// If any cell has no items left, the puzzle has no solution. // If any cell has no items left, the puzzle has no solution.
@@ -92,16 +92,6 @@ internal enum class PuzzleSolution {
MULTIPLE_SOLUTIONS 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> { fun getAllClues(rows: List<List<Item<ItemClass<*>>>>): MutableSet<Clue> {
val clues = mutableSetOf<Clue>() val clues = mutableSetOf<Clue>()
rows.forEach { row -> rows.forEach { row ->