Implement undo functionality

Not quite sure about its usability though
This commit is contained in:
Christian Basler
2025-03-14 06:32:12 +01:00
parent 63f6fca83f
commit a3611cb9a7
6 changed files with 116 additions and 28 deletions

View File

@@ -127,10 +127,8 @@ fun PuzzleGrid(
allOptions.map { Toggleable(it, item.options.contains(it)) }
}
LaunchedEffect(item) {
item.optionsRemovedListeners.add { removed ->
options
.filter { removed.contains(it.item) }
.forEach { it.enabled = false }
item.optionsChangedListeners.add { enabled ->
options.forEach { it.enabled = enabled.contains(it.item) }
}
item.selectionChangedListeners.add {
selection = it
@@ -151,9 +149,17 @@ fun PuzzleGrid(
},
selectedItem = selection,
onSelectItem = {
item.selection = it
if (it != null) {
grid.snapshot()
item.selection = it
row.cleanupOptions()
} else {
while (item.selection != null) {
if (!grid.undo()) break
}
options.forEach {
it.enabled = item.options.contains(it.item)
}
}
}
)

View File

@@ -16,6 +16,7 @@ class Game(
row.forEachIndexed { index, gameCell ->
if (index == position.index) {
gameCell.selection = position.item
gameCell.mutable = false
} else {
gameCell.options.remove(position.item)
}
@@ -23,7 +24,7 @@ class Game(
}
grid.forEach { row ->
row.forEach { cell ->
cell.optionsRemovedListeners.add {
cell.optionsChangedListeners.add {
if (onStartListeners.isNotEmpty()) {
onStartListeners.forEach { it() }
onStartListeners.clear()

View File

@@ -1,19 +1,43 @@
package ch.dissem.yaep.domain
class GameCell<C : ItemClass<C>>(
selection: Item<C>?=null,
selection: Item<C>? = null,
val solution: Item<C>? = null,
options: Collection<Item<C>>
options: Collection<Item<C>>,
/*** If mutable is false, the cell will silently ignore all changes to 'selection'. */
var mutable: Boolean = true
) {
val selectionChangedListeners = mutableListOf<(Item<C>?) -> Unit>()
val optionsRemovedListeners = mutableListOf<(Collection<Item<C>>) -> Unit>()
val selectionChangedListeners: MutableList<(Item<C>?) -> Unit> =
mutableListOf<(Item<C>?) -> Unit>()
val optionsChangedListeners: MutableList<(Collection<Item<C>>) -> Unit> =
mutableListOf<(Collection<Item<C>>) -> Unit>()
var selection: Item<C>? = selection
set(value) {
field = value
selectionChangedListeners.forEach { listener -> listener(value) }
if (mutable) {
field = value
selectionChangedListeners.forEach { listener -> listener(value) }
}
}
val options = ObservableSet(options.toMutableSet()) { optionsRemovedListeners.forEach { listener -> listener(it) } }
val options: ObservableSet<Item<C>> = ObservableSet(options.toMutableSet()) { before, after ->
optionsChangedListeners.forEach { listener ->
listener(after)
}
}
private val snapshots = ArrayDeque<Pair<Item<C>?, Collection<Item<C>>>>()
fun snapshot() {
snapshots.addLast(selection to options.toSet())
}
fun undo(): Boolean {
val (selection, options) = snapshots.removeLastOrNull() ?: return false
this.selection = selection
this.options.clear()
this.options.addAll(options)
return true
}
}
fun <C : ItemClass<C>> GameCell<C>?.mayBe(item: Item<C>, mayHaveSelection: Boolean = true) =
@@ -23,5 +47,5 @@ fun <C : ItemClass<C>> GameCell<C>?.mayBe(item: Item<C>, mayHaveSelection: Boole
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
fun <C : ItemClass<C>> GameCell<C>?.hasNoSelection(): Boolean = this != null && this.selection == null

View File

@@ -5,20 +5,20 @@ class GameRow<C : ItemClass<C>>(
val options: List<Item<C>>,
val cells: List<GameCell<C>>
) : List<GameCell<C>> by cells {
var isSolved = false
var isSolved: Boolean = false
private set(value) {
field = value
if (value) {
onSolvedListener()
}
}
var onSolvedListener = {}
var onSolvedListener: () -> Unit = {}
fun onSolved(listener: () -> Unit) {
onSolvedListener = listener
}
fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element }
fun indexOf(element: C): Int = indexOfFirst { it.selection?.itemType == element }
fun cleanupOptions() {
if (isSolved && all {
@@ -64,4 +64,19 @@ class GameRow<C : ItemClass<C>>(
}
}
fun snapshot() {
cells.forEach { it.snapshot() }
}
fun undo(): Boolean {
val didChange = cells.map { it.undo() }.reduce { a, b -> a || b }
if (didChange) {
isSolved = all {
it.solution != null
&& it.options.size == 1
&& it.options.single() == it.selection
}
}
return didChange
}
}

View File

@@ -31,6 +31,14 @@ class Grid(
fun cleanupOptions() {
forEach { row -> row.cleanupOptions() }
}
fun snapshot() {
forEach { row -> row.snapshot() }
}
fun undo(): Boolean {
return map { row -> row.undo() }.reduce { a, b -> a || b }
}
}
fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid(

View File

@@ -2,28 +2,62 @@ package ch.dissem.yaep.domain
class ObservableSet<E> private constructor(
private val mutableSet: MutableSet<E>,
private val onElementRemoved: (Set<E>) -> Unit
private val onElementChanged: (before: Set<E>, after: Set<E>) -> Unit
) : MutableSet<E> by mutableSet {
constructor(elements: Collection<E>, onElementRemoved: (Set<E>) -> Unit) : this(
constructor(
elements: Collection<E>,
onElementChanged: (before: Set<E>, after: Set<E>) -> Unit
) : this(
elements.toMutableSet(),
onElementRemoved
onElementChanged
)
private inner class ObservableIterator(
private val delegate: MutableIterator<E>
) : MutableIterator<E> by delegate {
override fun remove() {
val previous = mutableSet.toSet()
delegate.remove()
onElementChanged(previous, mutableSet.toSet())
}
}
override fun iterator(): MutableIterator<E> {
return ObservableIterator(mutableSet.iterator())
}
override fun add(element: E): Boolean {
return observe(setOf(element)) { mutableSet.add(element) }
}
override fun remove(element: E): Boolean {
return observeRemoval(setOf(element)) { mutableSet.remove(element) }
return observe(setOf(element)) { mutableSet.remove(element) }
}
override fun addAll(elements: Collection<E>): Boolean {
return observe(elements.toSet()) { mutableSet.addAll(elements) }
}
override fun removeAll(elements: Collection<E>): Boolean =
observeRemoval(elements.toSet(), mutableSet::removeAll)
observe(elements.toSet(), mutableSet::removeAll)
override fun retainAll(elements: Collection<E>): Boolean =
observeRemoval(elements.toSet(), mutableSet::retainAll)
observe(elements.toSet(), mutableSet::retainAll)
private fun observeRemoval(values: Set<E>, operation: (Set<E>) -> Boolean): Boolean {
val removed = operation(values)
if (removed) {
onElementRemoved(values)
override fun clear() {
return observe(setOf()) { mutableSet.clear() }
}
private fun <R> observe(values: Set<E>, operation: (Set<E>) -> R): R {
val previous = mutableSet.toSet()
val result = operation(values)
val changed = !(previous.containsAll(mutableSet) && previous.size == mutableSet.size)
if (changed) {
val new = mutableSet.toSet()
onElementChanged(previous, new)
}
return removed
return result
}
}