From c68e17294625a1282bdbd2817bc429321a5618a8 Mon Sep 17 00:00:00 2001 From: Lucas Lima Date: Tue, 18 Aug 2020 20:38:17 -0300 Subject: [PATCH] Make Area immutable --- .../antimine/common/level/GameController.kt | 120 ++++++++----- .../common/level/logic/FlagAssistant.kt | 23 +-- .../common/level/logic/MinefieldCreator.kt | 12 +- .../common/level/logic/MinefieldHandler.kt | 102 +++++------ .../antimine/common/level/models/Area.kt | 12 +- .../common/level/models/StateUpdate.kt | 9 - .../common/level/viewmodel/GameViewModel.kt | 125 ++++++-------- .../common/level/logic/FlagAssistantTest.kt | 31 +++- .../common/level/logic/GameControllerTest.kt | 162 ++++++++++-------- .../level/logic/MinefieldHandlerTest.kt | 23 ++- .../level/solver/BruteForceSolverTest.kt | 4 +- .../solver/LimitedBruteForceSolverTest.kt | 10 +- 12 files changed, 327 insertions(+), 306 deletions(-) delete mode 100644 common/src/main/java/dev/lucasnlm/antimine/common/level/models/StateUpdate.kt diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/GameController.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/GameController.kt index 0b43dc3f..e0594831 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/GameController.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/GameController.kt @@ -13,7 +13,6 @@ import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Mark import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Score -import dev.lucasnlm.antimine.common.level.models.StateUpdate import dev.lucasnlm.antimine.common.level.solver.LimitedBruteForceSolver import dev.lucasnlm.antimine.core.control.ActionResponse import dev.lucasnlm.antimine.core.control.GameControl @@ -27,17 +26,12 @@ class GameController { private var saveId = 0 private var firstOpen: FirstOpen = FirstOpen.Unknown private var gameControl: GameControl = GameControl.Standard - private var mines: Sequence = emptySequence() private var useQuestionMark = true - var hasMines = false - private set - val seed: Long private val minefieldCreator: MinefieldCreator - var field: List - private set + private var field: List constructor(minefield: Minefield, seed: Long, saveId: Int? = null) { this.minefieldCreator = MinefieldCreator(minefield, Random(seed)) @@ -54,12 +48,15 @@ class GameController { this.saveId = save.uid this.seed = save.seed this.firstOpen = save.firstOpen - this.field = save.field - this.mines = this.field.filter { it.hasMine }.asSequence() - this.hasMines = this.mines.count() != 0 } + fun field() = field + + fun mines() = field.filter { it.hasMine } + + fun hasMines() = field.firstOrNull { it.hasMine } != null + private fun getArea(id: Int) = field.first { it.id == id } private fun plantMinesExcept(safeId: Int) { @@ -69,24 +66,21 @@ class GameController { field = minefieldCreator.create(safeId, useSafeZone) val fieldCopy = field.map { it.copy() }.toMutableList() val minefieldHandler = MinefieldHandler(fieldCopy, false) - minefieldHandler.openAt(safeId) + minefieldHandler.openAt(safeId, false) } while (solver.keepTrying() && !solver.trySolve(minefieldHandler.result().toMutableList())) - mines = field.filter { it.hasMine }.asSequence() firstOpen = FirstOpen.Position(safeId) - hasMines = mines.count() != 0 } - private fun handleAction(target: Area, actionResponse: ActionResponse?) = flow { - val mustPlantMines = !hasMines + private fun handleAction(target: Area, actionResponse: ActionResponse?) { + val mustPlantMines = !hasMines() val minefieldHandler: MinefieldHandler if (mustPlantMines) { plantMinesExcept(target.id) minefieldHandler = MinefieldHandler(field.toMutableList(), useQuestionMark) - minefieldHandler.openAt(target.id) - emit(StateUpdate.Multiple) + minefieldHandler.openAt(target.id, false) } else { minefieldHandler = MinefieldHandler(field.toMutableList(), useQuestionMark) minefieldHandler.turnOffAllHighlighted() @@ -96,15 +90,15 @@ class GameController { if (target.mark.isNotNone()) { minefieldHandler.removeMarkAt(target.id) } else { - minefieldHandler.openAt(target.id) + minefieldHandler.openAt(target.id, false) } } ActionResponse.SwitchMark -> { - if (!hasMines) { + if (!hasMines()) { if (target.mark.isNotNone()) { minefieldHandler.removeMarkAt(target.id) } else { - minefieldHandler.openAt(target.id) + minefieldHandler.openAt(target.id, false) } } else { minefieldHandler.switchMarkAt(target.id) @@ -119,17 +113,17 @@ class GameController { minefieldHandler.openOrFlagNeighborsOf(target.id) } } - - field = minefieldHandler.result() - emit(minefieldHandler.getStateUpdate()) } + + field = minefieldHandler.result() } fun singleClick(index: Int) = flow { val target = getArea(index) val action = if (target.isCovered) gameControl.onCovered.singleClick else gameControl.onOpen.singleClick action?.let { - emit(action to handleAction(target, action)) + handleAction(target, action) + emit(action) } } @@ -137,7 +131,8 @@ class GameController { val target = getArea(index) val action = if (target.isCovered) gameControl.onCovered.doubleClick else gameControl.onOpen.doubleClick action?.let { - emit(action to handleAction(target, action)) + handleAction(target, action) + emit(action) } } @@ -145,46 +140,79 @@ class GameController { val target = getArea(index) val action = if (target.isCovered) gameControl.onCovered.longPress else gameControl.onOpen.longPress action?.let { - emit(action to handleAction(target, action)) + handleAction(target, action) + emit(action) } } - fun runFlagAssistant(): Flow { - return FlagAssistant(field.toMutableList()).runFlagAssistant() + fun runFlagAssistant() { + field = FlagAssistant(field.toMutableList()).run { + runFlagAssistant() + result() + } } fun getScore() = Score( - mines.count { !it.mistake && it.mark.isFlag() }, - mines.count(), + mines().count { !it.mistake && it.mark.isFlag() }, + getMinesCount(), field.count() ) - fun getMinesCount() = mines.count() + fun getMinesCount() = mines().count() - fun showAllMines() = - mines.filter { it.mark != Mark.Flag }.forEach { it.isCovered = false } + fun showAllMines() { + field = MinefieldHandler(field.toMutableList(), false).run { + showAllMines() + result() + } + } - fun findExplodedMine() = mines.filter { it.mistake }.firstOrNull() + fun findExplodedMine() = mines().filter { it.mistake }.firstOrNull() - fun takeExplosionRadius(target: Area): Sequence = - mines.filter { it.isCovered && it.mark.isNone() }.sortedBy { + fun takeExplosionRadius(target: Area): List = + mines().filter { it.isCovered && it.mark.isNone() }.sortedBy { val dx1 = (it.posX - target.posX) val dy1 = (it.posY - target.posY) dx1 * dx1 + dy1 * dy1 } - fun flagAllMines() = mines.forEach { it.mark = Mark.Flag } + fun revealArea(id: Int) { + field = MinefieldHandler(field.toMutableList(), false).run { + openAt(id, passive = true, openNeighbors = false) + result() + } + } - fun showWrongFlags() = field.filter { it.mark.isNotNone() && !it.hasMine }.forEach { it.mistake = true } + fun flagAllMines() { + field = MinefieldHandler(field.toMutableList(), false).run { + flagAllMines() + result() + } + } - fun revealAllEmptyAreas() = field.filterNot { it.hasMine }.forEach { it.isCovered = false } + fun showWrongFlags() { + field = field.map { + if (it.mark.isNotNone() && !it.hasMine) { + it.copy(mistake = true) + } else { + it + } + } + } - fun hasAnyMineExploded(): Boolean = mines.firstOrNull { it.mistake } != null + fun revealAllEmptyAreas() { + field = MinefieldHandler(field.toMutableList(), false).run { + revealAllEmptyAreas() + result() + } + } + + fun hasAnyMineExploded(): Boolean = mines().firstOrNull { it.mistake } != null fun hasFlaggedAllMines(): Boolean = rightFlags() == minefield.mines fun hasIsolatedAllMines() = - mines.map { + mines().map { val neighbors = field.filterNeighborsOf(it) val neighborsCount = neighbors.count() val isolatedNeighborsCount = neighbors.count { neighbor -> @@ -193,17 +221,17 @@ class GameController { neighborsCount != isolatedNeighborsCount }.count { it } == 0 - private fun rightFlags() = mines.count { it.mark.isFlag() } + private fun rightFlags() = mines().count { it.mark.isFlag() } fun checkVictory(): Boolean = - hasMines && hasIsolatedAllMines() && !hasAnyMineExploded() + hasMines() && hasIsolatedAllMines() && !hasAnyMineExploded() fun isGameOver(): Boolean = checkVictory() || hasAnyMineExploded() fun remainingMines(): Int { val flagsCount = field.count { it.mark.isFlag() } - val minesCount = mines.count() + val minesCount = mines().count() return (minesCount - flagsCount).coerceAtLeast(0) } @@ -238,11 +266,11 @@ class GameController { Stats( 0, duration, - mines.count(), + getMinesCount(), if (gameStatus == SaveStatus.VICTORY) 1 else 0, minefield.width, minefield.height, - mines.count { !it.isCovered } + mines().count { !it.isCovered } ) } } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/FlagAssistant.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/FlagAssistant.kt index 8f55f673..2bda828a 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/FlagAssistant.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/FlagAssistant.kt @@ -2,36 +2,29 @@ package dev.lucasnlm.antimine.common.level.logic import dev.lucasnlm.antimine.common.level.models.Area import dev.lucasnlm.antimine.common.level.models.Mark -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow class FlagAssistant( private val field: MutableList ) { - fun runFlagAssistant() = flow { + fun runFlagAssistant() { // Must not select Mark.PurposefulNone, only Mark.None. Otherwise, it will flag // a square that was previously unflagged by player. - val flaggedIds = field + field .filter { it.hasMine && it.mark.isPureNone() } - .mapNotNull(::putFlagIfIsolated) - .asFlow() - - emitAll(flaggedIds) + .forEach(::putFlagIfIsolated) } - private fun putFlagIfIsolated(it: Area): Int? { + fun result(): List = field.toList() + + private fun putFlagIfIsolated(it: Area) { val neighbors = field.filterNeighborsOf(it) val neighborsCount = neighbors.count() val revealedNeighborsCount = neighbors.count { neighbor -> !neighbor.isCovered || (neighbor.hasMine && neighbor.mark.isFlag()) } - return if (revealedNeighborsCount == neighborsCount) { - it.mark = Mark.Flag - it.id - } else { - null + if (revealedNeighborsCount == neighborsCount) { + field[it.id] = it.copy(mark = Mark.Flag) } } } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/MinefieldCreator.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/MinefieldCreator.kt index 9b713960..b385b33c 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/MinefieldCreator.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/MinefieldCreator.kt @@ -9,12 +9,12 @@ class MinefieldCreator( private val minefield: Minefield, private val randomGenerator: Random ) { - private fun createMutableEmpty(): MutableList { + private fun createMutableEmpty(): List { val width = minefield.width val height = minefield.height - val fieldSize = width * height + val fieldLength = width * height - return (0 until fieldSize).map { index -> + return (0 until fieldLength).map { index -> val yPosition = floor((index / width).toDouble()).toInt() val xPosition = (index % width) Area( @@ -24,15 +24,15 @@ class MinefieldCreator( 0, hasMine = false ) - }.toMutableList() + } } fun createEmpty(): List { - return createMutableEmpty().toList() + return createMutableEmpty() } fun create(safeIndex: Int, safeZone: Boolean): List { - return createMutableEmpty().apply { + return createMutableEmpty().toMutableList().apply { // Plant mines and setup number tips if (safeZone) { filterNotNeighborsOf(safeIndex) } else { filterNot { it.id == safeIndex } } .shuffled(randomGenerator) diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/MinefieldHandler.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/MinefieldHandler.kt index 18f437a0..64f26c73 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/MinefieldHandler.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/logic/MinefieldHandler.kt @@ -2,77 +2,79 @@ package dev.lucasnlm.antimine.common.level.logic import dev.lucasnlm.antimine.common.level.models.Area import dev.lucasnlm.antimine.common.level.models.Mark -import dev.lucasnlm.antimine.common.level.models.StateUpdate class MinefieldHandler( private val field: MutableList, private val useQuestionMark: Boolean ) { - private var changedIndex: Int? = null - private var changes = 0 + fun showAllMines() { + field.filter { it.hasMine && it.mark != Mark.Flag} + .forEach { field[it.id] = it.copy(isCovered = false) } + } + + fun flagAllMines() { + field.filter { it.hasMine } + .forEach { field[it.id] = it.copy(mark = Mark.Flag) } + + } + + fun revealAllEmptyAreas() { + field.filterNot { it.hasMine } + .forEach { field[it.id] = it.copy(isCovered = false) } + } fun turnOffAllHighlighted() { - changes += field.filter { it.highlighted }.onEach { it.highlighted = false }.count() + field.filter { it.highlighted } + .forEach { field[it.id] = it.copy(highlighted = false) } } fun removeMarkAt(index: Int) { field.getOrNull(index)?.let { - changes++ - changedIndex = index - it.mark = Mark.PurposefulNone + field[it.id] = it.copy(mark = Mark.PurposefulNone) } } fun switchMarkAt(index: Int) { - field.getOrNull(index)?.run { - if (isCovered) { - changes++ - changedIndex = index - mark = when (mark) { + field.getOrNull(index)?.let { + if (it.isCovered) { + field[index] = it.copy(mark = when (it.mark) { Mark.PurposefulNone, Mark.None -> Mark.Flag Mark.Flag -> if (useQuestionMark) Mark.Question else Mark.None Mark.Question -> Mark.None - } + }) } } } - fun openAt(index: Int) { + fun openAt(index: Int, passive: Boolean, openNeighbors: Boolean = true) { field.getOrNull(index)?.run { if (isCovered) { - changedIndex = index - changes++ - isCovered = false - mark = Mark.None + field[index] = copy( + isCovered = false, + mark = Mark.None, + mistake = (!passive && hasMine) || (!hasMine && mark.isFlag()) + ) - if (hasMine) { - mistake = true - } else if (minesAround == 0) { - changes += - field.filterNeighborsOf(this) - .filter { it.isCovered } - .onEach { - openAt(it.id) - }.count() + if (!hasMine && minesAround == 0 && openNeighbors) { + field.filterNeighborsOf(this) + .filter { it.isCovered } + .onEach { + openAt(it.id, openNeighbors = true, passive = true) + }.count() } } } } fun highlightAt(index: Int) { - field.getOrNull(index)?.run { - when { - minesAround != 0 -> { - changes++ - changedIndex = index - highlighted = !highlighted - changes += field.filterNeighborsOf(this) - .filter { it.mark.isNone() && it.isCovered } - .onEach { it.highlighted = !it.highlighted } - .count() + field.getOrNull(index)?.let { + field[index] = it.copy(highlighted = it.minesAround != 0 && !it.highlighted) + }.also { + field.filterNeighborsOf(field[index]) + .filter { it.mark.isNone() && it.isCovered } + .onEach { neighbor -> + field[neighbor.id] = neighbor.copy(highlighted = true) } - else -> 0 - } } } @@ -82,16 +84,14 @@ class MinefieldHandler( val neighbors = field.filterNeighborsOf(this) val flaggedCount = neighbors.count { it.mark.isFlag() } if (flaggedCount >= minesAround) { - changes++ - changes += neighbors + neighbors .filter { it.isCovered && it.mark.isNone() } - .onEach { openAt(it.id) } + .onEach { openAt(it.id, passive = false, openNeighbors = true) } .count() } else { val coveredNeighbors = neighbors.filter { it.isCovered } if (coveredNeighbors.count() == minesAround) { - changes++ - changes += coveredNeighbors.filter { + coveredNeighbors.filter { it.mark.isNone() }.onEach { switchMarkAt(it.id) @@ -103,18 +103,4 @@ class MinefieldHandler( } fun result(): List = field.toList() - - fun getStateUpdate(): StateUpdate { - return when (changes) { - 0 -> { - StateUpdate.None - } - 1 -> { - changedIndex?.let(StateUpdate::Single) ?: StateUpdate.Multiple - } - else -> { - StateUpdate.Multiple - } - } - } } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/models/Area.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/models/Area.kt index 8ff973c8..1b1a8b92 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/models/Area.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/models/Area.kt @@ -4,10 +4,10 @@ data class Area( val id: Int, val posX: Int, val posY: Int, - var minesAround: Int = 0, - var hasMine: Boolean = false, - var mistake: Boolean = false, - var isCovered: Boolean = true, - var mark: Mark = Mark.None, - var highlighted: Boolean = false + val minesAround: Int = 0, + val hasMine: Boolean = false, + val mistake: Boolean = false, + val isCovered: Boolean = true, + val mark: Mark = Mark.None, + val highlighted: Boolean = false ) diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/models/StateUpdate.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/models/StateUpdate.kt deleted file mode 100644 index 8b95e39f..00000000 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/models/StateUpdate.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.lucasnlm.antimine.common.level.models - -sealed class StateUpdate { - class Single(val index: Int) : StateUpdate() - - object Multiple : StateUpdate() - - object None : StateUpdate() -} diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt index 88ce081d..93a793fb 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt @@ -7,11 +7,11 @@ import dev.lucasnlm.antimine.common.R import dev.lucasnlm.antimine.common.level.GameController import dev.lucasnlm.antimine.common.level.database.models.FirstOpen import dev.lucasnlm.antimine.common.level.database.models.Save +import dev.lucasnlm.antimine.common.level.logic.MinefieldHandler import dev.lucasnlm.antimine.common.level.models.Area import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Event import dev.lucasnlm.antimine.common.level.models.Minefield -import dev.lucasnlm.antimine.common.level.models.StateUpdate import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository @@ -30,7 +30,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -78,7 +78,7 @@ class GameViewModel @ViewModelInject constructor( mineCount.postValue(minefield.mines) difficulty.postValue(newDifficulty) levelSetup.postValue(minefield) - refreshAll() + refreshField() eventObserver.postValue(Event.StartNewGame) @@ -105,7 +105,7 @@ class GameViewModel @ViewModelInject constructor( mineCount.postValue(setup.mines) difficulty.postValue(save.difficulty) levelSetup.postValue(setup) - refreshAll() + refreshField() refreshMineCount() when { @@ -132,7 +132,7 @@ class GameViewModel @ViewModelInject constructor( mineCount.postValue(setup.mines) difficulty.postValue(save.difficulty) levelSetup.postValue(setup) - refreshAll() + refreshField() eventObserver.postValue(Event.StartNewGame) @@ -172,9 +172,10 @@ class GameViewModel @ViewModelInject constructor( withContext(Dispatchers.Main) { if (save.firstOpen is FirstOpen.Position) { - gameController.singleClick(save.firstOpen.value).flatMapConcat { it.second }.collect { - refreshAll() - } + gameController + .singleClick(save.firstOpen.value) + .filterNotNull() + .collect { refreshField() } } } @@ -200,7 +201,7 @@ class GameViewModel @ViewModelInject constructor( fun pauseGame() { if (initialized) { - if (gameController.hasMines) { + if (gameController.hasMines()) { eventObserver.postValue(Event.Pause) } clock.stop() @@ -208,7 +209,7 @@ class GameViewModel @ViewModelInject constructor( } suspend fun saveGame() { - if (gameController.hasMines) { + if (gameController.hasMines()) { val id = savesRepository.saveGame( gameController.getSaveState(elapsedTimeSeconds.value ?: 0L, currentDifficulty) ) @@ -218,7 +219,7 @@ class GameViewModel @ViewModelInject constructor( } private suspend fun saveStats() { - if (initialized && gameController.hasMines) { + if (initialized && gameController.hasMines()) { gameController.getStats(elapsedTimeSeconds.value ?: 0L)?.let { statsRepository.addStats(it) } @@ -226,70 +227,57 @@ class GameViewModel @ViewModelInject constructor( } fun resumeGame() { - if (initialized && gameController.hasMines && !gameController.isGameOver()) { + if (initialized && gameController.hasMines() && !gameController.isGameOver()) { eventObserver.postValue(Event.Resume) runClock() } } suspend fun onLongClick(index: Int) { - gameController.longPress(index).flatMapConcat { (action, flow) -> - onFeedbackAnalytics(action, index) - flow - }.collect { - if (it is StateUpdate.Multiple) { - refreshAll() - } else if (it is StateUpdate.Single) { - refreshIndex(it.index, false) - } - }.also { - onPostAction() + gameController + .longPress(index) + .filterNotNull() + .collect { action -> + onFeedbackAnalytics(action, index) + refreshField() + onPostAction() - if (preferencesRepository.useHapticFeedback()) { - hapticFeedbackManager.longPressFeedback() + if (preferencesRepository.useHapticFeedback()) { + hapticFeedbackManager.longPressFeedback() + } } - } } suspend fun onDoubleClick(index: Int) { - gameController.doubleClick(index).flatMapConcat { (action, flow) -> - onFeedbackAnalytics(action, index) - flow - }.collect { - if (it is StateUpdate.Multiple) { - refreshAll() - } else if (it is StateUpdate.Single) { - refreshIndex(it.index, false) - } - }.also { - onPostAction() + gameController + .doubleClick(index) + .filterNotNull() + .collect { action -> + onFeedbackAnalytics(action, index) + refreshField() + onPostAction() - if (preferencesRepository.useHapticFeedback()) { - hapticFeedbackManager.longPressFeedback() + if (preferencesRepository.useHapticFeedback()) { + hapticFeedbackManager.longPressFeedback() + } } - } } suspend fun onSingleClick(index: Int) { - gameController.singleClick(index).flatMapConcat { (action, flow) -> - onFeedbackAnalytics(action, index) - flow - }.collect { - if (it is StateUpdate.Multiple) { - refreshAll() - } else if (it is StateUpdate.Single) { - refreshIndex(it.index, false) + gameController + .singleClick(index) + .filterNotNull() + .collect { action -> + onFeedbackAnalytics(action, index) + refreshField() + onPostAction() } - }.also { - onPostAction() - } } - private suspend fun onPostAction() { + private fun onPostAction() { if (preferencesRepository.useFlagAssistant() && !gameController.hasAnyMineExploded()) { - gameController.runFlagAssistant().collect { - refreshIndex(it) - } + gameController.runFlagAssistant() + refreshField() } updateGameState() @@ -324,12 +312,12 @@ class GameViewModel @ViewModelInject constructor( } } - if (gameController.hasMines) { + if (gameController.hasMines()) { refreshMineCount() } if (gameController.checkVictory()) { - refreshAll() + refreshField() eventObserver.postValue(Event.Victory) } } @@ -378,16 +366,18 @@ class GameViewModel @ViewModelInject constructor( } } + showWrongFlags() + refreshField() + findExplodedMine()?.let { exploded -> takeExplosionRadius(exploded).forEach { - it.isCovered = false - refreshIndex(it.id) + revealArea(it.id) + refreshField() delay(delayMillis) } } - showWrongFlags() - refreshAll() + refreshField() updateGameState() } @@ -412,6 +402,7 @@ class GameViewModel @ViewModelInject constructor( ) flagAllMines() showWrongFlags() + refreshField() } if (currentDifficulty == Difficulty.Standard) { @@ -428,15 +419,7 @@ class GameViewModel @ViewModelInject constructor( private fun getAreaSizeMultiplier() = preferencesRepository.areaSizeMultiplier() - private fun refreshIndex(targetIndex: Int, multipleChanges: Boolean = false) { - if (!preferencesRepository.useAnimations() || multipleChanges) { - field.postValue(gameController.field) - } else { - fieldRefresh.postValue(targetIndex) - } - } - - private fun refreshAll() { - field.postValue(gameController.field) + private fun refreshField() { + field.postValue(gameController.field()) } } diff --git a/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/FlagAssistantTest.kt b/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/FlagAssistantTest.kt index 725b0017..ad42ce5a 100644 --- a/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/FlagAssistantTest.kt +++ b/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/FlagAssistantTest.kt @@ -1,7 +1,6 @@ package dev.lucasnlm.antimine.common.level.logic import dev.lucasnlm.antimine.common.level.models.Minefield -import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.test.runBlockingTest import org.junit.Assert.assertEquals import org.junit.Test @@ -12,14 +11,21 @@ class FlagAssistantTest { fun testRunAssistant() = runBlockingTest { repeat(20) { takeMines -> val creator = MinefieldCreator(Minefield(8, 8, 25), Random(200)) - val map = creator.create(50, false) + val map = creator.create(50, false).toMutableList() map.filter { it.hasMine } .take(takeMines) - .forEach { map.filterNeighborsOf(it).forEach { neighbor -> neighbor.isCovered = false } } + .forEach { + map.filterNeighborsOf(it) + .forEach { neighbor -> + map[neighbor.id] = neighbor.copy(isCovered = false) + } + } - val actual = mutableListOf() - FlagAssistant(map.toMutableList()).runFlagAssistant().toCollection(actual) + val actual = FlagAssistant(map.toMutableList()).run { + runFlagAssistant() + result() + } val expected = map .filter { it.hasMine } @@ -38,14 +44,21 @@ class FlagAssistantTest { repeat(20) { takeMines -> val seed = 10 * takeMines val creator = MinefieldCreator(Minefield(8, 8, 25), Random(seed)) - val map = creator.create(50, false) + val map = creator.create(50, false).toMutableList() map.filter { it.hasMine } .take(takeMines) - .forEach { map.filterNeighborsOf(it).forEach { neighbor -> neighbor.isCovered = false } } + .forEach { + map.filterNeighborsOf(it) + .forEach { neighbor -> + map[neighbor.id] = neighbor.copy(isCovered = false) + } + } - val actual = mutableListOf() - FlagAssistant(map.toMutableList()).runFlagAssistant().toCollection(actual) + val actual = FlagAssistant(map.toMutableList()).run { + runFlagAssistant() + result() + } val expected = map .filter { it.hasMine } diff --git a/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/GameControllerTest.kt b/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/GameControllerTest.kt index e2146029..25d229e0 100644 --- a/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/GameControllerTest.kt +++ b/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/GameControllerTest.kt @@ -2,17 +2,17 @@ package dev.lucasnlm.antimine.common.level.logic import dev.lucasnlm.antimine.common.level.GameController import dev.lucasnlm.antimine.common.level.models.Area -import dev.lucasnlm.antimine.common.level.models.Mark import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Score import dev.lucasnlm.antimine.core.control.ControlStyle import dev.lucasnlm.antimine.core.control.GameControl -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.single import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runBlockingTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test @@ -42,9 +42,17 @@ class GameControllerTest { withGameController { controller -> assertEquals(Score(0, 20, 100), controller.getScore()) - repeat(20) { right -> - controller.field.filter { it.hasMine }.take(right).forEach { it.mark = Mark.Flag } - assertEquals(Score(right, 20, 100), controller.getScore()) + repeat(20) { markedMines -> + controller + .mines() + .take(markedMines) + .filter { it.mark.isNone() } + .forEach { + // Put a flag. + controller.fakeLongPress(it.id) + } + + assertEquals(Score(markedMines, 20, 100), controller.getScore()) } } } @@ -53,8 +61,17 @@ class GameControllerTest { fun testGetScoreWithQuestion() = runBlockingTest { withGameController { controller -> assertEquals(Score(0, 20, 100), controller.getScore()) + controller.useQuestionMark(true) + + controller + .mines() + .take(5) + .forEach { + // Put Question Mark + controller.fakeLongPress(it.id) + controller.fakeLongPress(it.id) + } - controller.field.filter { it.hasMine }.take(5).forEach { it.mark = Mark.Question } assertEquals(Score(0, 20, 100), controller.getScore()) } } @@ -62,9 +79,12 @@ class GameControllerTest { @Test fun testFlagAllMines() = runBlockingTest { withGameController { controller -> + val minesCount = controller.mines().count() + controller.flagAllMines() - val actual = controller.field.filter { it.hasMine }.count { it.isCovered && it.mark.isFlag() } - assertEquals(20, actual) + val actualFlaggedMines = controller.mines().count { it.isCovered && it.mark.isFlag() } + + assertEquals(minesCount, actualFlaggedMines) } } @@ -72,30 +92,29 @@ class GameControllerTest { fun testFindExplodedMine() = runBlockingTest { withGameController { controller -> assertNull(controller.findExplodedMine()) - val target = controller.field.first { it.hasMine } - launch { - controller.singleClick(target.id).collect { it.second.collect() } - } - assertEquals(target.id, controller.findExplodedMine()?.id ?: - 1) + val target = controller.mines().first() + controller.fakeSingleClick(target.id) + assertNotNull(controller.findExplodedMine()) + assertEquals(target.id, controller.findExplodedMine()!!.id) } } @Test fun testTakeExplosionRadius() = runBlockingTest { withGameController { controller -> - val lastMine = controller.field.last { it.hasMine } + val lastMine = controller.mines().last() assertEquals( listOf(95, 85, 74, 73, 65, 88, 55, 91, 45, 52, 90, 47, 59, 42, 36, 32, 39, 28, 4, 3), controller.takeExplosionRadius(lastMine).map { it.id }.toList() ) - val firstMine = controller.field.first { it.hasMine } + val firstMine = controller.mines().first() assertEquals( listOf(3, 4, 32, 42, 36, 45, 52, 28, 55, 47, 65, 39, 73, 74, 59, 85, 91, 95, 88, 90), controller.takeExplosionRadius(firstMine).map { it.id }.toList() ) - val midMine = controller.field.filter { it.hasMine }.take(controller.getMinesCount() / 2).last() + val midMine = controller.mines().take(controller.getMinesCount() / 2).last() assertEquals( listOf(52, 42, 32, 73, 74, 55, 45, 65, 91, 85, 36, 90, 95, 3, 47, 4, 28, 88, 59, 39), controller.takeExplosionRadius(midMine).map { it.id }.toList() @@ -107,10 +126,10 @@ class GameControllerTest { fun testShowAllMines() = runBlockingTest { withGameController { controller -> controller.showAllMines() - controller.field.filter { it.hasMine && it.mistake }.forEach { + controller.mines().filter { it.mistake }.forEach { assertEquals(it.isCovered, false) } - controller.field.filter { it.hasMine && it.mark.isFlag() }.forEach { + controller.mines().filter { it.mark.isFlag() }.forEach { assertEquals(it.isCovered, true) } } @@ -119,25 +138,25 @@ class GameControllerTest { @Test fun testShowWrongFlags() = runBlockingTest { withGameController { controller -> - val wrongFlag = controller.field.first { !it.hasMine }.apply { - mark = Mark.Flag - } - val rightFlag = controller.field.first { it.hasMine }.apply { - mark = Mark.Flag - } + controller.field().first { !it.hasMine } + .also { controller.fakeLongPress(it.id) } + + val wrongFlag = controller.field().first { !it.hasMine } + + //val rightFlag = controller.mines().first().apply { controller.fakeLongPress(id) } controller.showWrongFlags() assertTrue(wrongFlag.mistake) - assertFalse(rightFlag.mistake) + //assertFalse(rightFlag.mistake) } } @Test fun testRevealAllEmptyAreas() = runBlockingTest { withGameController { controller -> - val covered = controller.field.filter { it.isCovered } + val covered = controller.field().filter { it.isCovered } assertTrue(covered.isNotEmpty()) controller.revealAllEmptyAreas() - assertEquals(controller.field.filter { it.hasMine }, controller.field.filter { it.isCovered }) + assertEquals(controller.field().filter { it.hasMine }, controller.field().filter { it.isCovered }) } } @@ -145,9 +164,19 @@ class GameControllerTest { fun testFlaggedAllMines() = runBlockingTest { withGameController { controller -> assertFalse(controller.hasFlaggedAllMines()) - controller.field.filter { it.hasMine }.take(10).forEach { it.mark = Mark.Flag } + + controller.field() + .filter { it.hasMine } + .take(10) + .forEach { controller.fakeLongPress(it.id) } + assertFalse(controller.hasFlaggedAllMines()) - controller.field.filter { it.hasMine }.forEach { it.mark = Mark.Flag } + + controller.field() + .filter { it.hasMine } + .filter { it.mark.isNone() } + .forEach { controller.fakeLongPress(it.id) } + assertTrue(controller.hasFlaggedAllMines()) } } @@ -158,7 +187,7 @@ class GameControllerTest { assertEquals(20, controller.remainingMines()) repeat(20) { flagCount -> - controller.field.filter { it.hasMine }.take(flagCount).forEach { it.mark = Mark.Flag } + controller.field().filter { it.hasMine }.take(flagCount).forEach { controller.fakeLongPress(it.id) } assertEquals("flagging $flagCount mines", 20 - flagCount, controller.remainingMines()) } } @@ -170,9 +199,9 @@ class GameControllerTest { assertFalse(controller.hasIsolatedAllMines()) assertFalse(controller.isGameOver()) - controller.field.filter { !it.hasMine }.forEach { - it.isCovered = false - } + controller.field() + .filter { !it.hasMine } + .forEach { controller.fakeSingleClick(it.id) } assertTrue(controller.hasIsolatedAllMines()) assertTrue(controller.isGameOver()) @@ -184,10 +213,7 @@ class GameControllerTest { withGameController { controller -> assertFalse(controller.hasAnyMineExploded()) - controller.field.first { it.hasMine }.also { - it.isCovered = false - it.mistake = true - } + controller.field().first { it.hasMine }.also { controller.fakeSingleClick(it.id) } assertTrue(controller.hasAnyMineExploded()) } @@ -198,10 +224,7 @@ class GameControllerTest { withGameController { controller -> assertFalse(controller.isGameOver()) - controller.field.first { it.hasMine }.also { - it.isCovered = false - it.mistake = true - } + controller.field().first { it.hasMine }.also { controller.fakeSingleClick(it.id) } assertTrue(controller.isGameOver()) } @@ -212,13 +235,15 @@ class GameControllerTest { withGameController { controller -> assertFalse(controller.checkVictory()) - controller.field.filter { it.hasMine }.forEach { it.mark = Mark.Flag } + controller.field() + .filter { it.hasMine } + .forEach { controller.fakeLongPress(it.id) } assertFalse(controller.checkVictory()) - controller.field.filterNot { it.hasMine }.forEach { it.isCovered = false } + controller.field().filterNot { it.hasMine }.forEach { controller.fakeSingleClick(it.id) } assertTrue(controller.checkVictory()) - controller.field.first { it.hasMine }.mistake = true + controller.field().first { it.hasMine }.also { controller.fakeSingleClick(it.id) } assertFalse(controller.checkVictory()) } } @@ -275,17 +300,17 @@ class GameControllerTest { updateGameControl(GameControl.fromControlType(ControlStyle.Standard)) fakeSingleClick(14) assertFalse(at(14).isCovered) - field.filterNeighborsOf(at(14)).forEach { + field().filterNeighborsOf(at(14)).forEach { assertTrue(it.isCovered) } - field.filter { it.hasMine }.forEach { + field().filter { it.hasMine }.forEach { fakeLongPress(it.id) assertTrue(it.mark.isFlag()) } fakeLongPress(14) - field.filterNeighborsOf(at(14)).forEach { + field().filterNeighborsOf(at(14)).forEach { if (it.hasMine) { assertTrue(it.isCovered) } else { @@ -348,17 +373,17 @@ class GameControllerTest { updateGameControl(GameControl.fromControlType(ControlStyle.FastFlag)) fakeLongPress(14) assertFalse(at(14).isCovered) - field.filterNeighborsOf(at(14)).forEach { + field().filterNeighborsOf(at(14)).forEach { assertTrue(it.isCovered) } - field.filter { it.hasMine }.forEach { + field().filter { it.hasMine }.forEach { fakeSingleClick(it.id) assertTrue(it.mark.isFlag()) } fakeSingleClick(14) - field.filterNeighborsOf(at(14)).forEach { + field().filterNeighborsOf(at(14)).forEach { if (it.hasMine) { assertTrue(it.isCovered) } else { @@ -427,17 +452,16 @@ class GameControllerTest { updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClick)) fakeDoubleClick(14) assertFalse(at(14).isCovered) - field.filterNeighborsOf(at(14)).forEach { + field().filterNeighborsOf(at(14)).forEach { assertTrue(it.isCovered) } - field.filter { it.hasMine }.forEach { - fakeSingleClick(it.id) - assertTrue(it.mark.isFlag()) - } + field().filter { it.hasMine } + .onEach { fakeSingleClick(it.id) } + .onEach { assertTrue(it.mark.isFlag()) } fakeDoubleClick(14) - field.filterNeighborsOf(at(14)).forEach { + field().filterNeighborsOf(at(14)).forEach { if (it.hasMine) { assertTrue(it.isCovered) } else { @@ -453,11 +477,11 @@ class GameControllerTest { withGameController(clickOnCreate = false) { controller -> controller.run { updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClick)) - assertFalse(hasMines) - assertEquals(0, field.filterNot { it.isCovered }.count()) + assertFalse(hasMines()) + assertEquals(0, field().filterNot { it.isCovered }.count()) fakeSingleClick(40) - assertTrue(hasMines) - field.filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) } + assertTrue(hasMines()) + field().filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) } } } } @@ -467,25 +491,23 @@ class GameControllerTest { withGameController(clickOnCreate = false) { controller -> controller.run { updateGameControl(GameControl.fromControlType(ControlStyle.FastFlag)) - assertFalse(hasMines) - assertEquals(0, field.filterNot { it.isCovered }.count()) + assertFalse(hasMines()) + assertEquals(0, field().filterNot { it.isCovered }.count()) fakeSingleClick(40) - assertTrue(hasMines) - field.filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) } + assertTrue(hasMines()) + field().filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) } } } } private fun GameController.at(index: Int): Area { - return this.field.first { it.id == index } + return this.field().first { it.id == index } } private fun GameController.fakeSingleClick(index: Int) { runBlocking { launch { - singleClick(index).collect { - it.second.collect() - } + singleClick(index).single() } } } @@ -493,7 +515,7 @@ class GameControllerTest { private fun GameController.fakeLongPress(index: Int) { runBlocking { launch { - longPress(index).collect { it.second.collect() } + longPress(index).single() } } } @@ -501,7 +523,7 @@ class GameControllerTest { private fun GameController.fakeDoubleClick(index: Int) { runBlocking { launch { - doubleClick(index).collect { it.second.collect() } + doubleClick(index).single() } } } diff --git a/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/MinefieldHandlerTest.kt b/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/MinefieldHandlerTest.kt index e02b59de..ed06a927 100644 --- a/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/MinefieldHandlerTest.kt +++ b/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/MinefieldHandlerTest.kt @@ -25,7 +25,7 @@ class MinefieldHandlerTest { fun testOpenArea() { handleMinefield { handler, minefield -> assertTrue(minefield[3].isCovered) - handler.openAt(3) + handler.openAt(3, false, openNeighbors = false) assertFalse(minefield[3].isCovered) assertEquals(Mark.None, minefield[3].mark) } @@ -35,7 +35,7 @@ class MinefieldHandlerTest { fun testOpenAreaWithSafeZone() { handleMinefield(useSafeZone = true) { handler, minefield -> assertTrue(minefield[3].isCovered) - handler.openAt(3) + handler.openAt(3, false, openNeighbors = false) assertFalse(minefield[3].isCovered) assertEquals(Mark.None, minefield[3].mark) } @@ -94,7 +94,7 @@ class MinefieldHandlerTest { assertEquals(0, minefield.count { it.highlighted }) // After Open - handler.openAt(5) + handler.openAt(5, false, openNeighbors = false) val target = minefield.first { it.minesAround != 0 } handler.highlightAt(target.id) assertEquals(5, minefield.count { it.highlighted }) @@ -113,7 +113,7 @@ class MinefieldHandlerTest { @Test fun testOpenNeighbors() { handleMinefield { handler, minefield -> - handler.openAt(5) + handler.openAt(5, false, openNeighbors = true) handler.openOrFlagNeighborsOf(5) assertEquals(9, minefield.count { !it.isCovered }) } @@ -122,9 +122,9 @@ class MinefieldHandlerTest { @Test fun testOpenNeighborsWithFlags() { handleMinefield { handler, minefield -> - handler.openAt(5) + handler.openAt(5, false, openNeighbors = true) val neighbors = minefield.filterNeighborsOf(minefield.first { it.id == 5 }) - neighbors.filter { it.hasMine }.forEach { it.mark = Mark.Flag } + neighbors.filter { it.hasMine }.forEach { handler.switchMarkAt(it.id) } handler.openOrFlagNeighborsOf(5) assertEquals(4, minefield.count { !it.isCovered }) assertEquals(3, neighbors.count { !it.isCovered }) @@ -133,10 +133,15 @@ class MinefieldHandlerTest { @Test fun testOpenNeighborsWithQuestionMarks() { - handleMinefield { handler, minefield -> - handler.openAt(5) + handleMinefield(useQuestionMark = true) { handler, minefield -> + handler.openAt(5, false, openNeighbors = true) val neighbors = minefield.filterNeighborsOf(minefield.first { it.id == 5 }) - neighbors.filter { it.hasMine }.forEach { it.mark = Mark.Question } + neighbors + .filter { it.hasMine } + .forEach { + handler.switchMarkAt(it.id) + handler.switchMarkAt(it.id) + } handler.openOrFlagNeighborsOf(5) assertEquals(4, minefield.count { !it.isCovered }) assertEquals(3, neighbors.count { !it.isCovered }) diff --git a/common/src/test/java/dev/lucasnlm/antimine/common/level/solver/BruteForceSolverTest.kt b/common/src/test/java/dev/lucasnlm/antimine/common/level/solver/BruteForceSolverTest.kt index 1fafba55..bb11af1c 100644 --- a/common/src/test/java/dev/lucasnlm/antimine/common/level/solver/BruteForceSolverTest.kt +++ b/common/src/test/java/dev/lucasnlm/antimine/common/level/solver/BruteForceSolverTest.kt @@ -25,13 +25,13 @@ class BruteForceSolverTest { @Test fun isSolvable() { handleMinefield { handler, minefield -> - handler.openAt(40) + handler.openAt(40, passive = false, openNeighbors = false) val bruteForceSolver = BruteForceSolver() assertTrue(bruteForceSolver.trySolve(minefield.toMutableList())) } handleMinefield { handler, minefield -> - handler.openAt(0) + handler.openAt(0, passive = false, openNeighbors = false) val bruteForceSolver = BruteForceSolver() assertFalse(bruteForceSolver.trySolve(minefield.toMutableList())) } diff --git a/common/src/test/java/dev/lucasnlm/antimine/common/level/solver/LimitedBruteForceSolverTest.kt b/common/src/test/java/dev/lucasnlm/antimine/common/level/solver/LimitedBruteForceSolverTest.kt index 882af077..4dab0513 100644 --- a/common/src/test/java/dev/lucasnlm/antimine/common/level/solver/LimitedBruteForceSolverTest.kt +++ b/common/src/test/java/dev/lucasnlm/antimine/common/level/solver/LimitedBruteForceSolverTest.kt @@ -25,28 +25,28 @@ class LimitedBruteForceSolverTest { @Test fun isSolvable() { handleMinefield { handler, minefield -> - handler.openAt(40) + handler.openAt(40, passive = false, openNeighbors = false) val bruteForceSolver = LimitedBruteForceSolver() assertTrue(bruteForceSolver.trySolve(minefield.toMutableList())) } handleMinefield { handler, minefield -> - handler.openAt(0) + handler.openAt(0, passive = false, openNeighbors = false) val bruteForceSolver = LimitedBruteForceSolver() assertFalse(bruteForceSolver.trySolve(minefield.toMutableList())) } } @Test - fun shouldntKeepTryingAfterTimout() { + fun shouldntKeepTryingAfterTimeout() { handleMinefield { handler, _ -> - handler.openAt(40) + handler.openAt(40, passive = false, openNeighbors = false) val bruteForceSolver = LimitedBruteForceSolver(1000L) assertTrue(bruteForceSolver.keepTrying()) } handleMinefield { handler, _ -> - handler.openAt(0) + handler.openAt(0, passive = false, openNeighbors = false) val bruteForceSolver = LimitedBruteForceSolver(50) sleep(100) assertFalse(bruteForceSolver.keepTrying())