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.Minefield
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.core.control.ActionResponse
import dev.lucasnlm.antimine.core.control.GameControl
@ -27,17 +26,12 @@ class GameController {
private var saveId = 0
private var firstOpen: FirstOpen = FirstOpen.Unknown
private var gameControl: GameControl = GameControl.Standard
private var mines: Sequence<Area> = emptySequence()
private var useQuestionMark = true
var hasMines = false
private set
val seed: Long
private val minefieldCreator: MinefieldCreator
var field: List<Area>
private set
private var field: List<Area>
constructor(minefield: Minefield, seed: Long, saveId: Int? = null) {
this.minefieldCreator = MinefieldCreator(minefield, Random(seed))
@ -54,12 +48,15 @@ class GameController {
this.saveId = save.uid
this.seed = save.seed
this.firstOpen = save.firstOpen
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 plantMinesExcept(safeId: Int) {
@ -69,24 +66,21 @@ class GameController {
field = minefieldCreator.create(safeId, useSafeZone)
val fieldCopy = field.map { it.copy() }.toMutableList()
val minefieldHandler = MinefieldHandler(fieldCopy, false)
minefieldHandler.openAt(safeId)
minefieldHandler.openAt(safeId, false)
} while (solver.keepTrying() && !solver.trySolve(minefieldHandler.result().toMutableList()))
mines = field.filter { it.hasMine }.asSequence()
firstOpen = FirstOpen.Position(safeId)
hasMines = mines.count() != 0
}
private fun handleAction(target: Area, actionResponse: ActionResponse?) = flow {
val mustPlantMines = !hasMines
private fun handleAction(target: Area, actionResponse: ActionResponse?) {
val mustPlantMines = !hasMines()
val minefieldHandler: MinefieldHandler
if (mustPlantMines) {
plantMinesExcept(target.id)
minefieldHandler = MinefieldHandler(field.toMutableList(), useQuestionMark)
minefieldHandler.openAt(target.id)
emit(StateUpdate.Multiple)
minefieldHandler.openAt(target.id, false)
} else {
minefieldHandler = MinefieldHandler(field.toMutableList(), useQuestionMark)
minefieldHandler.turnOffAllHighlighted()
@ -96,15 +90,15 @@ class GameController {
if (target.mark.isNotNone()) {
minefieldHandler.removeMarkAt(target.id)
} else {
minefieldHandler.openAt(target.id)
minefieldHandler.openAt(target.id, false)
}
}
ActionResponse.SwitchMark -> {
if (!hasMines) {
if (!hasMines()) {
if (target.mark.isNotNone()) {
minefieldHandler.removeMarkAt(target.id)
} else {
minefieldHandler.openAt(target.id)
minefieldHandler.openAt(target.id, false)
}
} else {
minefieldHandler.switchMarkAt(target.id)
@ -119,17 +113,17 @@ class GameController {
minefieldHandler.openOrFlagNeighborsOf(target.id)
}
}
field = minefieldHandler.result()
emit(minefieldHandler.getStateUpdate())
}
field = minefieldHandler.result()
}
fun singleClick(index: Int) = flow {
val target = getArea(index)
val action = if (target.isCovered) gameControl.onCovered.singleClick else gameControl.onOpen.singleClick
action?.let {
emit(action to handleAction(target, action))
handleAction(target, action)
emit(action)
}
}
@ -137,7 +131,8 @@ class GameController {
val target = getArea(index)
val action = if (target.isCovered) gameControl.onCovered.doubleClick else gameControl.onOpen.doubleClick
action?.let {
emit(action to handleAction(target, action))
handleAction(target, action)
emit(action)
}
}
@ -145,46 +140,79 @@ class GameController {
val target = getArea(index)
val action = if (target.isCovered) gameControl.onCovered.longPress else gameControl.onOpen.longPress
action?.let {
emit(action to handleAction(target, action))
handleAction(target, action)
emit(action)
}
}
fun runFlagAssistant(): Flow<Int> {
return FlagAssistant(field.toMutableList()).runFlagAssistant()
fun runFlagAssistant() {
field = FlagAssistant(field.toMutableList()).run {
runFlagAssistant()
result()
}
}
fun getScore() = Score(
mines.count { !it.mistake && it.mark.isFlag() },
mines.count(),
mines().count { !it.mistake && it.mark.isFlag() },
getMinesCount(),
field.count()
)
fun getMinesCount() = mines.count()
fun getMinesCount() = mines().count()
fun showAllMines() =
mines.filter { it.mark != Mark.Flag }.forEach { it.isCovered = false }
fun showAllMines() {
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> =
mines.filter { it.isCovered && it.mark.isNone() }.sortedBy {
fun takeExplosionRadius(target: Area): List<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 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 hasIsolatedAllMines() =
mines.map {
mines().map {
val neighbors = field.filterNeighborsOf(it)
val neighborsCount = neighbors.count()
val isolatedNeighborsCount = neighbors.count { neighbor ->
@ -193,17 +221,17 @@ class GameController {
neighborsCount != isolatedNeighborsCount
}.count { it } == 0
private fun rightFlags() = mines.count { it.mark.isFlag() }
private fun rightFlags() = mines().count { it.mark.isFlag() }
fun checkVictory(): Boolean =
hasMines && hasIsolatedAllMines() && !hasAnyMineExploded()
hasMines() && hasIsolatedAllMines() && !hasAnyMineExploded()
fun isGameOver(): Boolean =
checkVictory() || hasAnyMineExploded()
fun remainingMines(): Int {
val flagsCount = field.count { it.mark.isFlag() }
val minesCount = mines.count()
val minesCount = mines().count()
return (minesCount - flagsCount).coerceAtLeast(0)
}
@ -238,11 +266,11 @@ class GameController {
Stats(
0,
duration,
mines.count(),
getMinesCount(),
if (gameStatus == SaveStatus.VICTORY) 1 else 0,
minefield.width,
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.Mark
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
class FlagAssistant(
private val field: MutableList<Area>
) {
fun runFlagAssistant() = flow {
fun runFlagAssistant() {
// Must not select Mark.PurposefulNone, only Mark.None. Otherwise, it will flag
// a square that was previously unflagged by player.
val flaggedIds = field
field
.filter { it.hasMine && it.mark.isPureNone() }
.mapNotNull(::putFlagIfIsolated)
.asFlow()
emitAll(flaggedIds)
.forEach(::putFlagIfIsolated)
}
private fun putFlagIfIsolated(it: Area): Int? {
fun result(): List<Area> = field.toList()
private fun putFlagIfIsolated(it: Area) {
val neighbors = field.filterNeighborsOf(it)
val neighborsCount = neighbors.count()
val revealedNeighborsCount = neighbors.count { neighbor ->
!neighbor.isCovered || (neighbor.hasMine && neighbor.mark.isFlag())
}
return if (revealedNeighborsCount == neighborsCount) {
it.mark = Mark.Flag
it.id
} else {
null
if (revealedNeighborsCount == neighborsCount) {
field[it.id] = it.copy(mark = Mark.Flag)
}
}
}

View file

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

View file

@ -2,77 +2,79 @@ package dev.lucasnlm.antimine.common.level.logic
import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Mark
import dev.lucasnlm.antimine.common.level.models.StateUpdate
class MinefieldHandler(
private val field: MutableList<Area>,
private val useQuestionMark: Boolean
) {
private var changedIndex: Int? = null
private var changes = 0
fun showAllMines() {
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() {
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) {
field.getOrNull(index)?.let {
changes++
changedIndex = index
it.mark = Mark.PurposefulNone
field[it.id] = it.copy(mark = Mark.PurposefulNone)
}
}
fun switchMarkAt(index: Int) {
field.getOrNull(index)?.run {
if (isCovered) {
changes++
changedIndex = index
mark = when (mark) {
field.getOrNull(index)?.let {
if (it.isCovered) {
field[index] = it.copy(mark = when (it.mark) {
Mark.PurposefulNone, Mark.None -> Mark.Flag
Mark.Flag -> if (useQuestionMark) Mark.Question else Mark.None
Mark.Question -> Mark.None
}
})
}
}
}
fun openAt(index: Int) {
fun openAt(index: Int, passive: Boolean, openNeighbors: Boolean = true) {
field.getOrNull(index)?.run {
if (isCovered) {
changedIndex = index
changes++
isCovered = false
mark = Mark.None
field[index] = copy(
isCovered = false,
mark = Mark.None,
mistake = (!passive && hasMine) || (!hasMine && mark.isFlag())
)
if (hasMine) {
mistake = true
} else if (minesAround == 0) {
changes +=
field.filterNeighborsOf(this)
.filter { it.isCovered }
.onEach {
openAt(it.id)
}.count()
if (!hasMine && minesAround == 0 && openNeighbors) {
field.filterNeighborsOf(this)
.filter { it.isCovered }
.onEach {
openAt(it.id, openNeighbors = true, passive = true)
}.count()
}
}
}
}
fun highlightAt(index: Int) {
field.getOrNull(index)?.run {
when {
minesAround != 0 -> {
changes++
changedIndex = index
highlighted = !highlighted
changes += field.filterNeighborsOf(this)
.filter { it.mark.isNone() && it.isCovered }
.onEach { it.highlighted = !it.highlighted }
.count()
field.getOrNull(index)?.let {
field[index] = it.copy(highlighted = it.minesAround != 0 && !it.highlighted)
}.also {
field.filterNeighborsOf(field[index])
.filter { it.mark.isNone() && it.isCovered }
.onEach { neighbor ->
field[neighbor.id] = neighbor.copy(highlighted = true)
}
else -> 0
}
}
}
@ -82,16 +84,14 @@ class MinefieldHandler(
val neighbors = field.filterNeighborsOf(this)
val flaggedCount = neighbors.count { it.mark.isFlag() }
if (flaggedCount >= minesAround) {
changes++
changes += neighbors
neighbors
.filter { it.isCovered && it.mark.isNone() }
.onEach { openAt(it.id) }
.onEach { openAt(it.id, passive = false, openNeighbors = true) }
.count()
} else {
val coveredNeighbors = neighbors.filter { it.isCovered }
if (coveredNeighbors.count() == minesAround) {
changes++
changes += coveredNeighbors.filter {
coveredNeighbors.filter {
it.mark.isNone()
}.onEach {
switchMarkAt(it.id)
@ -103,18 +103,4 @@ class MinefieldHandler(
}
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 posX: Int,
val posY: Int,
var minesAround: Int = 0,
var hasMine: Boolean = false,
var mistake: Boolean = false,
var isCovered: Boolean = true,
var mark: Mark = Mark.None,
var highlighted: Boolean = false
val minesAround: Int = 0,
val hasMine: Boolean = false,
val mistake: Boolean = false,
val isCovered: Boolean = true,
val mark: Mark = Mark.None,
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.database.models.FirstOpen
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.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event
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.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
@ -30,7 +30,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -78,7 +78,7 @@ class GameViewModel @ViewModelInject constructor(
mineCount.postValue(minefield.mines)
difficulty.postValue(newDifficulty)
levelSetup.postValue(minefield)
refreshAll()
refreshField()
eventObserver.postValue(Event.StartNewGame)
@ -105,7 +105,7 @@ class GameViewModel @ViewModelInject constructor(
mineCount.postValue(setup.mines)
difficulty.postValue(save.difficulty)
levelSetup.postValue(setup)
refreshAll()
refreshField()
refreshMineCount()
when {
@ -132,7 +132,7 @@ class GameViewModel @ViewModelInject constructor(
mineCount.postValue(setup.mines)
difficulty.postValue(save.difficulty)
levelSetup.postValue(setup)
refreshAll()
refreshField()
eventObserver.postValue(Event.StartNewGame)
@ -172,9 +172,10 @@ class GameViewModel @ViewModelInject constructor(
withContext(Dispatchers.Main) {
if (save.firstOpen is FirstOpen.Position) {
gameController.singleClick(save.firstOpen.value).flatMapConcat { it.second }.collect {
refreshAll()
}
gameController
.singleClick(save.firstOpen.value)
.filterNotNull()
.collect { refreshField() }
}
}
@ -200,7 +201,7 @@ class GameViewModel @ViewModelInject constructor(
fun pauseGame() {
if (initialized) {
if (gameController.hasMines) {
if (gameController.hasMines()) {
eventObserver.postValue(Event.Pause)
}
clock.stop()
@ -208,7 +209,7 @@ class GameViewModel @ViewModelInject constructor(
}
suspend fun saveGame() {
if (gameController.hasMines) {
if (gameController.hasMines()) {
val id = savesRepository.saveGame(
gameController.getSaveState(elapsedTimeSeconds.value ?: 0L, currentDifficulty)
)
@ -218,7 +219,7 @@ class GameViewModel @ViewModelInject constructor(
}
private suspend fun saveStats() {
if (initialized && gameController.hasMines) {
if (initialized && gameController.hasMines()) {
gameController.getStats(elapsedTimeSeconds.value ?: 0L)?.let {
statsRepository.addStats(it)
}
@ -226,70 +227,57 @@ class GameViewModel @ViewModelInject constructor(
}
fun resumeGame() {
if (initialized && gameController.hasMines && !gameController.isGameOver()) {
if (initialized && gameController.hasMines() && !gameController.isGameOver()) {
eventObserver.postValue(Event.Resume)
runClock()
}
}
suspend fun onLongClick(index: Int) {
gameController.longPress(index).flatMapConcat { (action, flow) ->
onFeedbackAnalytics(action, index)
flow
}.collect {
if (it is StateUpdate.Multiple) {
refreshAll()
} else if (it is StateUpdate.Single) {
refreshIndex(it.index, false)
}
}.also {
onPostAction()
gameController
.longPress(index)
.filterNotNull()
.collect { action ->
onFeedbackAnalytics(action, index)
refreshField()
onPostAction()
if (preferencesRepository.useHapticFeedback()) {
hapticFeedbackManager.longPressFeedback()
if (preferencesRepository.useHapticFeedback()) {
hapticFeedbackManager.longPressFeedback()
}
}
}
}
suspend fun onDoubleClick(index: Int) {
gameController.doubleClick(index).flatMapConcat { (action, flow) ->
onFeedbackAnalytics(action, index)
flow
}.collect {
if (it is StateUpdate.Multiple) {
refreshAll()
} else if (it is StateUpdate.Single) {
refreshIndex(it.index, false)
}
}.also {
onPostAction()
gameController
.doubleClick(index)
.filterNotNull()
.collect { action ->
onFeedbackAnalytics(action, index)
refreshField()
onPostAction()
if (preferencesRepository.useHapticFeedback()) {
hapticFeedbackManager.longPressFeedback()
if (preferencesRepository.useHapticFeedback()) {
hapticFeedbackManager.longPressFeedback()
}
}
}
}
suspend fun onSingleClick(index: Int) {
gameController.singleClick(index).flatMapConcat { (action, flow) ->
onFeedbackAnalytics(action, index)
flow
}.collect {
if (it is StateUpdate.Multiple) {
refreshAll()
} else if (it is StateUpdate.Single) {
refreshIndex(it.index, false)
gameController
.singleClick(index)
.filterNotNull()
.collect { action ->
onFeedbackAnalytics(action, index)
refreshField()
onPostAction()
}
}.also {
onPostAction()
}
}
private suspend fun onPostAction() {
private fun onPostAction() {
if (preferencesRepository.useFlagAssistant() && !gameController.hasAnyMineExploded()) {
gameController.runFlagAssistant().collect {
refreshIndex(it)
}
gameController.runFlagAssistant()
refreshField()
}
updateGameState()
@ -324,12 +312,12 @@ class GameViewModel @ViewModelInject constructor(
}
}
if (gameController.hasMines) {
if (gameController.hasMines()) {
refreshMineCount()
}
if (gameController.checkVictory()) {
refreshAll()
refreshField()
eventObserver.postValue(Event.Victory)
}
}
@ -378,16 +366,18 @@ class GameViewModel @ViewModelInject constructor(
}
}
showWrongFlags()
refreshField()
findExplodedMine()?.let { exploded ->
takeExplosionRadius(exploded).forEach {
it.isCovered = false
refreshIndex(it.id)
revealArea(it.id)
refreshField()
delay(delayMillis)
}
}
showWrongFlags()
refreshAll()
refreshField()
updateGameState()
}
@ -412,6 +402,7 @@ class GameViewModel @ViewModelInject constructor(
)
flagAllMines()
showWrongFlags()
refreshField()
}
if (currentDifficulty == Difficulty.Standard) {
@ -428,15 +419,7 @@ class GameViewModel @ViewModelInject constructor(
private fun getAreaSizeMultiplier() = preferencesRepository.areaSizeMultiplier()
private fun refreshIndex(targetIndex: Int, multipleChanges: Boolean = false) {
if (!preferencesRepository.useAnimations() || multipleChanges) {
field.postValue(gameController.field)
} else {
fieldRefresh.postValue(targetIndex)
}
}
private fun refreshAll() {
field.postValue(gameController.field)
private fun refreshField() {
field.postValue(gameController.field())
}
}

View file

@ -1,7 +1,6 @@
package dev.lucasnlm.antimine.common.level.logic
import dev.lucasnlm.antimine.common.level.models.Minefield
import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Test
@ -12,14 +11,21 @@ class FlagAssistantTest {
fun testRunAssistant() = runBlockingTest {
repeat(20) { takeMines ->
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 }
.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>()
FlagAssistant(map.toMutableList()).runFlagAssistant().toCollection(actual)
val actual = FlagAssistant(map.toMutableList()).run {
runFlagAssistant()
result()
}
val expected = map
.filter { it.hasMine }
@ -38,14 +44,21 @@ class FlagAssistantTest {
repeat(20) { takeMines ->
val seed = 10 * takeMines
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 }
.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>()
FlagAssistant(map.toMutableList()).runFlagAssistant().toCollection(actual)
val actual = FlagAssistant(map.toMutableList()).run {
runFlagAssistant()
result()
}
val expected = map
.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.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.Score
import dev.lucasnlm.antimine.core.control.ControlStyle
import dev.lucasnlm.antimine.core.control.GameControl
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
@ -42,9 +42,17 @@ class GameControllerTest {
withGameController { controller ->
assertEquals(Score(0, 20, 100), controller.getScore())
repeat(20) { right ->
controller.field.filter { it.hasMine }.take(right).forEach { it.mark = Mark.Flag }
assertEquals(Score(right, 20, 100), controller.getScore())
repeat(20) { markedMines ->
controller
.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 {
withGameController { controller ->
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())
}
}
@ -62,9 +79,12 @@ class GameControllerTest {
@Test
fun testFlagAllMines() = runBlockingTest {
withGameController { controller ->
val minesCount = controller.mines().count()
controller.flagAllMines()
val actual = controller.field.filter { it.hasMine }.count { it.isCovered && it.mark.isFlag() }
assertEquals(20, actual)
val actualFlaggedMines = controller.mines().count { it.isCovered && it.mark.isFlag() }
assertEquals(minesCount, actualFlaggedMines)
}
}
@ -72,30 +92,29 @@ class GameControllerTest {
fun testFindExplodedMine() = runBlockingTest {
withGameController { controller ->
assertNull(controller.findExplodedMine())
val target = controller.field.first { it.hasMine }
launch {
controller.singleClick(target.id).collect { it.second.collect() }
}
assertEquals(target.id, controller.findExplodedMine()?.id ?: - 1)
val target = controller.mines().first()
controller.fakeSingleClick(target.id)
assertNotNull(controller.findExplodedMine())
assertEquals(target.id, controller.findExplodedMine()!!.id)
}
}
@Test
fun testTakeExplosionRadius() = runBlockingTest {
withGameController { controller ->
val lastMine = controller.field.last { it.hasMine }
val lastMine = controller.mines().last()
assertEquals(
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()
)
val firstMine = controller.field.first { it.hasMine }
val firstMine = controller.mines().first()
assertEquals(
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()
)
val midMine = controller.field.filter { it.hasMine }.take(controller.getMinesCount() / 2).last()
val midMine = controller.mines().take(controller.getMinesCount() / 2).last()
assertEquals(
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()
@ -107,10 +126,10 @@ class GameControllerTest {
fun testShowAllMines() = runBlockingTest {
withGameController { controller ->
controller.showAllMines()
controller.field.filter { it.hasMine && it.mistake }.forEach {
controller.mines().filter { it.mistake }.forEach {
assertEquals(it.isCovered, false)
}
controller.field.filter { it.hasMine && it.mark.isFlag() }.forEach {
controller.mines().filter { it.mark.isFlag() }.forEach {
assertEquals(it.isCovered, true)
}
}
@ -119,25 +138,25 @@ class GameControllerTest {
@Test
fun testShowWrongFlags() = runBlockingTest {
withGameController { controller ->
val wrongFlag = controller.field.first { !it.hasMine }.apply {
mark = Mark.Flag
}
val rightFlag = controller.field.first { it.hasMine }.apply {
mark = Mark.Flag
}
controller.field().first { !it.hasMine }
.also { controller.fakeLongPress(it.id) }
val wrongFlag = controller.field().first { !it.hasMine }
//val rightFlag = controller.mines().first().apply { controller.fakeLongPress(id) }
controller.showWrongFlags()
assertTrue(wrongFlag.mistake)
assertFalse(rightFlag.mistake)
//assertFalse(rightFlag.mistake)
}
}
@Test
fun testRevealAllEmptyAreas() = runBlockingTest {
withGameController { controller ->
val covered = controller.field.filter { it.isCovered }
val covered = controller.field().filter { it.isCovered }
assertTrue(covered.isNotEmpty())
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 {
withGameController { controller ->
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())
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())
}
}
@ -158,7 +187,7 @@ class GameControllerTest {
assertEquals(20, controller.remainingMines())
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())
}
}
@ -170,9 +199,9 @@ class GameControllerTest {
assertFalse(controller.hasIsolatedAllMines())
assertFalse(controller.isGameOver())
controller.field.filter { !it.hasMine }.forEach {
it.isCovered = false
}
controller.field()
.filter { !it.hasMine }
.forEach { controller.fakeSingleClick(it.id) }
assertTrue(controller.hasIsolatedAllMines())
assertTrue(controller.isGameOver())
@ -184,10 +213,7 @@ class GameControllerTest {
withGameController { controller ->
assertFalse(controller.hasAnyMineExploded())
controller.field.first { it.hasMine }.also {
it.isCovered = false
it.mistake = true
}
controller.field().first { it.hasMine }.also { controller.fakeSingleClick(it.id) }
assertTrue(controller.hasAnyMineExploded())
}
@ -198,10 +224,7 @@ class GameControllerTest {
withGameController { controller ->
assertFalse(controller.isGameOver())
controller.field.first { it.hasMine }.also {
it.isCovered = false
it.mistake = true
}
controller.field().first { it.hasMine }.also { controller.fakeSingleClick(it.id) }
assertTrue(controller.isGameOver())
}
@ -212,13 +235,15 @@ class GameControllerTest {
withGameController { controller ->
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())
controller.field.filterNot { it.hasMine }.forEach { it.isCovered = false }
controller.field().filterNot { it.hasMine }.forEach { controller.fakeSingleClick(it.id) }
assertTrue(controller.checkVictory())
controller.field.first { it.hasMine }.mistake = true
controller.field().first { it.hasMine }.also { controller.fakeSingleClick(it.id) }
assertFalse(controller.checkVictory())
}
}
@ -275,17 +300,17 @@ class GameControllerTest {
updateGameControl(GameControl.fromControlType(ControlStyle.Standard))
fakeSingleClick(14)
assertFalse(at(14).isCovered)
field.filterNeighborsOf(at(14)).forEach {
field().filterNeighborsOf(at(14)).forEach {
assertTrue(it.isCovered)
}
field.filter { it.hasMine }.forEach {
field().filter { it.hasMine }.forEach {
fakeLongPress(it.id)
assertTrue(it.mark.isFlag())
}
fakeLongPress(14)
field.filterNeighborsOf(at(14)).forEach {
field().filterNeighborsOf(at(14)).forEach {
if (it.hasMine) {
assertTrue(it.isCovered)
} else {
@ -348,17 +373,17 @@ class GameControllerTest {
updateGameControl(GameControl.fromControlType(ControlStyle.FastFlag))
fakeLongPress(14)
assertFalse(at(14).isCovered)
field.filterNeighborsOf(at(14)).forEach {
field().filterNeighborsOf(at(14)).forEach {
assertTrue(it.isCovered)
}
field.filter { it.hasMine }.forEach {
field().filter { it.hasMine }.forEach {
fakeSingleClick(it.id)
assertTrue(it.mark.isFlag())
}
fakeSingleClick(14)
field.filterNeighborsOf(at(14)).forEach {
field().filterNeighborsOf(at(14)).forEach {
if (it.hasMine) {
assertTrue(it.isCovered)
} else {
@ -427,17 +452,16 @@ class GameControllerTest {
updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClick))
fakeDoubleClick(14)
assertFalse(at(14).isCovered)
field.filterNeighborsOf(at(14)).forEach {
field().filterNeighborsOf(at(14)).forEach {
assertTrue(it.isCovered)
}
field.filter { it.hasMine }.forEach {
fakeSingleClick(it.id)
assertTrue(it.mark.isFlag())
}
field().filter { it.hasMine }
.onEach { fakeSingleClick(it.id) }
.onEach { assertTrue(it.mark.isFlag()) }
fakeDoubleClick(14)
field.filterNeighborsOf(at(14)).forEach {
field().filterNeighborsOf(at(14)).forEach {
if (it.hasMine) {
assertTrue(it.isCovered)
} else {
@ -453,11 +477,11 @@ class GameControllerTest {
withGameController(clickOnCreate = false) { controller ->
controller.run {
updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClick))
assertFalse(hasMines)
assertEquals(0, field.filterNot { it.isCovered }.count())
assertFalse(hasMines())
assertEquals(0, field().filterNot { it.isCovered }.count())
fakeSingleClick(40)
assertTrue(hasMines)
field.filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) }
assertTrue(hasMines())
field().filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) }
}
}
}
@ -467,25 +491,23 @@ class GameControllerTest {
withGameController(clickOnCreate = false) { controller ->
controller.run {
updateGameControl(GameControl.fromControlType(ControlStyle.FastFlag))
assertFalse(hasMines)
assertEquals(0, field.filterNot { it.isCovered }.count())
assertFalse(hasMines())
assertEquals(0, field().filterNot { it.isCovered }.count())
fakeSingleClick(40)
assertTrue(hasMines)
field.filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) }
assertTrue(hasMines())
field().filterNeighborsOf(at(40)).forEach { assertFalse(it.isCovered) }
}
}
}
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) {
runBlocking {
launch {
singleClick(index).collect {
it.second.collect()
}
singleClick(index).single()
}
}
}
@ -493,7 +515,7 @@ class GameControllerTest {
private fun GameController.fakeLongPress(index: Int) {
runBlocking {
launch {
longPress(index).collect { it.second.collect() }
longPress(index).single()
}
}
}
@ -501,7 +523,7 @@ class GameControllerTest {
private fun GameController.fakeDoubleClick(index: Int) {
runBlocking {
launch {
doubleClick(index).collect { it.second.collect() }
doubleClick(index).single()
}
}
}

View file

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

View file

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

View file

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