Implement undo functionality
Not quite sure about its usability though
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,19 +1,43 @@
|
|||||||
package ch.dissem.yaep.domain
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user