Add Continue feature
This commit is contained in:
parent
99ce68978e
commit
3544c0717b
9 changed files with 105 additions and 51 deletions
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -7,5 +7,6 @@ data class EndGameDialogState(
|
|||
val title: String,
|
||||
val message: String,
|
||||
val isVictory: Boolean?,
|
||||
val showContinueButton: Boolean,
|
||||
val received: Int
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue