Merge pull request #35 from lucasnlm/open-mines-individually

Open mines individually
This commit is contained in:
Lucas Nunes 2020-04-02 08:57:21 -03:00 committed by GitHub
commit f0ded6f6e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 340 additions and 152 deletions

View file

@ -12,9 +12,10 @@
android:smallScreens="true"
android:xlargeScreens="true" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
<uses-permission
android:name="android.permission.VIBRATE" />
<uses-permission
android:name="android.permission.WAKE_LOCK" />
<uses-feature
android:name="android.hardware.touchscreen"

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 when available
eventObserver.observe(this@GameActivity, Observer {
onGameEvent(it)
if (lastEvent != it) {
onGameEvent(it)
lastEvent = it
}
})
elapsedTimeSeconds.observe(this@GameActivity, Observer {
@ -396,14 +402,11 @@ class GameActivity : DaggerAppCompatActivity() {
}
}
private fun waitAndShowEndGameDialog(
victory: Boolean,
await: Long = DateUtils.SECOND_IN_MILLIS
) {
if (await > 0L) {
private fun waitAndShowEndGameDialog(victory: Boolean, await: Boolean) {
if (await && viewModel.explosionDelay() != 0L) {
postDelayed(Handler(), {
showEndGameDialog(victory)
}, null, await)
}, null, (viewModel.explosionDelay() * 0.3).toLong())
} else {
showEndGameDialog(victory)
}
@ -448,7 +451,10 @@ class GameActivity : DaggerAppCompatActivity() {
viewModel.revealAllEmptyAreas()
viewModel.victory()
invalidateOptionsMenu()
waitAndShowEndGameDialog(true, 0L)
waitAndShowEndGameDialog(
victory = true,
await = false
)
}
Event.GameOver -> {
val score = Score(
@ -459,9 +465,14 @@ class GameActivity : DaggerAppCompatActivity() {
status = Status.Over(currentTime, score)
invalidateOptionsMenu()
viewModel.stopClock()
viewModel.gameOver()
waitAndShowEndGameDialog(false)
GlobalScope.launch(context = Dispatchers.Main) {
viewModel.gameOver()
waitAndShowEndGameDialog(
victory = false,
await = true
)
}
}
Event.ResumeVictory -> {
val score = Score(
@ -473,7 +484,10 @@ class GameActivity : DaggerAppCompatActivity() {
invalidateOptionsMenu()
viewModel.stopClock()
waitAndShowEndGameDialog(true)
waitAndShowEndGameDialog(
victory = true,
await = true
)
}
Event.ResumeGameOver -> {
val score = Score(
@ -485,7 +499,10 @@ class GameActivity : DaggerAppCompatActivity() {
invalidateOptionsMenu()
viewModel.stopClock()
waitAndShowEndGameDialog(false)
waitAndShowEndGameDialog(
victory = false,
await = true
)
}
else -> { }
}

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,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(

View file

@ -29,7 +29,7 @@ class ShareBuilder(
) {
private val context: Context = context.applicationContext
suspend fun share(minefield: Minefield, field: List<Area>, spentTime: Long?): Boolean {
suspend fun share(minefield: Minefield, field: Sequence<Area>, spentTime: Long?): Boolean {
val rightMines = field.count { it.hasMine && it.mark == Mark.Flag }
val totalMines = field.count { it.hasMine }
@ -42,7 +42,7 @@ class ShareBuilder(
}
}
private suspend fun createImage(minefield: Minefield, field: List<Area>): File? = withContext(Dispatchers.IO) {
private suspend fun createImage(minefield: Minefield, field: Sequence<Area>): File? = withContext(Dispatchers.IO) {
val size = 38f
val padding = 1f
val radius = 2f
@ -75,7 +75,7 @@ class ShareBuilder(
for (x in 0 until minefield.width) {
for (y in 0 until minefield.height) {
val area = field[x + y * minefield.width]
val area = field.first { it.id == (x + y * minefield.width) }
canvas.save()
canvas.translate(x * size + padding, y * size + padding)
area.paintOnCanvas(

View file

@ -13,8 +13,8 @@ class ShareViewModel(
) : AndroidViewModel(application) {
private val context = getApplication<Application>().applicationContext
suspend fun share(minefield: Minefield?, field: List<Area>?, spentTime: Long?) {
val result = if (minefield != null && field != null && field.isNotEmpty()) {
suspend fun share(minefield: Minefield?, field: Sequence<Area>?, spentTime: Long?) {
val result = if (minefield != null && field != null && field.count() != 0) {
ShareBuilder(context).share(minefield, field, spentTime)
} else {
false

View file

@ -25,11 +25,12 @@ class LevelFacade {
lateinit var field: Sequence<Area>
private set
private var mines: Sequence<Area> = sequenceOf()
var mines: Sequence<Area> = sequenceOf()
private set
constructor(minefield: Minefield, seed: Long = randomSeed()) {
this.minefield = minefield
this.randomGenerator = Random().apply { setSeed(seed) }
this.randomGenerator = Random(seed)
this.seed = seed
this.saveId = 0
createEmptyField()
@ -37,7 +38,7 @@ class LevelFacade {
constructor(save: Save) {
this.minefield = save.minefield
this.randomGenerator = Random().apply { setSeed(save.seed) }
this.randomGenerator = Random(save.seed)
this.field = save.field.asSequence()
this.mines = this.field.filter { it.hasMine }.asSequence()
this.hasMines = this.mines.count() != 0
@ -55,12 +56,10 @@ class LevelFacade {
}.asSequence()
}
private fun getArea(id: Int) = field.first { it.id == id }
fun getArea(id: Int) = field.first { it.id == id }
fun switchMarkAt(index: Int): Boolean {
val changed: Boolean
fun switchMarkAt(index: Int): Area =
getArea(index).apply {
changed = isCovered
if (isCovered) {
mark = when (mark) {
Mark.PurposefulNone, Mark.None -> Mark.Flag
@ -69,19 +68,18 @@ class LevelFacade {
}
}
}
return changed
}
fun removeMark(index: Int) {
fun removeMark(index: Int) =
getArea(index).apply {
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 plantMinesExcept(index: Int, includeSafeArea: Boolean = false) {
plantRandomMines(index, includeSafeArea)
putMinesTips()
@ -96,11 +94,13 @@ class LevelFacade {
it.safeZone = true
}
findCrossNeighbors().forEach { neighbor ->
neighbor
.findCrossNeighbors()
.filterNot { it.safeZone }
.forEach { it.safeZone = true }
if (this@LevelFacade.minefield. width > 9) {
findCrossNeighbors().forEach { neighbor ->
neighbor
.findCrossNeighbors()
.filterNot { it.safeZone }
.forEach { it.safeZone = true }
}
}
}
}
@ -116,112 +116,137 @@ class LevelFacade {
private fun putMinesTips() {
field.forEach {
it.minesAround = if (it.hasMine) 0 else it.findNeighbors().filter { neighbor ->
it.minesAround = if (it.hasMine) 0 else it.findNeighbors().count { neighbor ->
neighbor.hasMine
}.count()
}
}
}
/**
* Run "Flood Fill algorithm" to open all empty neighbors of a target area.
*/
fun openField(target: Area): Boolean {
val result: Boolean = target.isCovered
fun openField(target: Area): Int {
var changes = 0
target.run {
if (isCovered) {
changes += 1
isCovered = false
mark = Mark.None
if (target.isCovered) {
target.isCovered = false
target.mark = Mark.None
if (target.minesAround == 0 && !target.hasMine) {
target.findNeighbors().forEach { openField(it) }
}
if (this.hasMines) {
field.filter { it.safeZone }.forEach { openField(it) }
}
if (target.hasMine) {
target.mistake = true
if (hasMine) {
mistake = true
} else if (minesAround == 0) {
findNeighbors()
.filter { it.isCovered }
.also {
changes += it.count()
}
.forEach { openField(it) }
}
}
}
return result
return changes
}
fun turnOffAllHighlighted() {
field.forEach {
it.highlighted = false
/**
* Disable all highlighted areas.
*
* @return true if any area was changed.
*/
fun turnOffAllHighlighted(): Boolean {
var changed: Boolean
field
.filter { it.highlighted }
.also { changed = it.count() != 0 }
.forEach { it.highlighted = false }
return changed
}
private fun toggleHighlight(target: Area): Int {
var changed = 1
target.apply {
highlighted = !highlighted
findNeighbors()
.filter { it.mark.isNone() && it.isCovered }
.also { changed += it.count() }
.forEach { it.highlighted = !it.highlighted }
}
return changed
}
private fun toggleHighlight(target: Area) {
target.highlighted = !target.highlighted
target.findNeighbors()
.filter { it.mark.isNone() && it.isCovered }
.forEach { it.highlighted = !it.highlighted }
}
fun clickArea(index: Int): Boolean = getArea(index).let {
when {
it.isCovered -> {
/**
* Open a given area by its index.
*
* @param index the target index
* @return true if multiple areas were open
*/
fun clickArea(index: Int): Int = getArea(index).run {
return when {
isCovered -> {
openField(getArea(index))
}
it.minesAround != 0 -> {
toggleHighlight(it)
true
}
else -> {
false
minesAround != 0 -> {
toggleHighlight(this)
}
else -> 0
}
}
fun openNeighbors(index: Int): List<Int> =
fun openNeighbors(index: Int): Sequence<Area> =
getArea(index)
.findNeighbors()
.filter {
it.mark.isNone()
}
.map {
openField(it)
it.id
it.mark.isNone() && it.isCovered
}.also {
it.forEach { field -> openField(field) }
}
fun runFlagAssistant() {
fun runFlagAssistant(): Sequence<Area> {
// Must not select Mark.PurposefulNone, only Mark.None. Otherwise, it will flag
// a square that was previously unflagged by player.
val assists = mutableListOf<Area>()
mines.filter { it.mark.isPureNone() }.forEach { field ->
val neighbors = field.findNeighbors()
val neighborsCount = neighbors.count()
val revealedNeighborsCount = neighbors.filter { neighbor ->
val revealedNeighborsCount = neighbors.count { neighbor ->
!neighbor.isCovered || (neighbor.hasMine && neighbor.mark.isFlag())
}.count()
}
field.mark = if (revealedNeighborsCount == neighborsCount) Mark.Flag else Mark.None
if (revealedNeighborsCount == neighborsCount) {
assists.add(field)
field.mark = Mark.Flag
} else {
field.mark = Mark.None
}
}
return assists.asSequence()
}
fun getStats() = Score(
mines.filter { !it.mistake && it.mark.isFlag() }.count(),
fun getScore() = Score(
mines.count { !it.mistake && it.mark.isFlag() },
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() {
field.filter { it.mark.isNotNone() && !it.hasMine }.forEach { it.mistake = true }
}
fun takeExplosionRadius(target: Area): 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
}
fun revealAllEmptyAreas() {
field.filter { !it.hasMine }.forEach { it.isCovered = false }
}
fun flagAllMines() = mines.forEach { it.mark = Mark.Flag }
fun showWrongFlags() = field.filter { it.mark.isNotNone() && !it.hasMine }.forEach { it.mistake = true }
fun revealAllEmptyAreas() = field.filterNot { it.hasMine }.forEach { it.isCovered = false }
fun hasAnyMineExploded(): Boolean = mines.firstOrNull { it.mistake } != null
@ -231,11 +256,11 @@ class LevelFacade {
mines.map {
val neighbors = it.findNeighbors()
val neighborsCount = neighbors.count()
val isolatedNeighborsCount = neighbors.filter { neighbor ->
val isolatedNeighborsCount = neighbors.count { neighbor ->
!neighbor.isCovered || neighbor.hasMine
}.count()
neighborsCount == isolatedNeighborsCount
}.filterNot { it }.count() == 0
}
neighborsCount != isolatedNeighborsCount
}.count { it } == 0
private fun rightFlags() = mines.count { it.mark.isFlag() }
@ -248,7 +273,7 @@ class LevelFacade {
return (minesCount - flagsCount).coerceAtLeast(0)
}
private fun Area.findNeighbors() = arrayOf(
private fun Area.findNeighbors() = sequenceOf(
getNeighbor(1, 0),
getNeighbor(1, 1),
getNeighbor(0, 1),
@ -259,7 +284,7 @@ class LevelFacade {
getNeighbor(1, -1)
).filterNotNull()
private fun Area.findCrossNeighbors() = arrayOf(
private fun Area.findCrossNeighbors() = sequenceOf(
getNeighbor(1, 0),
getNeighbor(0, 1),
getNeighbor(-1, 0),
@ -276,7 +301,16 @@ class LevelFacade {
hasAnyMineExploded() -> SaveStatus.DEFEAT
else -> SaveStatus.ON_GOING
}
return Save(saveId, seed, startTime, duration, minefield, difficulty, saveStatus, field.toList())
return Save(
saveId,
seed,
startTime,
duration,
minefield,
difficulty,
saveStatus,
field.toList()
)
}
fun setCurrentSaveId(id: Int) {

View file

@ -40,8 +40,8 @@ class AreaAdapter(
this.clickEnabled = value
}
fun bindField(area: List<Area>) {
this.field = area
fun bindField(field: Sequence<Area>) {
this.field = field.toList()
notifyDataSetChanged()
}

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
@ -37,34 +39,34 @@ class GameViewModel(
private var currentDifficulty: Difficulty = Difficulty.Standard
private var initialized = false
val field = MutableLiveData<List<Area>>()
val field = MutableLiveData<Sequence<Area>>()
val fieldRefresh = MutableLiveData<Int>()
val elapsedTimeSeconds = MutableLiveData<Long>()
val mineCount = MutableLiveData<Int>()
val difficulty = MutableLiveData<Difficulty>()
val levelSetup = MutableLiveData<Minefield>()
fun startNewGame(difficulty: Difficulty = currentDifficulty): Minefield {
fun startNewGame(newDifficulty: Difficulty = currentDifficulty): Minefield {
clock.reset()
elapsedTimeSeconds.postValue(0L)
currentDifficulty = difficulty
currentDifficulty = newDifficulty
val minefield = minefieldRepository.fromDifficulty(
difficulty, dimensionRepository, preferencesRepository
newDifficulty, dimensionRepository, preferencesRepository
)
levelFacade = LevelFacade(minefield)
mineCount.postValue(minefield.mines)
this.difficulty.postValue(difficulty)
difficulty.postValue(newDifficulty)
levelSetup.postValue(minefield)
field.postValue(levelFacade.field.toList())
refreshAll()
eventObserver.postValue(Event.StartNewGame)
analyticsManager.sentEvent(
Analytics.NewGame(
minefield, difficulty,
minefield, newDifficulty,
levelFacade.seed,
useAccessibilityMode()
)
@ -83,7 +85,7 @@ class GameViewModel(
mineCount.postValue(setup.mines)
difficulty.postValue(save.difficulty)
levelSetup.postValue(setup)
field.postValue(levelFacade.field.toList())
refreshAll()
when {
levelFacade.hasAnyMineExploded() -> eventObserver.postValue(Event.ResumeGameOver)
@ -132,62 +134,66 @@ class GameViewModel(
}
fun resumeGame() {
if (initialized) {
if (levelFacade.hasMines) {
eventObserver.postValue(Event.Resume)
}
if (initialized && levelFacade.hasMines) {
eventObserver.postValue(Event.Resume)
}
}
fun onLongClick(index: Int) {
levelFacade.turnOffAllHighlighted()
refreshAll()
if (levelFacade.hasCoverOn(index)) {
if (levelFacade.switchMarkAt(index)) {
refreshField(index)
levelFacade.switchMarkAt(index).run {
refreshIndex(id)
hapticFeedbackInteractor.toggleFlagFeedback()
}
analyticsManager.sentEvent(Analytics.LongPressArea(index))
} else {
levelFacade.openNeighbors(index)
levelFacade.openNeighbors(index).forEach { refreshIndex(it.id) }
analyticsManager.sentEvent(Analytics.LongPressMultipleArea(index))
}
field.postValue(levelFacade.field.toList())
refreshGame()
updateGameState()
}
fun onClickArea(index: Int) {
levelFacade.turnOffAllHighlighted()
if (levelFacade.turnOffAllHighlighted()) {
refreshAll()
}
if (levelFacade.hasMarkOn(index)) {
levelFacade.removeMark(index)
levelFacade.removeMark(index).run {
refreshIndex(id)
}
hapticFeedbackInteractor.toggleFlagFeedback()
refreshField(index)
} else {
if (!levelFacade.hasMines) {
levelFacade.plantMinesExcept(index, true)
}
levelFacade.clickArea(index)
field.postValue(levelFacade.field.toList())
levelFacade.clickArea(index).run {
refreshIndex(index, this)
}
}
if (preferencesRepository.useFlagAssistant() && !levelFacade.hasAnyMineExploded()) {
levelFacade.runFlagAssistant()
levelFacade.runFlagAssistant().forEach {
Handler().post {
refreshIndex(it.id)
}
}
}
refreshGame()
updateGameState()
analyticsManager.sentEvent(Analytics.PressArea(index))
}
private fun refreshMineCount() = mineCount.postValue(levelFacade.remainingMines())
private fun refreshGame() {
private fun updateGameState() {
when {
levelFacade.hasAnyMineExploded() -> {
hapticFeedbackInteractor.explosionFeedback()
@ -219,15 +225,26 @@ class GameViewModel(
clock.stop()
}
fun revealAllEmptyAreas() {
levelFacade.revealAllEmptyAreas()
}
fun revealAllEmptyAreas() = levelFacade.revealAllEmptyAreas()
fun gameOver() {
fun explosionDelay() = if (preferencesRepository.useAnimations()) 750L else 0L
suspend fun gameOver() {
levelFacade.run {
analyticsManager.sentEvent(Analytics.GameOver(clock.time(), getStats()))
showAllMines()
analyticsManager.sentEvent(Analytics.GameOver(clock.time(), getScore()))
val delayMillis = explosionDelay() / levelFacade.mines.count().coerceAtLeast(10)
findExplodedMine()?.let { exploded ->
takeExplosionRadius(exploded).forEach {
it.isCovered = false
refreshIndex(it.id)
delay(delayMillis)
}
}
showWrongFlags()
refreshAll()
updateGameState()
}
GlobalScope.launch {
@ -240,7 +257,7 @@ class GameViewModel(
analyticsManager.sentEvent(
Analytics.Victory(
clock.time(),
getStats(),
getScore(),
currentDifficulty
)
)
@ -255,7 +272,15 @@ class GameViewModel(
fun useAccessibilityMode() = preferencesRepository.useLargeAreas()
private fun refreshField(index: Int) {
fieldRefresh.postValue(index)
private fun refreshIndex(targetIndex: Int, changes: Int = 1) {
if (!preferencesRepository.useAnimations() || changes > 1) {
field.postValue(levelFacade.field)
} else {
fieldRefresh.postValue(targetIndex)
}
}
private fun refreshAll() {
field.postValue(levelFacade.field)
}
}

View file

@ -13,6 +13,7 @@ interface IPreferencesRepository {
fun useFlagAssistant(): Boolean
fun useHapticFeedback(): Boolean
fun useLargeAreas(): Boolean
fun useAnimations(): Boolean
}
class PreferencesRepository(
@ -45,4 +46,7 @@ class PreferencesRepository(
override fun useLargeAreas(): Boolean =
getBoolean("preference_large_area", false)
override fun useAnimations(): Boolean =
getBoolean("preference_animation", true)
}

View file

@ -29,6 +29,13 @@
android:title="@string/auto_flag"
android:summary="@string/settings_auto_flag_desc"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:checked="true"
android:defaultValue="true"
android:key="preference_animation"
android:title="@string/animations"
app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory

View file

@ -1,8 +1,9 @@
package dev.lucasnlm.antimine.common.level
import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Minefield
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 org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
@ -261,6 +262,29 @@ class LevelFacadeTest {
}
}
@Test
fun testRemoveMark() {
levelFacadeOf(3, 3, 1, 200L).run {
plantMinesExcept(3)
switchMarkAt(7)
assertTrue(hasMarkOn(7))
removeMark(7)
assertTrue(hasNoneOn(7))
}
}
@Test
fun testTurnOffAllHighlighted() {
levelFacadeOf(3, 3, 1, 200L).run {
plantMinesExcept(3)
getArea(7).highlighted = true
getArea(8).highlighted = true
assertEquals(field.count { it.highlighted }, 2)
turnOffAllHighlighted()
assertEquals(field.count { it.highlighted }, 0)
}
}
@Test
fun testOpenField() {
levelFacadeOf(3, 3, 1, 200L).run {
@ -326,6 +350,61 @@ class LevelFacadeTest {
}
}
@Test
fun testFlagAllMines() {
levelFacadeOf(3, 3, 5, 200L).run {
plantMinesExcept(3)
field.filter { it.hasMine }.forEach {
assertFalse(it.mark.isFlag())
}
flagAllMines()
field.filter { it.hasMine }.forEach {
assertTrue(it.mark.isFlag())
}
}
}
@Test
fun testFindExplodedMine() {
levelFacadeOf(3, 3, 5, 200L).run {
plantMinesExcept(3)
val mine = field.first { it.hasMine }
assertEquals(findExplodedMine(), null)
openField(mine)
assertEquals(findExplodedMine(), mine)
}
}
@Test
fun testTakeExplosionRadius() {
levelFacadeOf(6, 6, 10, 200L).run {
plantMinesExcept(3)
val mine = field.last { it.hasMine }
assertEquals(
listOf(35, 33, 22, 27, 17, 32, 25, 8, 7, 2),
takeExplosionRadius(mine).map { it.id }.toList()
)
}
levelFacadeOf(6, 6, 10, 200L).run {
plantMinesExcept(3)
val mine = field.first { it.hasMine }
assertEquals(
listOf(2, 8, 7, 17, 22, 25, 27, 32, 33, 35),
takeExplosionRadius(mine).map { it.id }.toList()
)
}
levelFacadeOf(6, 6, 10, 200L).run {
plantMinesExcept(3)
val mine = field.filter { it.hasMine }.elementAt(4)
assertEquals(
listOf(22, 17, 27, 33, 35, 8, 32, 25, 2, 7),
takeExplosionRadius(mine).map { it.id }.toList()
)
}
}
@Test
fun testShowWrongFlags() {
levelFacadeOf(3, 3, 5, 200L).run {
@ -353,6 +432,22 @@ class LevelFacadeTest {
}
}
@Test
fun testGetScore() {
levelFacadeOf(3, 3, 5, 200L).run {
assertEquals(getScore(), Score(0, 0, 9))
plantMinesExcept(3)
assertEquals(getScore(), Score(0, 5, 9))
field.filter { it.hasMine }.forEach { it.mark = Mark.Flag }
assertEquals(getScore(), Score(5, 5, 9))
field.first { it.hasMine }.apply {
isCovered = false
mistake = true
}
assertEquals(getScore(), Score(4, 5, 9))
}
}
@Test
fun testFlaggedAllMines() {
levelFacadeOf(3, 3, 5, 200L).run {

View file

@ -28,15 +28,15 @@ ext.versions = [
// Google
instantApp : '17.0.0',
material : '1.1.0',
playCore : '1.6.5',
playCore : '1.7.1',
// Kotlin
kotlin : '1.3.50',
kotlin : '1.3.70',
coroutines : '1.3.4',
// Jetpack
lifecycle : '1.1.1',
room : '2.2.4',
room : '2.2.5',
// Third Party
amplitude : '2.23.2',

View file

@ -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()