Add Continue feature

This commit is contained in:
Lucas Lima 2020-12-12 21:10:46 -03:00
parent 99ce68978e
commit 3544c0717b
9 changed files with 105 additions and 51 deletions

View file

@ -163,6 +163,16 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
)
continueObserver.observe(
this@GameActivity,
{
lifecycleScope.launch {
gameViewModel.increaseErrorTolerance()
eventObserver.postValue(Event.ResumeGame)
}
}
)
shareObserver.observe(
this@GameActivity,
{
@ -632,7 +642,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
}
private fun showEndGameDialog(victory: Boolean) {
private fun showEndGameDialog(victory: Boolean, canContinue: Boolean) {
val currentGameStatus = status
if (currentGameStatus is Status.Over && !isFinishing && !drawer.isDrawerOpen(GravityCompat.START)) {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null &&
@ -641,6 +651,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
val score = currentGameStatus.score
EndGameDialogFragment.newInstance(
victory,
canContinue,
score?.rightMines ?: 0,
score?.totalMines ?: 0,
currentGameStatus.time,
@ -663,26 +674,25 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
}
private fun showEndGameAlert(victory: Boolean) {
private fun showEndGameAlert(victory: Boolean, canContinue: Boolean) {
val canShowWindow = preferencesRepository.showWindowsWhenFinishGame()
if (!isFinishing) {
if (canShowWindow) {
showEndGameDialog(victory)
showEndGameDialog(victory, !victory && canContinue)
} else {
showEndGameToast(victory)
}
}
}
private fun waitAndShowEndGameAlert(victory: Boolean, await: Boolean) {
private fun waitAndShowEndGameAlert(victory: Boolean, await: Boolean, canContinue: Boolean) {
if (await && gameViewModel.explosionDelay() != 0L) {
lifecycleScope.launch {
delay((gameViewModel.explosionDelay() * 0.3).toLong())
showEndGameAlert(victory)
showEndGameAlert(victory, canContinue)
}
} else {
showEndGameAlert(victory)
showEndGameAlert(victory, canContinue)
}
}
@ -764,7 +774,8 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
waitAndShowEndGameAlert(
victory = true,
await = false
await = false,
canContinue = false,
)
}
}
@ -787,7 +798,8 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
gameViewModel.saveGame()
waitAndShowEndGameAlert(
victory = false,
await = true
await = true,
canContinue = gameViewModel.hasUnknownMines(),
)
}
}

View file

@ -35,6 +35,7 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
endGameViewModel.sendEvent(
EndGameDialogEvent.BuildCustomEndGame(
isVictory = if (getInt(DIALOG_TOTAL_MINES, 0) > 0) getBoolean(DIALOG_IS_VICTORY) else null,
showContinueButton = getBoolean(DIALOG_SHOW_CONTINUE),
time = getLong(DIALOG_TIME, 0L),
rightMines = getInt(DIALOG_RIGHT_MINES, 0),
totalMines = getInt(DIALOG_TOTAL_MINES, 0),
@ -90,8 +91,18 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
}
}
} else {
setNeutralButton(R.string.retry) { _, _ ->
gameViewModel.retryObserver.postValue(Unit)
if (state.showContinueButton) {
setNegativeButton(R.string.retry) { _, _ ->
gameViewModel.retryObserver.postValue(Unit)
}
setNeutralButton(R.string.continue_game) { _, _ ->
gameViewModel.continueObserver.postValue(Unit)
}
} else {
setNeutralButton(R.string.retry) { _, _ ->
gameViewModel.retryObserver.postValue(Unit)
}
}
}
}
@ -108,6 +119,7 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
companion object {
fun newInstance(
victory: Boolean,
showContinueButton: Boolean,
rightMines: Int,
totalMines: Int,
time: Long,
@ -115,6 +127,7 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
) = EndGameDialogFragment().apply {
arguments = Bundle().apply {
putBoolean(DIALOG_IS_VICTORY, victory)
putBoolean(DIALOG_SHOW_CONTINUE, showContinueButton)
putInt(DIALOG_RIGHT_MINES, rightMines)
putInt(DIALOG_TOTAL_MINES, totalMines)
putInt(DIALOG_RECEIVED, received)
@ -123,6 +136,7 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
}
const val DIALOG_IS_VICTORY = "dialog_state"
private const val DIALOG_SHOW_CONTINUE = "dialog_show_continue"
private const val DIALOG_TIME = "dialog_time"
private const val DIALOG_RIGHT_MINES = "dialog_right_mines"
private const val DIALOG_TOTAL_MINES = "dialog_total_mines"

View file

@ -5,6 +5,7 @@ import androidx.annotation.DrawableRes
sealed class EndGameDialogEvent {
data class BuildCustomEndGame(
val isVictory: Boolean?,
val showContinueButton: Boolean,
val time: Long,
val rightMines: Int,
val totalMines: Int,

View file

@ -7,5 +7,6 @@ data class EndGameDialogState(
val title: String,
val message: String,
val isVictory: Boolean?,
val showContinueButton: Boolean,
val received: Int
)

View file

@ -69,8 +69,9 @@ class EndGameDialogViewModel(
R.drawable.emoji_triangular_flag,
"",
"",
false,
0
isVictory = false,
showContinueButton = false,
received = 0
)
override suspend fun mapEventToState(event: EndGameDialogEvent) = flow {
@ -82,6 +83,7 @@ class EndGameDialogViewModel(
title = context.getString(R.string.you_won),
message = messageTo(event.time, event.isVictory),
isVictory = true,
showContinueButton = false,
received = event.received
)
}
@ -91,6 +93,7 @@ class EndGameDialogViewModel(
title = context.getString(R.string.you_lost),
message = messageTo(event.time, event.isVictory),
isVictory = false,
showContinueButton = event.showContinueButton,
received = event.received
)
}
@ -100,6 +103,7 @@ class EndGameDialogViewModel(
title = context.getString(R.string.new_game),
message = context.getString(R.string.new_game_request),
isVictory = false,
showContinueButton = event.showContinueButton,
received = event.received
)
}

View file

@ -27,6 +27,7 @@ class GameController {
private var useQuestionMark = true
private var useOpenOnSwitchControl = true
private var useNoGuessing = true
private var errorTolerance = 0
val seed: Long
@ -238,25 +239,23 @@ class GameController {
return result
}
fun hasAnyMineExploded(): Boolean = mines().firstOrNull { it.mistake } != null
private fun hasAnyMineExploded(): Boolean = mines().firstOrNull { it.mistake } != null
private fun explodedMinesCount(): Int = mines().count { it.mistake }
fun hasFlaggedAllMines(): Boolean = rightFlags() == minefield.mines
fun hasIsolatedAllMines(): Boolean {
return field.let {
val openSquares = it.count { area -> !area.isCovered }
val mines = it.count { area -> area.hasMine }
(openSquares + mines) == it.size
}
return field.count { area -> !area.hasMine && area.isCovered } == 0
}
private fun rightFlags() = mines().count { it.mark.isFlag() }
fun checkVictory(): Boolean =
fun isVictory(): Boolean =
hasMines() && hasIsolatedAllMines() && !hasAnyMineExploded()
fun isGameOver(): Boolean =
checkVictory() || hasAnyMineExploded()
hasIsolatedAllMines() || (explodedMinesCount() > errorTolerance)
fun remainingMines(): Int {
val flagsCount = field.count { it.mark.isFlag() }
@ -266,8 +265,8 @@ class GameController {
fun getSaveState(duration: Long, difficulty: Difficulty): Save {
val saveStatus: SaveStatus = when {
checkVictory() -> SaveStatus.VICTORY
hasAnyMineExploded() -> SaveStatus.DEFEAT
isVictory() -> SaveStatus.VICTORY
isGameOver() -> SaveStatus.DEFEAT
else -> SaveStatus.ON_GOING
}
return Save(
@ -290,10 +289,14 @@ class GameController {
fun getActionsCount() = actions
fun increaseErrorTolerance() {
errorTolerance++
}
fun getStats(duration: Long): Stats? {
val gameStatus: SaveStatus = when {
checkVictory() -> SaveStatus.VICTORY
hasAnyMineExploded() -> SaveStatus.DEFEAT
isVictory() -> SaveStatus.VICTORY
isGameOver() -> SaveStatus.DEFEAT
else -> SaveStatus.ON_GOING
}
return if (gameStatus == SaveStatus.ON_GOING) {

View file

@ -52,6 +52,7 @@ open class GameViewModel(
) : ViewModel() {
val eventObserver = MutableLiveData<Event>()
val retryObserver = MutableLiveData<Unit>()
val continueObserver = MutableLiveData<Unit>()
val shareObserver = MutableLiveData<Unit>()
private lateinit var gameController: GameController
@ -116,8 +117,8 @@ open class GameViewModel(
refreshMineCount()
when {
gameController.hasAnyMineExploded() -> eventObserver.postValue(Event.GameOver)
gameController.checkVictory() -> eventObserver.postValue(Event.Victory)
gameController.isGameOver() -> eventObserver.postValue(Event.GameOver)
gameController.isVictory() -> eventObserver.postValue(Event.Victory)
else -> eventObserver.postValue(Event.ResumeGame)
}
@ -169,6 +170,10 @@ open class GameViewModel(
}
}
fun increaseErrorTolerance() {
gameController.increaseErrorTolerance()
}
suspend fun retryGame(uid: Int): Minefield = withContext(Dispatchers.IO) {
val save = savesRepository.loadFromId(uid)
@ -291,7 +296,7 @@ open class GameViewModel(
}
private fun onPostAction() {
if (preferencesRepository.useFlagAssistant() && !gameController.hasAnyMineExploded()) {
if (preferencesRepository.useFlagAssistant() && !gameController.isGameOver()) {
gameController.runFlagAssistant()
refreshField()
}
@ -323,7 +328,7 @@ open class GameViewModel(
private fun updateGameState() {
when {
gameController.hasAnyMineExploded() -> {
gameController.isGameOver() -> {
eventObserver.postValue(Event.GameOver)
}
else -> {
@ -335,7 +340,7 @@ open class GameViewModel(
refreshMineCount()
}
if (gameController.checkVictory()) {
if (gameController.isVictory()) {
refreshField()
eventObserver.postValue(Event.Victory)
}
@ -391,22 +396,15 @@ open class GameViewModel(
fun explosionDelay() = if (preferencesRepository.useAnimations()) 750L else 0L
suspend fun gameOver(fromResumeGame: Boolean) {
fun hasUnknownMines(): Boolean {
return !gameController.hasIsolatedAllMines()
}
suspend fun revealMines() {
val explosionTime = (explosionDelay() / gameController.getMinesCount().coerceAtLeast(10))
val delayMillis = explosionTime.coerceAtMost(25L)
gameController.run {
analyticsManager.sentEvent(Analytics.GameOver(clock.time(), getScore()))
val explosionTime = (explosionDelay() / gameController.getMinesCount().coerceAtLeast(10))
val delayMillis = explosionTime.coerceAtMost(25L)
if (!fromResumeGame) {
if (preferencesRepository.useHapticFeedback()) {
hapticFeedbackManager.explosionFeedback()
}
if (preferencesRepository.isSoundEffectsEnabled()) {
soundManager.play(R.raw.mine_explosion_sound)
}
}
showWrongFlags()
refreshField()
@ -419,6 +417,26 @@ open class GameViewModel(
}
showAllMines()
}
}
suspend fun gameOver(fromResumeGame: Boolean) {
gameController.run {
analyticsManager.sentEvent(Analytics.GameOver(clock.time(), getScore()))
if (!fromResumeGame) {
if (preferencesRepository.useHapticFeedback()) {
hapticFeedbackManager.explosionFeedback()
}
if (preferencesRepository.isSoundEffectsEnabled()) {
soundManager.play(R.raw.mine_explosion_sound)
}
}
if (gameController.hasIsolatedAllMines()) {
gameController.revealAllEmptyAreas()
}
refreshField()
updateGameState()
@ -462,7 +480,7 @@ open class GameViewModel(
preferencesRepository.incrementProgressiveValue()
}
if (clock.time() < 30) {
if (clock.time() < 30L) {
playGamesManager.unlockAchievement(Achievement.ThirtySeconds)
}

View file

@ -78,6 +78,7 @@
<string name="open_tile">Open</string>
<string name="flag_tile">Flag</string>
<string name="retry">Retry</string>
<string name="continue_game">Continue</string>
<string name="empty">Empty</string>
<string name="cant_do_it_now">Impossible to do that now</string>
<string name="you_have_received">You have received: +%1$d</string>

View file

@ -256,26 +256,26 @@ class GameControllerTest {
@Test
fun testVictory() = runBlockingTest {
withGameController { controller ->
assertFalse(controller.checkVictory())
assertFalse(controller.isVictory())
controller.mines().forEach { controller.fakeLongPress(it.id) }
assertFalse(controller.checkVictory())
assertFalse(controller.isVictory())
controller.field { !it.hasMine }.forEach { controller.fakeSingleClick(it.id) }
assertTrue(controller.checkVictory())
assertTrue(controller.isVictory())
controller.mines().first().run {
controller.fakeSingleClick(id)
controller.fakeSingleClick(id)
}
assertFalse(controller.checkVictory())
assertFalse(controller.isVictory())
}
}
@Test
fun testCantShowVictoryIfHasNoMines() = runBlockingTest {
withGameController { controller ->
assertFalse(controller.checkVictory())
assertFalse(controller.isVictory())
}
}