From e91cbf285eb01ef138f1895b48731216a9ee430e Mon Sep 17 00:00:00 2001 From: Lucas Lima Date: Fri, 26 Jun 2020 00:16:06 -0300 Subject: [PATCH] Add custom control support --- .../mocks/DisabledHapticFeedbackInteractor.kt | 2 +- .../antimine/common/level/LevelFacade.kt | 151 +++++++++--------- .../level/utils/HapticFeedbackInteractor.kt | 6 +- .../common/level/viewmodel/GameViewModel.kt | 103 ++++-------- .../core/analytics/models/Analytics.kt | 9 +- .../antimine/core/control/GameControl.kt | 38 +++++ .../antimine/common/level/LevelFacadeTest.kt | 8 +- 7 files changed, 155 insertions(+), 162 deletions(-) create mode 100644 common/src/main/java/dev/lucasnlm/antimine/core/control/GameControl.kt diff --git a/app/src/test/java/dev/lucasnlm/antimine/mocks/DisabledHapticFeedbackInteractor.kt b/app/src/test/java/dev/lucasnlm/antimine/mocks/DisabledHapticFeedbackInteractor.kt index 6102592f..142df399 100644 --- a/app/src/test/java/dev/lucasnlm/antimine/mocks/DisabledHapticFeedbackInteractor.kt +++ b/app/src/test/java/dev/lucasnlm/antimine/mocks/DisabledHapticFeedbackInteractor.kt @@ -3,7 +3,7 @@ package dev.lucasnlm.antimine.mocks import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor class DisabledHapticFeedbackInteractor : IHapticFeedbackInteractor { - override fun toggleFlagFeedback() { } + override fun longPressFeedback() { } override fun explosionFeedback() { } } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt index 79d7659d..df9058c5 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt @@ -9,6 +9,9 @@ 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.core.control.Action +import dev.lucasnlm.antimine.core.control.ActionFeedback +import dev.lucasnlm.antimine.core.control.GameControl import kotlin.math.floor import kotlin.random.Random @@ -18,6 +21,7 @@ class LevelFacade { private val startTime = System.currentTimeMillis() private var saveId = 0 private var firstOpen: FirstOpen = FirstOpen.Unknown + private val gameControl: GameControl = GameControl.Standard var hasMines = false private set @@ -66,8 +70,43 @@ class LevelFacade { fun getArea(id: Int) = field.first { it.id == id } - fun switchMarkAt(index: Int): Area = - getArea(index).apply { + /** + * Run a game [action] on a given tile. + * @return The number of changed tiles. + */ + private fun Area.runActionOn(action: Action?): ActionFeedback { + val highlightedChanged = turnOffAllHighlighted() + + val changed = when (action) { + Action.OpenTile -> { + if (!hasMines) { + plantMinesExcept(id, true) + } + + if (mark.isNotNone()) { + mark = Mark.PurposefulNone + 1 + } else { + openTile() + } + } + Action.SwitchMark -> { + if (isCovered) switchMark() + 1 + } + Action.HighlightNeighbors -> { + if (minesAround != 0) highlight() else 0 + } + Action.OpenNeighbors -> { + if (!isCovered) { openNeighbors() } else { 0 } + } + else -> 0 + } + + return ActionFeedback(action, id, (changed + highlightedChanged) > 1) + } + + fun Area.switchMark(): Area = apply { if (isCovered) { mark = when (mark) { Mark.PurposefulNone, Mark.None -> Mark.Flag @@ -82,14 +121,6 @@ class LevelFacade { mark = Mark.PurposefulNone } - fun hasCoverOn(index: Int): Boolean = getArea(index).isCovered - - fun hasMarkOn(index: Int): Boolean = getArea(index).mark.isNotNone() - - fun hasNoneOn(index: Int): Boolean = getArea(index).mark.isNone() - - fun isHighlighted(index: Int): Boolean = getArea(index).highlighted - fun plantMinesExcept(index: Int, includeSafeArea: Boolean = false) { plantRandomMines(index, includeSafeArea) putMinesTips() @@ -136,9 +167,9 @@ class LevelFacade { /** * Run "Flood Fill algorithm" to open all empty neighbors of a target area. */ - fun openField(target: Area): Int { + fun Area.openTile(): Int { var changes = 0 - target.run { + run { if (isCovered) { changes += 1 isCovered = false @@ -152,7 +183,7 @@ class LevelFacade { .also { changes += it.count() } - .forEach { openField(it) } + .forEach { it.openTile() } } } } @@ -161,16 +192,13 @@ class LevelFacade { /** * Disable all highlighted areas. - * - * @return true if any area was changed. + * @return the number of changed tiles. */ - fun turnOffAllHighlighted(): Boolean { - var changed: Boolean - field - .filter { it.highlighted } - .also { changed = it.count() != 0 } - .forEach { it.highlighted = false } - return changed + fun turnOffAllHighlighted(): Int { + return field.filter { it.highlighted }.run { + forEach { it.highlighted = false } + count() + } } private fun toggleHighlight(target: Area): Int { @@ -185,34 +213,8 @@ class LevelFacade { return changed } - /** - * Open a given area by its index. - * - * @param index the target index - * @return true if multiple areas were open - */ - fun singleClick(index: Int): Int = getArea(index).run { - return when { - isCovered -> { - openField(getArea(index)) - } - minesAround != 0 -> { - toggleHighlight(this) - } - else -> 0 - } - } - fun doubleClick(index: Int): Int = getArea(index).run { - return when { - isCovered -> { - openField(getArea(index)) - } - else -> 0 - } - } - - fun highlight(index: Int): Int = getArea(index).run { + private fun Area.highlight(): Int = run { return when { minesAround != 0 -> { toggleHighlight(this) @@ -221,41 +223,42 @@ class LevelFacade { } } - @Suppress("unused") - fun longPressOpenArea(index: Int): Sequence { - val neighbors = getArea(index).findNeighbors().filter { - it.mark.isNone() && it.isCovered - } - - val neighborsCount = neighbors.count() - val minesCount = neighbors.count { it.hasMine } - - if (neighborsCount == minesCount) { - neighbors.forEach { area -> area.mark = Mark.Flag } + fun singleClick(index: Int): ActionFeedback = getArea(index).run { + return if (isCovered) { + runActionOn(gameControl.onCovered.singleClick) } else { - neighbors.forEach { area -> openField(area) } + runActionOn(gameControl.onOpen.singleClick) } - - return neighbors } - /** - * Open all tiles neighbors of [index]. - * If the number of flagged neighbors is less than the number of neighbors - * it won't open any of them to avoid miss clicks. - */ - fun openNeighbors(index: Int): Sequence { - val neighbors = getArea(index).findNeighbors() + fun doubleClick(index: Int): ActionFeedback = getArea(index).run { + return if (isCovered) { + runActionOn(gameControl.onCovered.doubleClick) + } else { + runActionOn(gameControl.onOpen.doubleClick) + } + } + + fun longPress(index: Int): ActionFeedback = getArea(index).run { + return if (isCovered) { + runActionOn(gameControl.onCovered.longPress) + } else { + runActionOn(gameControl.onOpen.longPress) + } + } + + fun Area.openNeighbors(): Int { + val neighbors = findNeighbors() val flaggedCount = neighbors.count { it.mark.isFlag() } - return if (flaggedCount >= getArea(index).minesAround) { + return if (flaggedCount >= minesAround) { neighbors .filter { it.mark.isNone() && it.isCovered }.also { - it.forEach { area -> openField(area) } - } + it.forEach { area -> area.openTile() } + }.count() } else { - sequenceOf() + 0 } } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/utils/HapticFeedbackInteractor.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/utils/HapticFeedbackInteractor.kt index c184e141..5963c8c1 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/utils/HapticFeedbackInteractor.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/utils/HapticFeedbackInteractor.kt @@ -8,7 +8,7 @@ import android.os.Vibrator import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository interface IHapticFeedbackInteractor { - fun toggleFlagFeedback() + fun longPressFeedback() fun explosionFeedback() } @@ -18,7 +18,7 @@ class HapticFeedbackInteractor( ) : IHapticFeedbackInteractor { private val vibrator: Vibrator = context.getSystemService(VIBRATOR_SERVICE) as Vibrator - override fun toggleFlagFeedback() { + override fun longPressFeedback() { if (preferencesRepository.useHapticFeedback()) { vibrateTo(70, 240) vibrateTo(10, 100) @@ -44,7 +44,7 @@ class HapticFeedbackInteractor( } class DisabledIHapticFeedbackInteractor : IHapticFeedbackInteractor { - override fun toggleFlagFeedback() { } + override fun longPressFeedback() { } override fun explosionFeedback() { } } 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 3c4ebcc3..8e433557 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 @@ -19,6 +19,8 @@ import dev.lucasnlm.antimine.common.level.utils.Clock import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor import dev.lucasnlm.antimine.core.analytics.AnalyticsManager import dev.lucasnlm.antimine.core.analytics.models.Analytics +import dev.lucasnlm.antimine.core.control.Action +import dev.lucasnlm.antimine.core.control.ActionFeedback import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -40,7 +42,6 @@ class GameViewModel @ViewModelInject constructor( private lateinit var levelFacade: LevelFacade private var currentDifficulty: Difficulty = Difficulty.Standard private var initialized = false - private var oldGame = false val field = MutableLiveData>() val fieldRefresh = MutableLiveData() @@ -147,7 +148,6 @@ class GameViewModel @ViewModelInject constructor( startNewGame() }.also { initialized = true - oldGame = true } } @@ -163,7 +163,6 @@ class GameViewModel @ViewModelInject constructor( startNewGame() }.also { initialized = true - oldGame = true } } @@ -179,7 +178,6 @@ class GameViewModel @ViewModelInject constructor( startNewGame() }.also { initialized = true - oldGame = false } } @@ -218,44 +216,29 @@ class GameViewModel @ViewModelInject constructor( } fun onLongClick(index: Int) { - val isHighlighted = levelFacade.isHighlighted(index) - levelFacade.turnOffAllHighlighted() - refreshAll() - - if (levelFacade.hasCoverOn(index)) { - levelFacade.switchMarkAt(index).run { - refreshIndex(id) - hapticFeedbackInteractor.toggleFlagFeedback() - } - - analyticsManager.sentEvent(Analytics.LongPressArea(index)) - } else if (!preferencesRepository.useDoubleClickToOpen() || isHighlighted) { - levelFacade.openNeighbors(index).forEach { refreshIndex(it.id) } - analyticsManager.sentEvent(Analytics.LongPressMultipleArea(index)) - } else { - levelFacade.highlight(index).run { - refreshIndex(index, this) - } - } - - updateGameState() + val feedback = levelFacade.longPress(index) + refreshIndex(index, feedback.multipleChanges) + onFeedbackAnalytics(feedback) + onPostAction() } fun onDoubleClickArea(index: Int) { - if (levelFacade.turnOffAllHighlighted()) { - refreshAll() - } + val feedback = levelFacade.doubleClick(index) + refreshIndex(index, feedback.multipleChanges) + onFeedbackAnalytics(feedback) + onPostAction() - if (preferencesRepository.useDoubleClickToOpen()) { - if (!levelFacade.hasMines) { - levelFacade.plantMinesExcept(index, true) - } + hapticFeedbackInteractor.longPressFeedback() + } - levelFacade.doubleClick(index).run { - refreshIndex(index, this) - } - } + fun onSingleClick(index: Int) { + val feedback = levelFacade.singleClick(index) + refreshIndex(index, feedback.multipleChanges) + onFeedbackAnalytics(feedback) + onPostAction() + } + private fun onPostAction() { if (preferencesRepository.useFlagAssistant() && !levelFacade.hasAnyMineExploded()) { levelFacade.runFlagAssistant().forEach { Handler().post { @@ -265,51 +248,19 @@ class GameViewModel @ViewModelInject constructor( } updateGameState() - analyticsManager.sentEvent(Analytics.PressArea(index)) } - fun onSingleClick(index: Int) { - var openAnyArea = false - - if (levelFacade.turnOffAllHighlighted()) { - refreshAll() - } - - if (levelFacade.hasMarkOn(index)) { - levelFacade.removeMark(index).run { - refreshIndex(id) - } - hapticFeedbackInteractor.toggleFlagFeedback() - } else if (!preferencesRepository.useDoubleClickToOpen() || !levelFacade.hasMines) { - if (!levelFacade.hasMines) { - levelFacade.plantMinesExcept(index, true) - } - - levelFacade.singleClick(index).run { - refreshIndex(index, this) - } - - openAnyArea = true - } - - if (openAnyArea) { - if (preferencesRepository.useFlagAssistant() && !levelFacade.hasAnyMineExploded()) { - levelFacade.runFlagAssistant().forEach { - Handler().post { - refreshIndex(it.id) - } - } - } - - updateGameState() - analyticsManager.sentEvent(Analytics.PressArea(index)) + private fun onFeedbackAnalytics(feedback: ActionFeedback) { + when (feedback.action) { + Action.OpenTile -> { analyticsManager.sentEvent(Analytics.OpenTile(feedback.index)) } + Action.SwitchMark -> { analyticsManager.sentEvent(Analytics.SwitchMark(feedback.index)) } + Action.HighlightNeighbors -> { analyticsManager.sentEvent(Analytics.HighlightNeighbors(feedback.index)) } + Action.OpenNeighbors -> { analyticsManager.sentEvent(Analytics.OpenNeighbors(feedback.index)) } } } private fun refreshMineCount() = mineCount.postValue(levelFacade.remainingMines()) - fun isCurrentGame() = !oldGame - private fun updateGameState() { when { levelFacade.hasAnyMineExploded() -> { @@ -393,8 +344,8 @@ class GameViewModel @ViewModelInject constructor( fun useAccessibilityMode() = preferencesRepository.useLargeAreas() - private fun refreshIndex(targetIndex: Int, changes: Int = 1) { - if (!preferencesRepository.useAnimations() || changes > 1) { + private fun refreshIndex(targetIndex: Int, multipleChanges: Boolean = false) { + if (!preferencesRepository.useAnimations() || multipleChanges) { field.postValue(levelFacade.field) } else { fieldRefresh.postValue(targetIndex) diff --git a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt index c0f9b86b..ed9b879c 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt @@ -48,12 +48,13 @@ sealed class Analytics( class ResumePreviousGame : Analytics("Resume previous game") - class LongPressArea(index: Int) : Analytics("Long press area", mapOf("Index" to index.toString())) + class OpenTile(index: Int) : Analytics("Open Tile", mapOf("Index" to index.toString())) - class LongPressMultipleArea(index: Int) : - Analytics("Long press to open multiple", mapOf("Index" to index.toString())) + class SwitchMark(index: Int) : Analytics("Switch Mark", mapOf("Index" to index.toString())) - class PressArea(index: Int) : Analytics("Press area", mapOf("Index" to index.toString())) + class HighlightNeighbors(index: Int) : Analytics("Highlight Neighbors", mapOf("Index" to index.toString())) + + class OpenNeighbors(index: Int) : Analytics("Open Neighbors", mapOf("Index" to index.toString())) class GameOver(time: Long, score: Score) : Analytics( "Game Over", diff --git a/common/src/main/java/dev/lucasnlm/antimine/core/control/GameControl.kt b/common/src/main/java/dev/lucasnlm/antimine/core/control/GameControl.kt new file mode 100644 index 00000000..4f3aa300 --- /dev/null +++ b/common/src/main/java/dev/lucasnlm/antimine/core/control/GameControl.kt @@ -0,0 +1,38 @@ +package dev.lucasnlm.antimine.core.control + +enum class Action { + OpenTile, + SwitchMark, + HighlightNeighbors, + OpenNeighbors, +} + +data class Actions( + val singleClick: Action?, + val doubleClick: Action?, + val longPress: Action? +) + +sealed class GameControl( + val onCovered: Actions, + val onOpen: Actions +) { + object Standard : GameControl( + onCovered = Actions( + singleClick = Action.OpenTile, + longPress = Action.SwitchMark, + doubleClick = null + ), + onOpen = Actions( + singleClick = Action.HighlightNeighbors, + longPress = Action.OpenNeighbors, + doubleClick = null + ) + ) +} + +data class ActionFeedback( + val action: Action?, + val index: Int, + val multipleChanges: Boolean +) diff --git a/common/src/test/java/dev/lucasnlm/antimine/common/level/LevelFacadeTest.kt b/common/src/test/java/dev/lucasnlm/antimine/common/level/LevelFacadeTest.kt index cd082f3c..364e8ecc 100644 --- a/common/src/test/java/dev/lucasnlm/antimine/common/level/LevelFacadeTest.kt +++ b/common/src/test/java/dev/lucasnlm/antimine/common/level/LevelFacadeTest.kt @@ -197,21 +197,21 @@ class LevelFacadeTest { fun testFlagAssistant() { levelFacadeOf(3, 3, 1, 200L).run { plantMinesExcept(3) - field.filterNot { it.hasMine }.forEach { openField(it) } + field.filterNot { it.hasMine }.forEach { openTile(it) } runFlagAssistant() field.filter { it.hasMine }.map { it.mark.isFlag() }.forEach(::assertTrue) } levelFacadeOf(3, 3, 2, 200L).run { plantMinesExcept(3) - field.filterNot { it.hasMine }.forEach { openField(it) } + field.filterNot { it.hasMine }.forEach { openTile(it) } runFlagAssistant() field.filter { it.hasMine }.map { it.mark.isFlag() }.forEach(::assertTrue) } levelFacadeOf(3, 3, 8, 200L).run { plantMinesExcept(3) - field.filterNot { it.hasMine }.forEach { openField(it) } + field.filterNot { it.hasMine }.forEach { openTile(it) } runFlagAssistant() field.filter { it.hasMine }.map { it.mark.isFlag() }.forEach(::assertFalse) } @@ -419,7 +419,7 @@ class LevelFacadeTest { plantMinesExcept(3) val mine = field.first { it.hasMine } assertEquals(findExplodedMine(), null) - openField(mine) + openTile(mine) assertEquals(findExplodedMine(), mine) } }