Make mines explode individually

This commit is contained in:
Lucas Lima 2020-03-26 13:12:22 -03:00
parent ca33bc7caf
commit 0e226eb620
5 changed files with 75 additions and 30 deletions

View file

@ -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 {
if (lastEvent != it) {
onGameEvent(it)
lastEvent = it
}
})
elapsedTimeSeconds.observe(this@GameActivity, Observer {
@ -459,10 +465,12 @@ class GameActivity : DaggerAppCompatActivity() {
status = Status.Over(currentTime, score)
invalidateOptionsMenu()
viewModel.stopClock()
viewModel.gameOver()
GlobalScope.launch(context = Dispatchers.Main) {
viewModel.gameOver()
waitAndShowEndGameDialog(false)
}
}
Event.ResumeVictory -> {
val score = Score(
rightMines,

View file

@ -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,10 +303,12 @@ class TvGameActivity : DaggerAppCompatActivity() {
status = Status.Over(currentTime, score)
invalidateOptionsMenu()
viewModel.stopClock()
viewModel.gameOver()
GlobalScope.launch(context = Dispatchers.Main) {
viewModel.gameOver()
waitAndShowGameOverConfirmNewGame()
}
}
Event.ResumeVictory, Event.ResumeGameOver -> {
val score = Score(
rightMines,

View file

@ -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<Int> {
// Must not select Mark.PurposefulNone, only Mark.None. Otherwise, it will flag
// a square that was previously unflagged by player.
val assists = mutableListOf<Int>()
mines.filter { it.mark.isPureNone() }.forEach { field ->
val neighbors = field.findNeighbors()
val neighborsCount = neighbors.count()
@ -197,31 +201,42 @@ 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(
mines.filter { !it.mistake && it.mark.isFlag() }.count(),
mines.count(),
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<Area> =
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

View file

@ -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) {
private fun refreshField(index: Int? = null) {
if (index == null || levelFacade.isEmpty(index)) {
field.postValue(levelFacade.field.toList())
} else {
fieldRefresh.postValue(index)
}
}
}

View file

@ -171,11 +171,13 @@ class WatchGameActivity : DaggerAppCompatActivity(), AmbientModeSupport.AmbientC
Event.GameOver -> {
status = Status.Over()
viewModel.stopClock()
viewModel.gameOver()
GlobalScope.launch(context = Dispatchers.Main) {
viewModel.gameOver()
messageText.text = getString(R.string.game_over)
waitAndShowNewGameButton()
}
}
Event.ResumeVictory -> {
status = Status.Over()
messageText.text = getString(R.string.victory)