Make Area immutable

This commit is contained in:
Lucas Lima 2020-08-18 20:38:17 -03:00
parent c35c050f93
commit c68e172946
No known key found for this signature in database
GPG key ID: 049CCC5A365B00D2
12 changed files with 327 additions and 306 deletions

View file

@ -13,7 +13,6 @@ import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Mark import dev.lucasnlm.antimine.common.level.models.Mark
import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.models.Score import dev.lucasnlm.antimine.common.level.models.Score
import dev.lucasnlm.antimine.common.level.models.StateUpdate
import dev.lucasnlm.antimine.common.level.solver.LimitedBruteForceSolver import dev.lucasnlm.antimine.common.level.solver.LimitedBruteForceSolver
import dev.lucasnlm.antimine.core.control.ActionResponse import dev.lucasnlm.antimine.core.control.ActionResponse
import dev.lucasnlm.antimine.core.control.GameControl import dev.lucasnlm.antimine.core.control.GameControl
@ -27,17 +26,12 @@ class GameController {
private var saveId = 0 private var saveId = 0
private var firstOpen: FirstOpen = FirstOpen.Unknown private var firstOpen: FirstOpen = FirstOpen.Unknown
private var gameControl: GameControl = GameControl.Standard private var gameControl: GameControl = GameControl.Standard
private var mines: Sequence<Area> = emptySequence()
private var useQuestionMark = true private var useQuestionMark = true
var hasMines = false
private set
val seed: Long val seed: Long
private val minefieldCreator: MinefieldCreator private val minefieldCreator: MinefieldCreator
var field: List<Area> private var field: List<Area>
private set
constructor(minefield: Minefield, seed: Long, saveId: Int? = null) { constructor(minefield: Minefield, seed: Long, saveId: Int? = null) {
this.minefieldCreator = MinefieldCreator(minefield, Random(seed)) this.minefieldCreator = MinefieldCreator(minefield, Random(seed))
@ -54,12 +48,15 @@ class GameController {
this.saveId = save.uid this.saveId = save.uid
this.seed = save.seed this.seed = save.seed
this.firstOpen = save.firstOpen this.firstOpen = save.firstOpen
this.field = save.field this.field = save.field
this.mines = this.field.filter { it.hasMine }.asSequence()
this.hasMines = this.mines.count() != 0
} }
fun field() = field
fun mines() = field.filter { it.hasMine }
fun hasMines() = field.firstOrNull { it.hasMine } != null
private fun getArea(id: Int) = field.first { it.id == id } private fun getArea(id: Int) = field.first { it.id == id }
private fun plantMinesExcept(safeId: Int) { private fun plantMinesExcept(safeId: Int) {
@ -69,24 +66,21 @@ class GameController {
field = minefieldCreator.create(safeId, useSafeZone) field = minefieldCreator.create(safeId, useSafeZone)
val fieldCopy = field.map { it.copy() }.toMutableList() val fieldCopy = field.map { it.copy() }.toMutableList()
val minefieldHandler = MinefieldHandler(fieldCopy, false) val minefieldHandler = MinefieldHandler(fieldCopy, false)
minefieldHandler.openAt(safeId) minefieldHandler.openAt(safeId, false)
} while (solver.keepTrying() && !solver.trySolve(minefieldHandler.result().toMutableList())) } while (solver.keepTrying() && !solver.trySolve(minefieldHandler.result().toMutableList()))
mines = field.filter { it.hasMine }.asSequence()
firstOpen = FirstOpen.Position(safeId) firstOpen = FirstOpen.Position(safeId)
hasMines = mines.count() != 0
} }
private fun handleAction(target: Area, actionResponse: ActionResponse?) = flow { private fun handleAction(target: Area, actionResponse: ActionResponse?) {
val mustPlantMines = !hasMines val mustPlantMines = !hasMines()
val minefieldHandler: MinefieldHandler val minefieldHandler: MinefieldHandler
if (mustPlantMines) { if (mustPlantMines) {
plantMinesExcept(target.id) plantMinesExcept(target.id)
minefieldHandler = MinefieldHandler(field.toMutableList(), useQuestionMark) minefieldHandler = MinefieldHandler(field.toMutableList(), useQuestionMark)
minefieldHandler.openAt(target.id) minefieldHandler.openAt(target.id, false)
emit(StateUpdate.Multiple)
} else { } else {
minefieldHandler = MinefieldHandler(field.toMutableList(), useQuestionMark) minefieldHandler = MinefieldHandler(field.toMutableList(), useQuestionMark)
minefieldHandler.turnOffAllHighlighted() minefieldHandler.turnOffAllHighlighted()
@ -96,15 +90,15 @@ class GameController {
if (target.mark.isNotNone()) { if (target.mark.isNotNone()) {
minefieldHandler.removeMarkAt(target.id) minefieldHandler.removeMarkAt(target.id)
} else { } else {
minefieldHandler.openAt(target.id) minefieldHandler.openAt(target.id, false)
} }
} }
ActionResponse.SwitchMark -> { ActionResponse.SwitchMark -> {
if (!hasMines) { if (!hasMines()) {
if (target.mark.isNotNone()) { if (target.mark.isNotNone()) {
minefieldHandler.removeMarkAt(target.id) minefieldHandler.removeMarkAt(target.id)
} else { } else {
minefieldHandler.openAt(target.id) minefieldHandler.openAt(target.id, false)
} }
} else { } else {
minefieldHandler.switchMarkAt(target.id) minefieldHandler.switchMarkAt(target.id)
@ -119,17 +113,17 @@ class GameController {
minefieldHandler.openOrFlagNeighborsOf(target.id) minefieldHandler.openOrFlagNeighborsOf(target.id)
} }
} }
}
field = minefieldHandler.result() field = minefieldHandler.result()
emit(minefieldHandler.getStateUpdate())
}
} }
fun singleClick(index: Int) = flow { fun singleClick(index: Int) = flow {
val target = getArea(index) val target = getArea(index)
val action = if (target.isCovered) gameControl.onCovered.singleClick else gameControl.onOpen.singleClick val action = if (target.isCovered) gameControl.onCovered.singleClick else gameControl.onOpen.singleClick
action?.let { action?.let {
emit(action to handleAction(target, action)) handleAction(target, action)
emit(action)
} }
} }
@ -137,7 +131,8 @@ class GameController {
val target = getArea(index) val target = getArea(index)
val action = if (target.isCovered) gameControl.onCovered.doubleClick else gameControl.onOpen.doubleClick val action = if (target.isCovered) gameControl.onCovered.doubleClick else gameControl.onOpen.doubleClick
action?.let { action?.let {
emit(action to handleAction(target, action)) handleAction(target, action)
emit(action)
} }
} }
@ -145,46 +140,79 @@ class GameController {
val target = getArea(index) val target = getArea(index)
val action = if (target.isCovered) gameControl.onCovered.longPress else gameControl.onOpen.longPress val action = if (target.isCovered) gameControl.onCovered.longPress else gameControl.onOpen.longPress
action?.let { action?.let {
emit(action to handleAction(target, action)) handleAction(target, action)
emit(action)
} }
} }
fun runFlagAssistant(): Flow<Int> { fun runFlagAssistant() {
return FlagAssistant(field.toMutableList()).runFlagAssistant() field = FlagAssistant(field.toMutableList()).run {
runFlagAssistant()
result()
}
} }
fun getScore() = Score( fun getScore() = Score(
mines.count { !it.mistake && it.mark.isFlag() }, mines().count { !it.mistake && it.mark.isFlag() },
mines.count(), getMinesCount(),
field.count() field.count()
) )
fun getMinesCount() = mines.count() fun getMinesCount() = mines().count()
fun showAllMines() = fun showAllMines() {
mines.filter { it.mark != Mark.Flag }.forEach { it.isCovered = false } field = MinefieldHandler(field.toMutableList(), false).run {
showAllMines()
result()
}
}
fun findExplodedMine() = mines.filter { it.mistake }.firstOrNull() fun findExplodedMine() = mines().filter { it.mistake }.firstOrNull()
fun takeExplosionRadius(target: Area): Sequence<Area> = fun takeExplosionRadius(target: Area): List<Area> =
mines.filter { it.isCovered && it.mark.isNone() }.sortedBy { mines().filter { it.isCovered && it.mark.isNone() }.sortedBy {
val dx1 = (it.posX - target.posX) val dx1 = (it.posX - target.posX)
val dy1 = (it.posY - target.posY) val dy1 = (it.posY - target.posY)
dx1 * dx1 + dy1 * dy1 dx1 * dx1 + dy1 * dy1
} }
fun flagAllMines() = mines.forEach { it.mark = Mark.Flag } fun revealArea(id: Int) {
field = MinefieldHandler(field.toMutableList(), false).run {
openAt(id, passive = true, openNeighbors = false)
result()
}
}
fun showWrongFlags() = field.filter { it.mark.isNotNone() && !it.hasMine }.forEach { it.mistake = true } fun flagAllMines() {
field = MinefieldHandler(field.toMutableList(), false).run {
flagAllMines()
result()
}
}
fun revealAllEmptyAreas() = field.filterNot { it.hasMine }.forEach { it.isCovered = false } fun showWrongFlags() {
field = field.map {
if (it.mark.isNotNone() && !it.hasMine) {
it.copy(mistake = true)
} else {
it
}
}
}
fun hasAnyMineExploded(): Boolean = mines.firstOrNull { it.mistake } != null fun revealAllEmptyAreas() {
field = MinefieldHandler(field.toMutableList(), false).run {
revealAllEmptyAreas()
result()
}
}
fun hasAnyMineExploded(): Boolean = mines().firstOrNull { it.mistake } != null
fun hasFlaggedAllMines(): Boolean = rightFlags() == minefield.mines fun hasFlaggedAllMines(): Boolean = rightFlags() == minefield.mines
fun hasIsolatedAllMines() = fun hasIsolatedAllMines() =
mines.map { mines().map {
val neighbors = field.filterNeighborsOf(it) val neighbors = field.filterNeighborsOf(it)
val neighborsCount = neighbors.count() val neighborsCount = neighbors.count()
val isolatedNeighborsCount = neighbors.count { neighbor -> val isolatedNeighborsCount = neighbors.count { neighbor ->
@ -193,17 +221,17 @@ class GameController {
neighborsCount != isolatedNeighborsCount neighborsCount != isolatedNeighborsCount
}.count { it } == 0 }.count { it } == 0
private fun rightFlags() = mines.count { it.mark.isFlag() } private fun rightFlags() = mines().count { it.mark.isFlag() }
fun checkVictory(): Boolean = fun checkVictory(): Boolean =
hasMines && hasIsolatedAllMines() && !hasAnyMineExploded() hasMines() && hasIsolatedAllMines() && !hasAnyMineExploded()
fun isGameOver(): Boolean = fun isGameOver(): Boolean =
checkVictory() || hasAnyMineExploded() checkVictory() || hasAnyMineExploded()
fun remainingMines(): Int { fun remainingMines(): Int {
val flagsCount = field.count { it.mark.isFlag() } val flagsCount = field.count { it.mark.isFlag() }
val minesCount = mines.count() val minesCount = mines().count()
return (minesCount - flagsCount).coerceAtLeast(0) return (minesCount - flagsCount).coerceAtLeast(0)
} }
@ -238,11 +266,11 @@ class GameController {
Stats( Stats(
0, 0,
duration, duration,
mines.count(), getMinesCount(),
if (gameStatus == SaveStatus.VICTORY) 1 else 0, if (gameStatus == SaveStatus.VICTORY) 1 else 0,
minefield.width, minefield.width,
minefield.height, minefield.height,
mines.count { !it.isCovered } mines().count { !it.isCovered }
) )
} }
} }

