Merge pull request #35 from lucasnlm/open-mines-individually
Open mines individually
This commit is contained in:
commit
f0ded6f6e4
13 changed files with 340 additions and 152 deletions
|
@ -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"
|
||||
|
|
|
@ -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 -> { }
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue