Add improvements
almost playable
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 ->
|
||||||
|
|||||||
Reference in New Issue
Block a user