diff --git a/app/src/main/java/dev/lucasnlm/antimine/GameActivity.kt b/app/src/main/java/dev/lucasnlm/antimine/GameActivity.kt index bf7d9250..6b4e2824 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/GameActivity.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/GameActivity.kt @@ -43,6 +43,7 @@ import dev.lucasnlm.antimine.level.view.LevelFragment import dev.lucasnlm.antimine.preferences.PreferencesActivity import dev.lucasnlm.antimine.share.viewmodel.ShareViewModel import kotlinx.android.synthetic.main.activity_game.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -99,8 +100,13 @@ class GameActivity : DaggerAppCompatActivity() { } private fun bindViewModel() = viewModel.apply { + var lastEvent: Event? = null // TODO use distinctUntilChanged + eventObserver.observe(this@GameActivity, Observer { - onGameEvent(it) + if (lastEvent != it) { + onGameEvent(it) + lastEvent = it + } }) elapsedTimeSeconds.observe(this@GameActivity, Observer { @@ -459,9 +465,11 @@ class GameActivity : DaggerAppCompatActivity() { status = Status.Over(currentTime, score) invalidateOptionsMenu() viewModel.stopClock() - viewModel.gameOver() - waitAndShowEndGameDialog(false) + GlobalScope.launch(context = Dispatchers.Main) { + viewModel.gameOver() + waitAndShowEndGameDialog(false) + } } Event.ResumeVictory -> { val score = Score( diff --git a/app/src/main/java/dev/lucasnlm/antimine/TvGameActivity.kt b/app/src/main/java/dev/lucasnlm/antimine/TvGameActivity.kt index 5594b0b8..aa2b5571 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/TvGameActivity.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/TvGameActivity.kt @@ -32,6 +32,7 @@ import dev.lucasnlm.antimine.level.view.CustomLevelDialogFragment import dev.lucasnlm.antimine.level.view.LevelFragment import dev.lucasnlm.antimine.preferences.PreferencesActivity import kotlinx.android.synthetic.main.activity_tv_game.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -302,9 +303,11 @@ class TvGameActivity : DaggerAppCompatActivity() { status = Status.Over(currentTime, score) invalidateOptionsMenu() viewModel.stopClock() - viewModel.gameOver() - waitAndShowGameOverConfirmNewGame() + GlobalScope.launch(context = Dispatchers.Main) { + viewModel.gameOver() + waitAndShowGameOverConfirmNewGame() + } } Event.ResumeVictory, Event.ResumeGameOver -> { val score = Score( 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 927fe737..1a5ab9aa 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 @@ -82,6 +82,8 @@ class LevelFacade { fun hasMarkOn(index: Int): Boolean = getArea(index).mark.isNotNone() + fun isEmpty(index: Int): Boolean = getArea(index).let { it.minesAround == 0 && !it.hasMine && !it.isCovered } + fun plantMinesExcept(index: Int, includeSafeArea: Boolean = false) { plantRandomMines(index, includeSafeArea) putMinesTips() @@ -187,9 +189,11 @@ class LevelFacade { it.id } - fun runFlagAssistant() { + fun runFlagAssistant(): Sequence { // Must not select Mark.PurposefulNone, only Mark.None. Otherwise, it will flag // a square that was previously unflagged by player. + val assists = mutableListOf() + mines.filter { it.mark.isPureNone() }.forEach { field -> val neighbors = field.findNeighbors() val neighborsCount = neighbors.count() @@ -197,8 +201,15 @@ class LevelFacade { !neighbor.isCovered || (neighbor.hasMine && neighbor.mark.isFlag()) }.count() - field.mark = if (revealedNeighborsCount == neighborsCount) Mark.Flag else Mark.None + if (revealedNeighborsCount == neighborsCount) { + assists.add(field.id) + field.mark = Mark.Flag + } else { + field.mark = Mark.None + } } + + return assists.asSequence() } fun getStats() = Score( @@ -207,21 +218,25 @@ class LevelFacade { field.count() ) - fun showAllMines() { + fun showAllMines() = mines.filter { it.mark != Mark.Flag }.forEach { it.isCovered = false } - } - fun flagAllMines() { - mines.forEach { it.mark = Mark.Flag } - } + fun findExplodedMine() = mines.filter { it.mistake }.firstOrNull() - fun showWrongFlags() { + fun takeExplosionRadius(target: Area, take: Int = 20): Sequence = + mines.filter { it.isCovered && it.mark.isNone() }.sortedBy { + val dx1 = (it.posX - target.posX) + val dy1 = (it.posY - target.posY) + dx1 * dx1 + dy1 * dy1 + }.take(take) + + fun flagAllMines() = mines.forEach { it.mark = Mark.Flag } + + fun showWrongFlags() = field.filter { it.mark.isNotNone() && !it.hasMine }.forEach { it.mistake = true } - } - fun revealAllEmptyAreas() { + fun revealAllEmptyAreas() = field.filter { !it.hasMine }.forEach { it.isCovered = false } - } fun hasAnyMineExploded(): Boolean = mines.firstOrNull { it.mistake } != null 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 2e2990b5..1f431e7e 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 @@ -1,6 +1,7 @@ package dev.lucasnlm.antimine.common.level.viewmodel import android.app.Application +import android.os.Handler import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository @@ -19,6 +20,7 @@ import dev.lucasnlm.antimine.core.analytics.models.Analytics import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -58,7 +60,7 @@ class GameViewModel( mineCount.postValue(minefield.mines) this.difficulty.postValue(difficulty) levelSetup.postValue(minefield) - field.postValue(levelFacade.field.toList()) + refreshField() eventObserver.postValue(Event.StartNewGame) @@ -83,7 +85,7 @@ class GameViewModel( mineCount.postValue(setup.mines) difficulty.postValue(save.difficulty) levelSetup.postValue(setup) - field.postValue(levelFacade.field.toList()) + refreshField() when { levelFacade.hasAnyMineExploded() -> eventObserver.postValue(Event.ResumeGameOver) @@ -155,8 +157,7 @@ class GameViewModel( analyticsManager.sentEvent(Analytics.LongPressMultipleArea(index)) } - field.postValue(levelFacade.field.toList()) - + refreshField() refreshGame() } @@ -173,12 +174,15 @@ class GameViewModel( } levelFacade.clickArea(index) - - field.postValue(levelFacade.field.toList()) + refreshField(index) } if (preferencesRepository.useFlagAssistant() && !levelFacade.hasAnyMineExploded()) { - levelFacade.runFlagAssistant() + levelFacade.runFlagAssistant().forEach { + Handler().post { + refreshField(it) + } + } } refreshGame() @@ -223,11 +227,20 @@ class GameViewModel( levelFacade.revealAllEmptyAreas() } - fun gameOver() { + suspend fun gameOver() { levelFacade.run { analyticsManager.sentEvent(Analytics.GameOver(clock.time(), getStats())) - showAllMines() + + findExplodedMine()?.let { exploded -> + takeExplosionRadius(exploded).forEach { + it.isCovered = false + refreshField(it.id) + delay(75L) + } + } + showWrongFlags() + refreshGame() } GlobalScope.launch { @@ -255,7 +268,11 @@ class GameViewModel( fun useAccessibilityMode() = preferencesRepository.useLargeAreas() - private fun refreshField(index: Int) { - fieldRefresh.postValue(index) + private fun refreshField(index: Int? = null) { + if (index == null || levelFacade.isEmpty(index)) { + field.postValue(levelFacade.field.toList()) + } else { + fieldRefresh.postValue(index) + } } } diff --git a/wear/src/main/java/dev/lucasnlm/antimine/wear/WatchGameActivity.kt b/wear/src/main/java/dev/lucasnlm/antimine/wear/WatchGameActivity.kt index 0e03ad7f..f8dc1187 100644 --- a/wear/src/main/java/dev/lucasnlm/antimine/wear/WatchGameActivity.kt +++ b/wear/src/main/java/dev/lucasnlm/antimine/wear/WatchGameActivity.kt @@ -171,10 +171,12 @@ class WatchGameActivity : DaggerAppCompatActivity(), AmbientModeSupport.AmbientC Event.GameOver -> { status = Status.Over() viewModel.stopClock() - viewModel.gameOver() - messageText.text = getString(R.string.game_over) - waitAndShowNewGameButton() + GlobalScope.launch(context = Dispatchers.Main) { + viewModel.gameOver() + messageText.text = getString(R.string.game_over) + waitAndShowNewGameButton() + } } Event.ResumeVictory -> { status = Status.Over()