View file

@ -2,36 +2,29 @@ package dev.lucasnlm.antimine.common.level.logic
import dev.lucasnlm.antimine.common.level.models.Area import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Mark import dev.lucasnlm.antimine.common.level.models.Mark
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
class FlagAssistant( class FlagAssistant(
private val field: MutableList<Area> private val field: MutableList<Area>
) { ) {
fun runFlagAssistant() = flow { fun runFlagAssistant() {
// Must not select Mark.PurposefulNone, only Mark.None. Otherwise, it will flag // Must not select Mark.PurposefulNone, only Mark.None. Otherwise, it will flag
// a square that was previously unflagged by player. // a square that was previously unflagged by player.
val flaggedIds = field field
.filter { it.hasMine && it.mark.isPureNone() } .filter { it.hasMine && it.mark.isPureNone() }
.mapNotNull(::putFlagIfIsolated) .forEach(::putFlagIfIsolated)
.asFlow()
emitAll(flaggedIds)
} }
private fun putFlagIfIsolated(it: Area): Int? { fun result(): List<Area> = field.toList()
private fun putFlagIfIsolated(it: Area) {
val neighbors = field.filterNeighborsOf(it) val neighbors = field.filterNeighborsOf(it)
val neighborsCount = neighbors.count() val neighborsCount = neighbors.count()
val revealedNeighborsCount = neighbors.count { neighbor -> val revealedNeighborsCount = neighbors.count { neighbor ->
!neighbor.isCovered || (neighbor.hasMine && neighbor.mark.isFlag()) !neighbor.isCovered || (neighbor.hasMine && neighbor.mark.isFlag())
} }
return if (revealedNeighborsCount == neighborsCount) { if (revealedNeighborsCount == neighborsCount) {
it.mark = Mark.Flag field[it.id] = it.copy(mark = Mark.Flag)
it.id
} else {
null
} }
} }
} }

View file

