diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt index 99f245b..3ee222f 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt @@ -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) + } } } ) diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Game.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Game.kt index 6db2ef5..f309ca1 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Game.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Game.kt @@ -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() diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameCell.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameCell.kt index 9e2f723..0c5e842 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameCell.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameCell.kt @@ -1,19 +1,43 @@ package ch.dissem.yaep.domain class GameCell>( - selection: Item?=null, + selection: Item? = null, val solution: Item? = null, - options: Collection> + options: Collection>, + /*** If mutable is false, the cell will silently ignore all changes to 'selection'. */ + var mutable: Boolean = true ) { - val selectionChangedListeners = mutableListOf<(Item?) -> Unit>() - val optionsRemovedListeners = mutableListOf<(Collection>) -> Unit>() + val selectionChangedListeners: MutableList<(Item?) -> Unit> = + mutableListOf<(Item?) -> Unit>() + val optionsChangedListeners: MutableList<(Collection>) -> Unit> = + mutableListOf<(Collection>) -> Unit>() var selection: Item? = 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> = ObservableSet(options.toMutableSet()) { before, after -> + optionsChangedListeners.forEach { listener -> + listener(after) + } + } + + private val snapshots = ArrayDeque?, Collection>>>() + + 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 > GameCell?.mayBe(item: Item, mayHaveSelection: Boolean = true) = @@ -23,5 +47,5 @@ fun > GameCell?.mayBe(item: Item, mayHaveSelection: Boole fun > GameCell?.isA(item: Item) = this != null && selection == item -fun > GameCell?.hasNoSelection() = this != null && this.selection == null +fun > GameCell?.hasNoSelection(): Boolean = this != null && this.selection == null diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt index d58a1a1..2bed349 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt @@ -5,20 +5,20 @@ class GameRow>( val options: List>, val cells: List> ) : List> 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>( } } + 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 + } } diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Grid.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Grid.kt index 381a92c..903556f 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Grid.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Grid.kt @@ -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>>>.toGrid() = Grid( diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/ObservableSet.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/ObservableSet.kt index 101f11e..0c43351 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/ObservableSet.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/ObservableSet.kt @@ -2,28 +2,62 @@ package ch.dissem.yaep.domain class ObservableSet private constructor( private val mutableSet: MutableSet, - private val onElementRemoved: (Set) -> Unit + private val onElementChanged: (before: Set, after: Set) -> Unit ) : MutableSet by mutableSet { - constructor(elements: Collection, onElementRemoved: (Set) -> Unit) : this( + constructor( + elements: Collection, + onElementChanged: (before: Set, after: Set) -> Unit + ) : this( elements.toMutableSet(), - onElementRemoved + onElementChanged ) + private inner class ObservableIterator( + private val delegate: MutableIterator + ) : MutableIterator by delegate { + + override fun remove() { + val previous = mutableSet.toSet() + delegate.remove() + onElementChanged(previous, mutableSet.toSet()) + } + } + + override fun iterator(): MutableIterator { + 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): Boolean { + return observe(elements.toSet()) { mutableSet.addAll(elements) } } override fun removeAll(elements: Collection): Boolean = - observeRemoval(elements.toSet(), mutableSet::removeAll) + observe(elements.toSet(), mutableSet::removeAll) override fun retainAll(elements: Collection): Boolean = - observeRemoval(elements.toSet(), mutableSet::retainAll) + observe(elements.toSet(), mutableSet::retainAll) - private fun observeRemoval(values: Set, operation: (Set) -> Boolean): Boolean { - val removed = operation(values) - if (removed) { - onElementRemoved(values) + override fun clear() { + return observe(setOf()) { mutableSet.clear() } + } + + private fun observe(values: Set, operation: (Set) -> 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 } }