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)) } allOptions.map { Toggleable(it, item.options.contains(it)) }
} }
LaunchedEffect(item) { LaunchedEffect(item) {
item.optionsRemovedListeners.add { removed -> item.optionsChangedListeners.add { enabled ->
options options.forEach { it.enabled = enabled.contains(it.item) }
.filter { removed.contains(it.item) }
.forEach { it.enabled = false }
} }
item.selectionChangedListeners.add { item.selectionChangedListeners.add {
selection = it selection = it
@@ -151,9 +149,17 @@ fun PuzzleGrid(
}, },
selectedItem = selection, selectedItem = selection,
onSelectItem = { onSelectItem = {
item.selection = it
if (it != null) { if (it != null) {
grid.snapshot()
item.selection = it
row.cleanupOptions() 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 -> row.forEachIndexed { index, gameCell ->
if (index == position.index) { if (index == position.index) {
gameCell.selection = position.item gameCell.selection = position.item
gameCell.mutable = false
} else { } else {
gameCell.options.remove(position.item) gameCell.options.remove(position.item)
} }
@@ -23,7 +24,7 @@ class Game(
} }
grid.forEach { row -> grid.forEach { row ->
row.forEach { cell -> row.forEach { cell ->
cell.optionsRemovedListeners.add { cell.optionsChangedListeners.add {
if (onStartListeners.isNotEmpty()) { if (onStartListeners.isNotEmpty()) {
onStartListeners.forEach { it() } onStartListeners.forEach { it() }
onStartListeners.clear() onStartListeners.clear()

View File

@@ -3,17 +3,41 @@ package ch.dissem.yaep.domain
class GameCell<C : ItemClass<C>>( class GameCell<C : ItemClass<C>>(
selection: Item<C>? = null, selection: Item<C>? = null,
val solution: 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 selectionChangedListeners: MutableList<(Item<C>?) -> Unit> =
val optionsRemovedListeners = mutableListOf<(Collection<Item<C>>) -> Unit>() mutableListOf<(Item<C>?) -> Unit>()
val optionsChangedListeners: MutableList<(Collection<Item<C>>) -> Unit> =
mutableListOf<(Collection<Item<C>>) -> Unit>()
var selection: Item<C>? = selection var selection: Item<C>? = selection
set(value) { set(value) {
if (mutable) {
field = value field = value
selectionChangedListeners.forEach { listener -> listener(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) = 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>) = 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(): Boolean = this != null && this.selection == null

View File

@@ -5,20 +5,20 @@ class GameRow<C : ItemClass<C>>(
val options: List<Item<C>>, val options: List<Item<C>>,
val cells: List<GameCell<C>> val cells: List<GameCell<C>>
) : List<GameCell<C>> by cells { ) : List<GameCell<C>> by cells {
var isSolved = false var isSolved: Boolean = false
private set(value) { private set(value) {
field = value field = value
if (value) { if (value) {
onSolvedListener() onSolvedListener()
} }
} }
var onSolvedListener = {} var onSolvedListener: () -> Unit = {}
fun onSolved(listener: () -> Unit) { fun onSolved(listener: () -> Unit) {
onSolvedListener = listener onSolvedListener = listener
} }
fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element } fun indexOf(element: C): Int = indexOfFirst { it.selection?.itemType == element }
fun cleanupOptions() { fun cleanupOptions() {
if (isSolved && all { 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() { fun cleanupOptions() {
forEach { row -> row.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( fun List<List<Item<ItemClass<*>>>>.toGrid() = Grid(

View File

@@ -2,28 +2,62 @@ package ch.dissem.yaep.domain
class ObservableSet<E> private constructor( class ObservableSet<E> private constructor(
private val mutableSet: MutableSet<E>, 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 { ) : 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(), 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 { 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 = override fun removeAll(elements: Collection<E>): Boolean =
observeRemoval(elements.toSet(), mutableSet::removeAll) observe(elements.toSet(), mutableSet::removeAll)
override fun retainAll(elements: Collection<E>): Boolean = 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 { override fun clear() {
val removed = operation(values) return observe(setOf()) { mutableSet.clear() }
if (removed) {
onElementRemoved(values)
} }
return removed
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 result
} }
} }