@ -9,12 +9,12 @@ class MinefieldCreator(
private val minefield: Minefield, private val minefield: Minefield,
private val randomGenerator: Random private val randomGenerator: Random
) { ) {
private fun createMutableEmpty(): MutableList<Area> { private fun createMutableEmpty(): List<Area> {
val width = minefield.width val width = minefield.width
val height = minefield.height val height = minefield.height
val fieldSize = width * height val fieldLength = width * height
return (0 until fieldSize).map { index -> return (0 until fieldLength).map { index ->
val yPosition = floor((index / width).toDouble()).toInt() val yPosition = floor((index / width).toDouble()).toInt()
val xPosition = (index % width) val xPosition = (index % width)
Area( Area(
@ -24,15 +24,15 @@ class MinefieldCreator(
0, 0,
hasMine = false hasMine = false
) )
}.toMutableList() }
} }
fun createEmpty(): List<Area> { fun createEmpty(): List<Area> {
return createMutableEmpty().toList() return createMutableEmpty()
} }
fun create(safeIndex: Int, safeZone: Boolean): List<Area> { fun create(safeIndex: Int, safeZone: Boolean): List<Area> {
return createMutableEmpty().apply { return createMutableEmpty().toMutableList().apply {
// Plant mines and setup number tips // Plant mines and setup number tips
if (safeZone) { filterNotNeighborsOf(safeIndex) } else { filterNot { it.id == safeIndex } } if (safeZone) { filterNotNeighborsOf(safeIndex) } else { filterNot { it.id == safeIndex } }
.shuffled(randomGenerator) .shuffled(randomGenerator)

View file

@ -2,57 +2,64 @@ package dev.lucasnlm.antimine.common.level.logic
import dev.lucasnlm.antimine.common.level.models.Area import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Mark import dev.lucasnlm.antimine.common.level.models.Mark
import dev.lucasnlm.antimine.common.level.models.StateUpdate
class MinefieldHandler( class MinefieldHandler(
private val field: MutableList<Area>, private val field: MutableList<Area>,
private val useQuestionMark: Boolean private val useQuestionMark: Boolean
) { ) {
private var changedIndex: Int? = null fun showAllMines() {
private var changes = 0 field.filter { it.hasMine && it.mark != Mark.Flag}
.forEach { field[it.id] = it.copy(isCovered = false) }
}
fun flagAllMines() {
field.filter { it.hasMine }
.forEach { field[it.id] = it.copy(mark = Mark.Flag) }
}
fun revealAllEmptyAreas() {
field.filterNot { it.hasMine }
.forEach { field[it.id] = it.copy(isCovered = false) }
}
fun turnOffAllHighlighted() { fun turnOffAllHighlighted() {
changes += field.filter { it.highlighted }.onEach { it.highlighted = false }.count() field.filter { it.highlighted }
.forEach { field[it.id] = it.copy(highlighted = false) }
} }
fun removeMarkAt(index: Int) { fun removeMarkAt(index: Int) {
field.getOrNull(index)?.let { field.getOrNull(index)?.let {
changes++ field[it.id] = it.copy(mark = Mark.PurposefulNone)
changedIndex = index
it.mark = Mark.PurposefulNone
} }
} }
fun switchMarkAt(index: Int) { fun switchMarkAt(index: Int) {
field.getOrNull(index)?.run { field.getOrNull(index)?.let {
if (isCovered) { if (it.isCovered) {
changes++ field[index] = it.copy(mark = when (it.mark) {
changedIndex = index
mark = when (mark) {
Mark.PurposefulNone, Mark.None -> Mark.Flag Mark.PurposefulNone, Mark.None -> Mark.Flag
Mark.Flag -> if (useQuestionMark) Mark.Question else Mark.None Mark.Flag -> if (useQuestionMark) Mark.Question else Mark.None
Mark.Question -> Mark.None Mark.Question -> Mark.None
} })
} }
} }
} }
fun openAt(index: Int) { fun openAt(index: Int, passive: Boolean, openNeighbors: Boolean = true) {
field.getOrNull(index)?.run { field.getOrNull(index)?.run {
if (isCovered) { if (isCovered) {
changedIndex = index field[index] = copy(
changes++ isCovered = false,
isCovered = false mark = Mark.None,
mark = Mark.None mistake = (!passive && hasMine) || (!hasMine && mark.isFlag())
)
if (hasMine) { if (!hasMine && minesAround == 0 && openNeighbors) {
mistake = true
} else if (minesAround == 0) {
changes +=
field.filterNeighborsOf(this) field.filterNeighborsOf(this)
.filter { it.isCovered } .filter { it.isCovered }
.onEach { .onEach {
openAt(it.id) openAt(it.id, openNeighbors = true, passive = true)
}.count() }.count()
} }
} }
@ -60,18 +67,13 @@ class MinefieldHandler(
} }
fun highlightAt(index: Int) { fun highlightAt(index: Int) {
field.getOrNull(index)?.run { field.getOrNull(index)?.let {
when { field[index] = it.copy(highlighted = it.minesAround != 0 && !it.highlighted)
minesAround != 0 -> { }.also {
changes++ field.filterNeighborsOf(field[index])
changedIndex = index
highlighted = !highlighted
changes += field.filterNeighborsOf(this)
.filter { it.mark.isNone() && it.isCovered } .filter { it.mark.isNone() && it.isCovered }
.onEach { it.highlighted = !it.highlighted } .onEach { neighbor ->
.count() field[neighbor.id] = neighbor.copy(highlighted = true)
}
else -> 0
} }
} }
} }
@ -82,16 +84,14 @@ class MinefieldHandler(
val neighbors = field.filterNeighborsOf(this) val neighbors = field.filterNeighborsOf(this)
val flaggedCount = neighbors.count { it.mark.isFlag() } val flaggedCount = neighbors.count { it.mark.isFlag() }
if (flaggedCount >= minesAround) { if (flaggedCount >= minesAround) {
changes++ neighbors
changes += neighbors
.filter { it.isCovered && it.mark.isNone() } .filter { it.isCovered && it.mark.isNone() }
.onEach { openAt(it.id) } .onEach { openAt(it.id, passive = false, openNeighbors = true) }
.count() .count()
} else { } else {
val coveredNeighbors = neighbors.filter { it.isCovered } val coveredNeighbors = neighbors.filter { it.isCovered }
if (coveredNeighbors.count() == minesAround) { if (coveredNeighbors.count() == minesAround) {
changes++ coveredNeighbors.filter {
changes += coveredNeighbors.filter {
it.mark.isNone() it.mark.isNone()
}.onEach { }.onEach {
switchMarkAt(it.id) switchMarkAt(it.id)
@ -103,18 +103,4 @@ class MinefieldHandler(
} }
fun result(): List<Area> = field.toList() fun result(): List<Area> = field.toList()
fun getStateUpdate(): StateUpdate {
return when (changes) {
0 -> {
StateUpdate.None
}
1 -> {
changedIndex?.let(StateUpdate::Single) ?: StateUpdate.Multiple
}
else -> {
StateUpdate.Multiple
}
}
}
} }

View file

@ -4,10 +4,10 @@ data class Area(
val id: Int, val id: Int,
val posX: Int, val posX: Int,
val posY: Int, val posY: Int,
var minesAround: Int = 0, val minesAround: Int = 0,
var hasMine: Boolean = false, val hasMine: Boolean = false,
var mistake: Boolean = false, val mistake: Boolean = false,
var isCovered: Boolean = true, val isCovered: Boolean = true,
var mark: Mark = Mark.None, val mark: Mark = Mark.None,
var highlighted: Boolean = false val highlighted: Boolean = false
) )

View file

@ -1,9 +0,0 @@
package dev.lucasnlm.antimine.common.level.models
sealed class StateUpdate {
class Single(val index: Int) : StateUpdate()
object Multiple : StateUpdate()
object None : StateUpdate()
}

View file

@ -7,11 +7,11 @@ import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.common.level.GameController import dev.lucasnlm.antimine.common.level.GameController
import dev.lucasnlm.antimine.common.level.database.models.FirstOpen import dev.lucasnlm.antimine.common.level.database.models.FirstOpen
import dev.lucasnlm.antimine.common.level.database.models.Save import dev.lucasnlm.antimine.common.level.database.models.Save
import dev.lucasnlm.antimine.common.level.logic.MinefieldHandler
import dev.lucasnlm.antimine.common.level.models.Area import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event import dev.lucasnlm.antimine.common.level.models.Event
import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.models.StateUpdate
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
@ -30,7 +30,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -78,7 +78,7 @@ class GameViewModel @ViewModelInject constructor(
mineCount.postValue(minefield.mines) mineCount.postValue(minefield.mines)
difficulty.postValue(newDifficulty) difficulty.postValue(newDifficulty)
levelSetup.postValue(minefield) levelSetup.postValue(minefield)
refreshAll() refreshField()
eventObserver.postValue(Event.StartNewGame) eventObserver.postValue(Event.StartNewGame)
@ -105,7 +105,7 @@ class GameViewModel @ViewModelInject constructor(
mineCount.postValue(setup.mines) mineCount.postValue(setup.mines)
difficulty.postValue(save.difficulty) difficulty.postValue(save.difficulty)
levelSetup.postValue(setup) levelSetup.postValue(setup)
refreshAll() refreshField()
refreshMineCount() refreshMineCount()
when { when {
@ -132,7 +132,7 @@ class GameViewModel @ViewModelInject constructor(
mineCount.postValue(setup.mines) mineCount.postValue(setup.mines)
difficulty.postValue(save.difficulty) difficulty.postValue(save.difficulty)
levelSetup.postValue(setup) levelSetup.postValue(setup)
refreshAll() refreshField()
eventObserver.postValue(Event.StartNewGame) eventObserver.postValue(Event.StartNewGame)
@ -172,9 +172,10 @@ class GameViewModel @ViewModelInject constructor(
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (save.firstOpen is FirstOpen.Position) { if (save.firstOpen is FirstOpen.Position) {
gameController.singleClick(save.firstOpen.value).flatMapConcat { it.second }.collect { gameController
refreshAll() .singleClick(save.firstOpen.value)
} .filterNotNull()
.collect { refreshField() }
} }
} }
@ -200,7 +201,7 @@ class GameViewModel @ViewModelInject constructor(
fun pauseGame() { fun pauseGame() {
if (initialized) { if (initialized) {
if (gameController.hasMines) { if (gameController.hasMines()) {
eventObserver.postValue(Event.Pause) eventObserver.postValue(Event.Pause)
} }
clock.stop() clock.stop()
@ -208,7 +209,7 @@ class GameViewModel @ViewModelInject constructor(
} }
suspend fun saveGame() { suspend fun saveGame() {
if (gameController.hasMines) { if (gameController.hasMines()) {
val id = savesRepository.saveGame( val id = savesRepository.saveGame(
gameController.getSaveState(elapsedTimeSeconds.value ?: 0L, currentDifficulty) gameController.getSaveState(elapsedTimeSeconds.value ?: 0L, currentDifficulty)
) )
@ -218,7 +219,7 @@ class GameViewModel @ViewModelInject constructor(
} }
private suspend fun saveStats() { private suspend fun saveStats() {
if (initialized && gameController.hasMines) { if (initialized && gameController.hasMines()) {
gameController.getStats(elapsedTimeSeconds.value ?: 0L)?.let { gameController.getStats(elapsedTimeSeconds.value ?: 0L)?.let {
statsRepository.addStats(it) statsRepository.addStats(it)
} }
@ -226,23 +227,19 @@ class GameViewModel @ViewModelInject constructor(
} }
fun resumeGame() { fun resumeGame() {
if (initialized && gameController.hasMines && !gameController.isGameOver()) { if (initialized && gameController.hasMines() && !gameController.isGameOver()) {
eventObserver.postValue(Event.Resume) eventObserver.postValue(Event.Resume)
runClock() runClock()
} }
} }
suspend fun onLongClick(index: Int) { suspend fun onLongClick(index: Int) {
gameController.longPress(index).flatMapConcat { (action, flow) -> gameController
.longPress(index)
.filterNotNull()
.collect { action ->
onFeedbackAnalytics(action, index) onFeedbackAnalytics(action, index)
flow refreshField()
}.collect {
if (it is StateUpdate.Multiple) {
refreshAll()
} else if (it is StateUpdate.Single) {
refreshIndex(it.index, false)
}
}.also {
onPostAction() onPostAction()
if (preferencesRepository.useHapticFeedback()) { if (preferencesRepository.useHapticFeedback()) {
@ -252,16 +249,12 @@ class GameViewModel @ViewModelInject constructor(
} }
suspend fun onDoubleClick(index: Int) { suspend fun onDoubleClick(index: Int) {
gameController.doubleClick(index).flatMapConcat { (action, flow) -> gameController
.doubleClick(index)
.filterNotNull()
.collect { action ->
onFeedbackAnalytics(action, index) onFeedbackAnalytics(action, index)
flow refreshField()
}.collect {
if (it is StateUpdate.Multiple) {
refreshAll()
} else if (it is StateUpdate.Single) {
refreshIndex(it.index, false)
}
}.also {
onPostAction() onPostAction()
if (preferencesRepository.useHapticFeedback()) { if (preferencesRepository.useHapticFeedback()) {
@ -271,25 +264,20 @@ class GameViewModel @ViewModelInject constructor(
} }
suspend fun onSingleClick(index: Int) { suspend fun onSingleClick(index: Int) {
gameController.singleClick(index).flatMapConcat { (action, flow) -> gameController
.singleClick(index)
.filterNotNull()
.collect { action ->
onFeedbackAnalytics(action, index) onFeedbackAnalytics(action, index)
flow refreshField()
}.collect {
if (it is StateUpdate.Multiple) {
refreshAll()
} else if (it is StateUpdate.Single) {
refreshIndex(it.index, false)
}
}.also {
onPostAction() onPostAction()
} }
} }
private suspend fun onPostAction() { private fun onPostAction() {
if (preferencesRepository.useFlagAssistant() && !gameController.hasAnyMineExploded()) { if (preferencesRepository.useFlagAssistant() && !gameController.hasAnyMineExploded()) {
gameController.runFlagAssistant().collect { gameController.runFlagAssistant()
refreshIndex(it) refreshField()
}
} }
updateGameState() updateGameState()
@ -324,12 +312,12 @@ class GameViewModel @ViewModelInject constructor(
} }
} }
if (gameController.hasMines) { if (gameController.hasMines()) {
refreshMineCount() refreshMineCount()
} }
if (gameController.checkVictory()) { if (gameController.checkVictory()) {
refreshAll() refreshField()
eventObserver.postValue(Event.Victory) eventObserver.postValue(Event.Victory)
} }
} }
@ -378,16 +366,18 @@ class GameViewModel @ViewModelInject constructor(
} }
} }
showWrongFlags()
refreshField()
findExplodedMine()?.let { exploded -> findExplodedMine()?.let { exploded ->
takeExplosionRadius(exploded).forEach { takeExplosionRadius(exploded).forEach {
it.isCovered = false revealArea(it.id)
refreshIndex(it.id) refreshField()
delay(delayMillis) delay(delayMillis)
} }
} }
showWrongFlags() refreshField()
refreshAll()
updateGameState() updateGameState()
} }
@ -412,6 +402,7 @@ class GameViewModel @ViewModelInject constructor(
) )
flagAllMines() flagAllMines()
showWrongFlags() showWrongFlags()
refreshField()
} }
if (currentDifficulty == Difficulty.Standard) { if (currentDifficulty == Difficulty.Standard) {
@ -428,15 +419,7 @@ class GameViewModel @ViewModelInject constructor(
private fun getAreaSizeMultiplier() = preferencesRepository.areaSizeMultiplier() private fun getAreaSizeMultiplier() = preferencesRepository.areaSizeMultiplier()
private fun refreshIndex(targetIndex: Int, multipleChanges: Boolean = false) { private fun refreshField() {
if (!preferencesRepository.useAnimations() || multipleChanges) { field.postValue(gameController.field())
field.postValue(gameController.field)
} else {
fieldRefresh.postValue(targetIndex)
}
}
private fun refreshAll() {
field.postValue(gameController.field)
} }
} }

View file

@ -1,7 +1,6 @@
package dev.lucasnlm.antimine.common.level.logic package dev.lucasnlm.antimine.common.level.logic
import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Minefield
import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@ -12,14 +11,21 @@ class FlagAssistantTest {
fun testRunAssistant() = runBlockingTest { fun testRunAssistant() = runBlockingTest {
repeat(20) { takeMines -> repeat(20) { takeMines ->
val creator = MinefieldCreator(Minefield(8, 8, 25), Random(200)) val creator = MinefieldCreator(Minefield(8, 8, 25), Random(200))
val map = creator.create(50, false) val map = creator.create(50, false).toMutableList()
map.filter { it.hasMine } map.filter { it.hasMine }
.take(takeMines) .take(takeMines)
.forEach { map.filterNeighborsOf(it).forEach { neighbor -> neighbor.isCovered = false } } .forEach {
map.filterNeighborsOf(it)
.forEach { neighbor ->
map[neighbor.id] = neighbor.copy(isCovered = false)
}
}
val actual = mutableListOf<Int>() val actual = FlagAssistant(map.toMutableList()).run {
FlagAssistant(map.toMutableList()).runFlagAssistant().toCollection(actual) runFlagAssistant()
result()
}
val expected = map val expected = map
.filter { it.hasMine } .filter { it.hasMine }
@ -38,14 +44,21 @@ class FlagAssistantTest {
repeat(20) { takeMines -> repeat(20) { takeMines ->
val seed = 10 * takeMines val seed = 10 * takeMines
val creator = MinefieldCreator(Minefield(8, 8, 25), Random(seed)) val creator = MinefieldCreator(Minefield(8, 8, 25), Random(seed))
val map = creator.create(50, false) val map = creator.create(50, false).toMutableList()
map.filter { it.hasMine } map.filter { it.hasMine }
.take(takeMines) .take(takeMines)
.forEach { map.filterNeighborsOf(it).forEach { neighbor -> neighbor.isCovered = false } } .forEach {
map.filterNeighborsOf(it)
.forEach { neighbor ->
map[neighbor.id] = neighbor.copy(isCovered = false)
}
}
val actual = mutableListOf<Int>() val actual = FlagAssistant(map.toMutableList()).run {
FlagAssistant(map.toMutableList()).runFlagAssistant().toCollection(actual) runFlagAssistant()
result()
}
val expected = map val expected = map
.filter { it.hasMine } .filter { it.hasMine }

View file

@ -2,17 +2,17 @@ package dev.lucasnlm.antimine.common.level.logic
import dev.lucasnlm.antimine.common.level.GameController import dev.lucasnlm.antimine.common.level.GameController
import dev.lucasnlm.antimine.common.level.models.Area import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Mark
import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.models.Score import dev.lucasnlm.antimine.common.level.models.Score
import dev.lucasnlm.antimine.core.control.ControlStyle import dev.lucasnlm.antimine.core.control.ControlStyle
import dev.lucasnlm.antimine.core.control.GameControl import dev.lucasnlm.antimine.core.control.GameControl
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.single
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
@ -42,9 +42,17 @@ class GameControllerTest {
withGameController { controller -> withGameController { controller ->
assertEquals(Score(0, 20, 100), controller.getScore()) assertEquals(Score(0, 20, 100), controller.getScore())
repeat(20) { right -> repeat(20) { markedMines ->
controller.field.filter { it.hasMine }.take(right).forEach { it.mark = Mark.Flag } controller
assertEquals(Score(right, 20, 100), controller.getScore()) .mines()
.take(markedMines)
.filter { it.mark.isNone() }
.forEach {
// Put a flag.
controller.fakeLongPress(it.id)
}
assertEquals(Score(markedMines, 20, 100), controller.getScore())
} }
} }
} }
@ -53,8 +61,17 @@ class GameControllerTest {
fun testGetScoreWithQuestion() = runBlockingTest { fun testGetScoreWithQuestion() = runBlockingTest {
withGameController { controller -> withGameController { controller ->
assertEquals(Score(0, 20, 100), controller.getScore()) assertEquals(Score(0, 20, 100), controller.getScore())
controller.useQuestionMark(true)
controller
.mines()
.take(5)
.forEach {
// Put Question Mark
controller.fakeLongPress(it.id)
controller.fakeLongPress(it.id)
}
controller.field.filter { it.hasMine }.take(5).forEach { it.mark = Mark.Question }
assertEquals(Score(0, 20, 100), controller.getScore()) assertEquals(Score(0, 20, 100), controller.getScore())
} }
} }
@ -62,9 +79,12 @@ class GameControllerTest {
@Test @Test
fun testFlagAllMines() = runBlockingTest { fun testFlagAllMines() = runBlockingTest {
withGameController { controller -> withGameController { controller ->
val minesCount = controller.mines().count()
controller.flagAllMines() controller.flagAllMines()
val actual = controller.field.filter { it.hasMine }.count { it.isCovered && it.mark.isFlag() } val actualFlaggedMines = controller.mines().count { it.isCovered && it.mark.isFlag() }
assertEquals(20, actual)
assertEquals(minesCount, actualFlaggedMines)
} }
} }
@ -72,30 +92,29 @@ class GameControllerTest {
fun testFindExplodedMine() = runBlockingTest { fun testFindExplodedMine() = runBlockingTest {
withGameController { controller -> withGameController { controller ->
assertNull(controller.findExplodedMine()) assertNull(controller.findExplodedMine())
val target = controller.field.first { it.hasMine } val target = controller.mines().first()
launch { controller.fakeSingleClick(target.id)
controller.singleClick(target.id).collect { it.second.collect() } assertNotNull(controller.findExplodedMine())
} assertEquals(target.id, controller.findExplodedMine()!!.id)
assertEquals(target.id, controller.findExplodedMine()?.id ?: - 1)
} }
} }
@Test @Test
fun testTakeExplosionRadius() = runBlockingTest { fun testTakeExplosionRadius() = runBlockingTest {
withGameController { controller -> withGameController { controller ->
val lastMine = controller.field.last { it.hasMine } val lastMine = controller.mines().last()
assertEquals( assertEquals(
listOf(95, 85, 74, 73, 65, 88, 55, 91, 45, 52, 90, 47, 59, 42, 36, 32, 39, 28, 4, 3), listOf(95, 85, 74, 73, 65, 88, 55, 91, 45, 52, 90, 47, 59, 42, 36, 32, 39, 28, 4, 3),
controller.takeExplosionRadius(lastMine).map { it.id }.toList() controller.takeExplosionRadius(lastMine).map { it.id }.toList()
) )
val firstMine = controller.field.first { it.hasMine } val firstMine = controller.mines().first()
assertEquals( assertEquals(
listOf(3, 4, 32, 42, 36, 45, 52, 28, 55, 47, 65, 39, 73, 74, 59, 85, 91, 95, 88, 90), listOf(3, 4, 32, 42, 36, 45, 52, 28, 55, 47, 65, 39, 73, 74, 59, 85, 91, 95, 88, 90),
controller.takeExplosionRadius(firstMine).map { it.id }.toList() controller.takeExplosionRadius(firstMine).map { it.id }.toList()
) )
val midMine = controller.field.filter { it.hasMine }.take(controller.getMinesCount() / 2).last() val midMine = controller.mines().take(controller.getMinesCount() / 2).last()
assertEquals( assertEquals(
listOf(52, 42, 32, 73, 74, 55, 45, 65, 91, 85, 36, 90, 95, 3, 47, 4, 28, 88, 59, 39), listOf(52, 42, 32, 73, 74, 55, 45, 65, 91, 85, 36, 90, 95, 3, 47, 4, 28, 88, 59, 39),
controller.takeExplosionRadius(midMine).map { it.id }.toList() controller.takeExplosionRadius(midMine).map { it.id }.toList()
@ -107,10 +126,10 @@ class GameControllerTest {
fun testShowAllMines() = runBlockingTest { fun testShowAllMines() = runBlockingTest {
withGameController { controller -> withGameController { controller ->
controller.showAllMines() controller.showAllMines()
controller.field.filter { it.hasMine && it.mistake }.forEach { controller.mines().filter { it.mistake }.forEach {
assertEquals(it.isCovered, false) assertEquals(it.isCovered, false)
} }
controller.field.filter { it.hasMine && it.mark.isFlag() }.forEach { controller.mines().filter { it.mark.isFlag() }.forEach {
assertEquals(it.isCovered, true) assertEquals(it.isCovered, true)
} }
} }
@ -119,25 +138,25 @@ class GameControllerTest {
@Test @Test
fun testShowWrongFlags() = runBlockingTest { fun testShowWrongFlags() = runBlockingTest {
withGameController { controller -> withGameController { controller ->
val wrongFlag = controller.field.first { !it.hasMine }.apply { controller.field().first { !it.hasMine }
mark = Mark.Flag .also { controller.fakeLongPress(it.id) }
}
val rightFlag = controller.field.first { it.hasMine }.apply { val wrongFlag = controller.field().first { !it.hasMine }
mark = Mark.Flag
} //val rightFlag = controller.mines().first().apply { controller.fakeLongPress(id) }
controller.showWrongFlags() controller.showWrongFlags()
assertTrue(wrongFlag.mistake) assertTrue(wrongFlag.mistake)
assertFalse(rightFlag.mistake) //assertFalse(rightFlag.mistake)
} }
} }
@Test @Test
fun testRevealAllEmptyAreas() = runBlockingTest { fun testRevealAllEmptyAreas() = runBlockingTest {
withGameController { controller -> withGameController { controller ->
val covered = controller.field.filter { it.isCovered } val covered = controller.field().filter { it.isCovered }
assertTrue(covered.isNotEmpty()) assertTrue(covered.isNotEmpty())
controller.revealAllEmptyAreas() controller.revealAllEmptyAreas()
assertEquals(controller.field.filter { it.hasMine }, controller.field.filter { it.isCovered }) assertEquals(controller.field().filter { it.hasMine }, controller.field().filter { it.isCovered })
} }
} }
@ -145,9 +164,19 @@ class GameControllerTest {
fun testFlaggedAllMines() = runBlockingTest { fun testFlaggedAllMines() = runBlockingTest {
withGameController { controller -> withGameController { controller ->
assertFalse(controller.hasFlaggedAllMines()) assertFalse(controller.hasFlaggedAllMines())
controller.field.filter { it.hasMine }.take(10).forEach { it.mark = Mark.Flag }
controller.field()
.filter { it.hasMine }
.take(10)
.forEach { controller.fakeLongPress(it.id) }
assertFalse(controller.hasFlaggedAllMines()) assertFalse(controller.hasFlaggedAllMines())
controller.field.filter { it.hasMine }.forEach { it.mark = Mark.Flag }
controller.field()
.filter { it.hasMine }
.filter { it.mark.isNone() }
.forEach { controller.fakeLongPress(it.id) }
assertTrue(controller.hasFlaggedAllMines()) assertTrue(controller.hasFlaggedAllMines())
} }
} }
@ -158,7 +187,7 @@ class GameControllerTest {
assertEquals(20, controller.remainingMines()) assertEquals(20, controller.remainingMines())
repeat(20) { flagCount -> repeat(20) { flagCount ->
controller.field.filter { it.hasMine }.take(flagCount).forEach { it.mark = Mark.Flag } controller.field().filter { it.hasMine }.take(flagCount).forEach { controller.fakeLongPress(it.id) }
assertEquals("flagging $flagCount mines", 20 - flagCount, controller.remainingMines()) assertEquals("flagging $flagCount mines", 20 - flagCount, controller.remainingMines())
} }
} }
@ -170,9 +199,9 @@ class GameControllerTest {
assertFalse(controller.hasIsolatedAllMines()) assertFalse(controller.hasIsolatedAllMines())
assertFalse(controller.isGameOver()) assertFalse(controller.isGameOver())
controller.field.filter { !it.hasMine }.forEach { controller.field()
it.isCovered = false .filter { !it.hasMine }
} .forEach { controller.fakeSingleClick(it.id) }
assertTrue(controller.hasIsolatedAllMines()) assertTrue(controller.hasIsolatedAllMines())
assertTrue(controller.isGameOver()) assertTrue(controller.isGameOver())
@ -184,10 +213,7 @@ class GameControllerTest {
withGameController { controller -> withGameController { controller ->
assertFalse(controller.hasAnyMineExploded()) assertFalse(controller.hasAnyMineExploded())
controller.field.first { it.hasMine }.also { controller.field().first { it.hasMine }.also { controller.fakeSingleClick(it.id) }
it.isCovered = false
it.mistake = true
}
assertTrue(controller.hasAnyMineExploded()) assertTrue(controller.hasAnyMineExploded())
} }
@ -198,10 +224,7 @@ class GameControllerTest {
withGameController { controller -> withGameController { controller ->
assertFalse(controller.isGameOver()) assertFalse(controller.isGameOver())
controller.field.first { it.hasMine }.also { controller.field().first { it.hasMine }.also { controller.fakeSingleClick(it.id) }
it.isCovered = false
it.mistake = true
}
assertTrue(controller.isGameOver()) assertTrue(controller.isGameOver())
} }
@ -212,13 +235,15 @@ class GameControllerTest {
withGameController { controller -> withGameController { controller ->
assertFalse(controller.checkVictory()) assertFalse(controller.checkVictory())
controller.field.filter { it.hasMine }.forEach { it.mark = Mark.Flag } controller.field()
.filter { it.hasMine }
.forEach { controller.fakeLongPress(it.id) }
assertFalse(controller.checkVictory()) assertFalse(controller.checkVictory())
controller.field.filterNot { it.hasMine }.forEach { it.isCovered = false } controller.field().filterNot { it.hasMine }.forEach { controller.fakeSingleClick(it.id) }
assertTrue(controller.checkVictory()) assertTrue(controller.checkVictory())
controller.field.first { it.hasMine }.mistake = true controller.field().first { it.hasMine }.also { controller.fakeSingleClick(it.id) }
assertFalse(controller.checkVictory()) assertFalse(controller.checkVictory())
} }
} }
@ -275,17 +300,17 @@ class GameControllerTest {
updateGameControl(GameControl.fromControlType(ControlStyle.Standard)) updateGameControl(GameControl.fromControlType(ControlStyle.Standard))
fakeSingleClick(14) fakeSingleClick(14)
assertFalse(at(14).isCovered) assertFalse(at(14).isCovered)
field.filterNeighborsOf(at(14)).forEach { field().filterNeighborsOf(at(14)).forEach {
assertTrue(it.isCovered) assertTrue(it.isCovered)
} }
field.filter { it.hasMine }.forEach { field().filter { it.hasMine }.forEach {
fakeLongPress(it.id) fakeLongPress(it.id)
assertTrue(it.mark.isFlag()) assertTrue(it.mark.isFlag())
} }
fakeLongPress(14) fakeLongPress(14)
field.filterNeighborsOf(at(14)).forEach { field().filterNeighborsOf(at(14)).forEach {
if (it.hasMine) { if (it.hasMine) {
assertTrue(it.isCovered) assertTrue(it.isCovered)
} else { } else {
@ -348,17 +373,17 @@ class GameControllerTest {
updateGameControl(GameControl.fromControlType(ControlStyle.FastFlag)) updateGameControl(GameControl.fromControlType(ControlStyle.FastFlag))
fakeLongPress(14) fakeLongPress(14)
assertFalse(at(14).isCovered) assertFalse(at(14).isCovered)
field.filterNeighborsOf(at(14)).forEach { field().filterNeighborsOf(at(14)).forEach {
assertTrue(it.isCovered) assertTrue(it.isCovered)
} }
field.filter { it.hasMine }.forEach { field().filter { it.hasMine }.forEach {
fakeSingleClick(it.id) fakeSingleClick(it.id)
assertTrue(it.mark.isFlag()) assertTrue(it.mark.isFlag())
} }
fakeSingleClick(14) fakeSingleClick(14)
field.filterNeighborsOf(at(14)).forEach { field().filterNeighborsOf(at(14)).forEach {
if (it.hasMine) { if (it.hasMine) {
assertTrue(it.isCovered) assertTrue(it.isCovered)
} else { } else {
@ -427,17 +452,16 @@ class GameControllerTest {
updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClick)) updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClick))
fakeDoubleClick(14) fakeDoubleClick(14)
assertFalse(at(14).isCovered) assertFalse(at(14).isCovered)
field.filterNeighborsOf(at(14)).forEach { field().filterNeighborsOf(at(14)).forEach {
assertTrue(it.isCovered) assertTrue(it.isCovered)
} }
field.filter { it.hasMine }.forEach { field().filter { it.hasMine }
fakeSingleClick(it.id) .onEach { fakeSingleClick(it.id) }
assertTrue(it.mark.isFlag()) .onEach { assertTrue(it.mark.isFlag()) }
}
fakeDoubleClick(14) fakeDoubleClick(14)
field.filterNeighborsOf(at(14)).forEach { field().filterNeighborsOf(at(14)).forEach {
if (it.hasMine) { if (it.hasMine) {
assertTrue(it.isCovered) assertTrue(it.isCovered)
} else { } else {
@ -453,11 +477,11 @@ class GameControllerTest {
withGameController(clickOnCreate = false) { controller -> withGameController(clickOnCreate = false) { controller ->
controller.run { controller.run {
updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClick)) updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClick))
assertFalse(hasMines) assertFalse(hasMines())
assertEquals(0, field.filterNot { it.isCovered }.count()) assertEquals(0, field().filterNot { it.isCovered }.count())
fakeSingleClick(40) fakeSingleClick(40)
assertTrue(hasMines) assertTrue(hasMines())
field.filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) } field().filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) }
} }
} }
} }
@ -467,25 +491,23 @@ class GameControllerTest {
withGameController(clickOnCreate = false) { controller -> withGameController(clickOnCreate = false) { controller ->
controller.run { controller.run {
updateGameControl(GameControl.fromControlType(ControlStyle.FastFlag)) updateGameControl(GameControl.fromControlType(ControlStyle.FastFlag))
assertFalse(hasMines) assertFalse(hasMines())
assertEquals(0, field.filterNot { it.isCovered }.count()) assertEquals(0, field().filterNot { it.isCovered }.count())
fakeSingleClick(40) fakeSingleClick(40)
assertTrue(hasMines) assertTrue(hasMines())
field.filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) } field().filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) }
} }
} }
} }
private fun GameController.at(index: Int): Area { private fun GameController.at(index: Int): Area {
return this.field.first { it.id == index } return this.field().first { it.id == index }
} }
private fun GameController.fakeSingleClick(index: Int) { private fun GameController.fakeSingleClick(index: Int) {
runBlocking { runBlocking {
launch { launch {
singleClick(index).collect { singleClick(index).single()
it.second.collect()
}
} }
} }
} }
@ -493,7 +515,7 @@ class GameControllerTest {
private fun GameController.fakeLongPress(index: Int) { private fun GameController.fakeLongPress(index: Int) {
runBlocking { runBlocking {
launch { launch {
longPress(index).collect { it.second.collect() } longPress(index).single()
} }
} }
} }
@ -501,7 +523,7 @@ class GameControllerTest {
private fun GameController.fakeDoubleClick(index: Int) { private fun GameController.fakeDoubleClick(index: Int) {
runBlocking { runBlocking {
launch { launch {
doubleClick(index).collect { it.second.collect() } doubleClick(index).single()
} }
} }
} }

View file

@ -25,7 +25,7 @@ class MinefieldHandlerTest {
fun testOpenArea() { fun testOpenArea() {
handleMinefield { handler, minefield -> handleMinefield { handler, minefield ->
assertTrue(minefield[3].isCovered) assertTrue(minefield[3].isCovered)
handler.openAt(3) handler.openAt(3, false, openNeighbors = false)
assertFalse(minefield[3].isCovered) assertFalse(minefield[3].isCovered)
assertEquals(Mark.None, minefield[3].mark) assertEquals(Mark.None, minefield[3].mark)
} }
@ -35,7 +35,7 @@ class MinefieldHandlerTest {
fun testOpenAreaWithSafeZone() { fun testOpenAreaWithSafeZone() {
handleMinefield(useSafeZone = true) { handler, minefield -> handleMinefield(useSafeZone = true) { handler, minefield ->
assertTrue(minefield[3].isCovered) assertTrue(minefield[3].isCovered)
handler.openAt(3) handler.openAt(3, false, openNeighbors = false)
assertFalse(minefield[3].isCovered) assertFalse(minefield[3].isCovered)
assertEquals(Mark.None, minefield[3].mark) assertEquals(Mark.None, minefield[3].mark)
} }
@ -94,7 +94,7 @@ class MinefieldHandlerTest {
assertEquals(0, minefield.count { it.highlighted }) assertEquals(0, minefield.count { it.highlighted })
// After Open // After Open
handler.openAt(5) handler.openAt(5, false, openNeighbors = false)
val target = minefield.first { it.minesAround != 0 } val target = minefield.first { it.minesAround != 0 }
handler.highlightAt(target.id) handler.highlightAt(target.id)
assertEquals(5, minefield.count { it.highlighted }) assertEquals(5, minefield.count { it.highlighted })
@ -113,7 +113,7 @@ class MinefieldHandlerTest {
@Test @Test
fun testOpenNeighbors() { fun testOpenNeighbors() {
handleMinefield { handler, minefield -> handleMinefield { handler, minefield ->
handler.openAt(5) handler.openAt(5, false, openNeighbors = true)
handler.openOrFlagNeighborsOf(5) handler.openOrFlagNeighborsOf(5)
assertEquals(9, minefield.count { !it.isCovered }) assertEquals(9, minefield.count { !it.isCovered })
} }
@ -122,9 +122,9 @@ class MinefieldHandlerTest {
@Test @Test
fun testOpenNeighborsWithFlags() { fun testOpenNeighborsWithFlags() {
handleMinefield { handler, minefield -> handleMinefield { handler, minefield ->
handler.openAt(5) handler.openAt(5, false, openNeighbors = true)
val neighbors = minefield.filterNeighborsOf(minefield.first { it.id == 5 }) val neighbors = minefield.filterNeighborsOf(minefield.first { it.id == 5 })
neighbors.filter { it.hasMine }.forEach { it.mark = Mark.Flag } neighbors.filter { it.hasMine }.forEach { handler.switchMarkAt(it.id) }
handler.openOrFlagNeighborsOf(5) handler.openOrFlagNeighborsOf(5)
assertEquals(4, minefield.count { !it.isCovered }) assertEquals(4, minefield.count { !it.isCovered })
assertEquals(3, neighbors.count { !it.isCovered }) assertEquals(3, neighbors.count { !it.isCovered })
@ -133,10 +133,15 @@ class MinefieldHandlerTest {
@Test @Test
fun testOpenNeighborsWithQuestionMarks() { fun testOpenNeighborsWithQuestionMarks() {
handleMinefield { handler, minefield -> handleMinefield(useQuestionMark = true) { handler, minefield ->
handler.openAt(5) handler.openAt(5, false, openNeighbors = true)
val neighbors = minefield.filterNeighborsOf(minefield.first { it.id == 5 }) val neighbors = minefield.filterNeighborsOf(minefield.first { it.id == 5 })
neighbors.filter { it.hasMine }.forEach { it.mark = Mark.Question } neighbors
.filter { it.hasMine }
.forEach {
handler.switchMarkAt(it.id)
handler.switchMarkAt(it.id)
}
handler.openOrFlagNeighborsOf(5) handler.openOrFlagNeighborsOf(5)
assertEquals(4, minefield.count { !it.isCovered }) assertEquals(4, minefield.count { !it.isCovered })
assertEquals(3, neighbors.count { !it.isCovered }) assertEquals(3, neighbors.count { !it.isCovered })

View file

@ -25,13 +25,13 @@ class BruteForceSolverTest {
@Test @Test
fun isSolvable() { fun isSolvable() {
handleMinefield { handler, minefield -> handleMinefield { handler, minefield ->
handler.openAt(40) handler.openAt(40, passive = false, openNeighbors = false)
val bruteForceSolver = BruteForceSolver() val bruteForceSolver = BruteForceSolver()
assertTrue(bruteForceSolver.trySolve(minefield.toMutableList())) assertTrue(bruteForceSolver.trySolve(minefield.toMutableList()))
} }
handleMinefield { handler, minefield -> handleMinefield { handler, minefield ->
handler.openAt(0) handler.openAt(0, passive = false, openNeighbors = false)
val bruteForceSolver = BruteForceSolver() val bruteForceSolver = BruteForceSolver()
assertFalse(bruteForceSolver.trySolve(minefield.toMutableList())) assertFalse(bruteForceSolver.trySolve(minefield.toMutableList()))
} }

View file

@ -25,28 +25,28 @@ class LimitedBruteForceSolverTest {
@Test @Test
fun isSolvable() { fun isSolvable() {
handleMinefield { handler, minefield -> handleMinefield { handler, minefield ->
handler.openAt(40) handler.openAt(40, passive = false, openNeighbors = false)
val bruteForceSolver = LimitedBruteForceSolver() val bruteForceSolver = LimitedBruteForceSolver()
assertTrue(bruteForceSolver.trySolve(minefield.toMutableList())) assertTrue(bruteForceSolver.trySolve(minefield.toMutableList()))
} }
handleMinefield { handler, minefield -> handleMinefield { handler, minefield ->
handler.openAt(0) handler.openAt(0, passive = false, openNeighbors = false)
val bruteForceSolver = LimitedBruteForceSolver() val bruteForceSolver = LimitedBruteForceSolver()
assertFalse(bruteForceSolver.trySolve(minefield.toMutableList())) assertFalse(bruteForceSolver.trySolve(minefield.toMutableList()))
} }
} }
@Test @Test
fun shouldntKeepTryingAfterTimout() { fun shouldntKeepTryingAfterTimeout() {
handleMinefield { handler, _ -> handleMinefield { handler, _ ->
handler.openAt(40) handler.openAt(40, passive = false, openNeighbors = false)
val bruteForceSolver = LimitedBruteForceSolver(1000L) val bruteForceSolver = LimitedBruteForceSolver(1000L)
assertTrue(bruteForceSolver.keepTrying()) assertTrue(bruteForceSolver.keepTrying())
} }
handleMinefield { handler, _ -> handleMinefield { handler, _ ->
handler.openAt(0) handler.openAt(0, passive = false, openNeighbors = false)
val bruteForceSolver = LimitedBruteForceSolver(50) val bruteForceSolver = LimitedBruteForceSolver(50)
sleep(100) sleep(100)
assertFalse(bruteForceSolver.keepTrying()) assertFalse(bruteForceSolver.keepTrying())