Add roboletric and improve testing

This commit is contained in:
Lucas Lima 2020-04-07 19:25:55 -03:00
parent eb92414df1
commit 0de579010c
No known key found for this signature in database
GPG key ID: C828A958035D9C34
23 changed files with 470 additions and 84 deletions

View file

@ -30,9 +30,25 @@ android {
} }
} }
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
kapt { kapt {
generateStubs true generateStubs true
} }
testOptions {
unitTests {
includeAndroidResources true
animationsDisabled true
}
}
} }
dependencies { dependencies {
@ -59,7 +75,20 @@ dependencies {
kapt "com.google.dagger:dagger-android-processor:$versions.dagger" kapt "com.google.dagger:dagger-android-processor:$versions.dagger"
kapt "com.google.dagger:dagger-compiler:$versions.dagger" kapt "com.google.dagger:dagger-compiler:$versions.dagger"
testImplementation "com.google.dagger:dagger-android:$versions.dagger"
testImplementation "com.google.dagger:dagger-android-support:$versions.dagger"
// Kotlin // Kotlin
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.coroutines"
// Tests
testImplementation "junit:junit:$versions.junit"
testImplementation "androidx.test:core:$versions.testCore"
testImplementation "androidx.test:core-ktx:$versions.testCore"
testImplementation "androidx.test.espresso:espresso-core:$versions.espresso"
testImplementation "androidx.test.espresso:espresso-contrib:$versions.espresso"
testImplementation "androidx.fragment:fragment-testing:$versions.fragmentTest"
testImplementation "org.robolectric:robolectric:$versions.robolectric"
} }

View file

@ -1,5 +1,6 @@
package dev.lucasnlm.antimine package dev.lucasnlm.antimine
import android.app.Application
import android.content.Context import android.content.Context
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
@ -11,16 +12,20 @@ import dev.lucasnlm.antimine.di.AppModule
import dev.lucasnlm.antimine.di.DaggerAppComponent import dev.lucasnlm.antimine.di.DaggerAppComponent
import javax.inject.Inject import javax.inject.Inject
class MainApplication : DaggerApplication() { open class MainApplication : DaggerApplication() {
@Inject @Inject
lateinit var analyticsManager: AnalyticsManager lateinit var analyticsManager: AnalyticsManager
protected open fun appModule(application: Application) = AppModule(application)
protected open fun levelModule(application: Application) = LevelModule(application)
override fun applicationInjector(): AndroidInjector<out DaggerApplication> = override fun applicationInjector(): AndroidInjector<out DaggerApplication> =
DaggerAppComponent.builder() DaggerAppComponent.builder()
.application(this) .application(this)
.appModule(AppModule(this)) .appModule(appModule(this))
.levelModule(LevelModule(this)) .levelModule(levelModule(this))
.build() .build()
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {

View file

@ -47,7 +47,7 @@ class EndGameDialogFragment : DaggerAppCompatDialogFragment() {
} }
arguments?.run { arguments?.run {
isVictory = getBoolean(DIALOG_STATE) == true isVictory = getBoolean(DIALOG_IS_VICTORY) == true
time = getLong(DIALOG_TIME) time = getLong(DIALOG_TIME)
rightMines = getInt(DIALOG_RIGHT_MINES) rightMines = getInt(DIALOG_RIGHT_MINES)
totalMines = getInt(DIALOG_TOTAL_MINES) totalMines = getInt(DIALOG_TOTAL_MINES)
@ -134,14 +134,14 @@ class EndGameDialogFragment : DaggerAppCompatDialogFragment() {
fun newInstance(victory: Boolean, rightMines: Int, totalMines: Int, time: Long): EndGameDialogFragment = fun newInstance(victory: Boolean, rightMines: Int, totalMines: Int, time: Long): EndGameDialogFragment =
EndGameDialogFragment().apply { EndGameDialogFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putBoolean(DIALOG_STATE, victory) putBoolean(DIALOG_IS_VICTORY, victory)
putInt(DIALOG_RIGHT_MINES, rightMines) putInt(DIALOG_RIGHT_MINES, rightMines)
putInt(DIALOG_TOTAL_MINES, totalMines) putInt(DIALOG_TOTAL_MINES, totalMines)
putLong(DIALOG_TIME, time) putLong(DIALOG_TIME, time)
} }
} }
private const val DIALOG_STATE = "dialog_state" const val DIALOG_IS_VICTORY = "dialog_state"
private const val DIALOG_TIME = "dialog_time" private const val DIALOG_TIME = "dialog_time"
private const val DIALOG_RIGHT_MINES = "dialog_right_mines" private const val DIALOG_RIGHT_MINES = "dialog_right_mines"
private const val DIALOG_TOTAL_MINES = "dialog_total_mines" private const val DIALOG_TOTAL_MINES = "dialog_total_mines"

View file

@ -17,18 +17,23 @@ class EngGameDialogViewModel : ViewModel() {
} }
} }
private fun List<String>.safeRandomEmoji(except: String? = null, fallback: String = "\uD83D\uDCA3") =
this.filter { it != except && checkGlyphAvailability(it) }
.ifEmpty { listOf(fallback) }
.random()
fun randomVictoryEmoji(except: String? = null) = listOf( fun randomVictoryEmoji(except: String? = null) = listOf(
"\uD83D\uDE00", "\uD83D\uDE0E", "\uD83D\uDE1D", "\uD83E\uDD73", "\uD83D\uDE06" "\uD83D\uDE00", "\uD83D\uDE0E", "\uD83D\uDE1D", "\uD83E\uDD73", "\uD83D\uDE06"
).filter { it != except && checkGlyphAvailability(it) }.random() ).safeRandomEmoji()
fun randomNeutralEmoji(except: String? = null) = listOf( fun randomNeutralEmoji(except: String? = null) = listOf(
"\uD83D\uDE01", "\uD83E\uDD14", "\uD83D\uDE42", "\uD83D\uDE09" "\uD83D\uDE01", "\uD83E\uDD14", "\uD83D\uDE42", "\uD83D\uDE09"
).filter { it != except && checkGlyphAvailability(it) }.random() ).safeRandomEmoji()
fun randomGameOverEmoji(except: String? = null) = listOf( fun randomGameOverEmoji(except: String? = null) = listOf(
"\uD83D\uDE10", "\uD83D\uDE44", "\uD83D\uDE25", "\uD83D\uDE13", "\uD83D\uDE31", "\uD83D\uDE10", "\uD83D\uDE44", "\uD83D\uDE25", "\uD83D\uDE13", "\uD83D\uDE31",
"\uD83E\uDD2C", "\uD83E\uDD15", "\uD83D\uDE16", "\uD83D\uDCA3", "\uD83D\uDE05" "\uD83E\uDD2C", "\uD83E\uDD15", "\uD83D\uDE16", "\uD83D\uDCA3", "\uD83D\uDE05"
).filter { it != except && checkGlyphAvailability(it) }.random() ).safeRandomEmoji()
fun messageTo(context: Context, rightMines: Int, totalMines: Int, time: Long, isVictory: Boolean): String = fun messageTo(context: Context, rightMines: Int, totalMines: Int, time: Long, isVictory: Boolean): String =
if (totalMines != 0 && time != 0L) { if (totalMines != 0 && time != 0L) {

View file

@ -31,6 +31,7 @@
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
android:textAlignment="center" android:textAlignment="center"
android:gravity="center_horizontal"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_emoji" app:layout_constraintTop_toBottomOf="@+id/title_emoji"
@ -45,6 +46,7 @@
android:textColor="@color/text_color" android:textColor="@color/text_color"
android:textSize="14sp" android:textSize="14sp"
android:textAlignment="center" android:textAlignment="center"
android:gravity="center_horizontal"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" app:layout_constraintTop_toBottomOf="@+id/title"

View file

@ -0,0 +1,12 @@
package dev.lucasnlm.antimine
import android.app.Application
import dev.lucasnlm.antimine.di.AppModule
import dev.lucasnlm.antimine.di.TestLevelModule
class TestApplication : MainApplication() {
override fun appModule(application: Application) = AppModule(application)
override fun levelModule(application: Application) = TestLevelModule(application)
}

View file

@ -0,0 +1,73 @@
package dev.lucasnlm.antimine.di
import android.app.Application
import android.content.Context
import androidx.lifecycle.MutableLiveData
import dagger.Module
import dagger.Provides
import dev.lucasnlm.antimine.common.level.di.LevelModule
import dev.lucasnlm.antimine.common.level.models.Event
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.mocks.MockDimensionRepository
import dev.lucasnlm.antimine.mocks.MockHapticFeedbackInteractor
import dev.lucasnlm.antimine.mocks.MockMinefieldRepository
import dev.lucasnlm.antimine.mocks.MockSavesRepository
@Module
class TestLevelModule(
application: Application
) : LevelModule(application) {
@Provides
override fun provideGameEventObserver(): MutableLiveData<Event> = MutableLiveData()
@Provides
override fun provideClock(): Clock = Clock()
@Provides
override fun provideGameViewModelFactory(
application: Application,
eventObserver: MutableLiveData<Event>,
savesRepository: ISavesRepository,
dimensionRepository: IDimensionRepository,
preferencesRepository: IPreferencesRepository,
hapticFeedbackInteractor: IHapticFeedbackInteractor,
minefieldRepository: IMinefieldRepository,
analyticsManager: AnalyticsManager,
clock: Clock
) = GameViewModelFactory(
application,
eventObserver,
savesRepository,
dimensionRepository,
preferencesRepository,
hapticFeedbackInteractor,
minefieldRepository,
analyticsManager,
clock
)
@Provides
override fun provideDimensionRepository(
context: Context,
preferencesRepository: IPreferencesRepository
): IDimensionRepository = MockDimensionRepository()
@Provides
override fun provideSavesRepository(): ISavesRepository = MockSavesRepository()
@Provides
override fun provideMinefieldRepository(): IMinefieldRepository = MockMinefieldRepository()
@Provides
override fun provideHapticFeedbackInteractor(
application: Application,
preferencesRepository: IPreferencesRepository
): IHapticFeedbackInteractor = MockHapticFeedbackInteractor()
}

View file

@ -0,0 +1,75 @@
package dev.lucasnlm.antimine.level.view
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.launchActivity
import dev.lucasnlm.antimine.GameActivity
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.TestApplication
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper
import java.util.concurrent.TimeUnit
@RunWith(RobolectricTestRunner::class)
@Config(minSdk = 16, maxSdk = 27, application = TestApplication::class)
@LooperMode(LooperMode.Mode.PAUSED)
class LevelFragmentTest {
@Test
fun testShowGameOverWhenTapAMine() {
launchActivity<GameActivity>().onActivity { activity ->
ShadowLooper.runUiThreadTasks()
// First tap
activity.findViewById<RecyclerView>(R.id.recyclerGrid)
.findViewHolderForItemId(40).itemView.performClick()
ShadowLooper.runUiThreadTasks()
// Tap on a mine
activity.findViewById<RecyclerView>(R.id.recyclerGrid)
.findViewHolderForItemId(26).itemView.performClick()
ShadowLooper.idleMainLooper(2, TimeUnit.SECONDS)
ShadowLooper.runUiThreadTasks()
val endGame = activity.supportFragmentManager.findFragmentByTag(EndGameDialogFragment.TAG)
assertNotNull(endGame)
assertEquals(endGame?.arguments?.get(EndGameDialogFragment.DIALOG_IS_VICTORY), false)
}
}
@Test
fun testShowVictoryWhenTapAllSafeAreas() {
val mines = sequenceOf(4, 9, 15, 26, 47, 53, 68, 71, 75)
val safeAreas = (0 until 81).filterNot { mines.contains(it) }.map { it.toLong() }
launchActivity<GameActivity>().onActivity { activity ->
ShadowLooper.runUiThreadTasks()
// First tap
activity.findViewById<RecyclerView>(R.id.recyclerGrid)
.findViewHolderForItemId(40).itemView.performClick()
ShadowLooper.runUiThreadTasks()
// Tap on safe places
safeAreas.forEach { safeArea ->
activity.findViewById<RecyclerView>(R.id.recyclerGrid)
.findViewHolderForItemId(safeArea).itemView.performClick()
ShadowLooper.runUiThreadTasks()
}
ShadowLooper.idleMainLooper(2, TimeUnit.SECONDS)
ShadowLooper.runUiThreadTasks()
val endGame = activity.supportFragmentManager.findFragmentByTag(EndGameDialogFragment.TAG)
assertNotNull(endGame)
assertEquals(endGame?.arguments?.get(EndGameDialogFragment.DIALOG_IS_VICTORY), true)
}
}
}

View file

@ -0,0 +1,12 @@
package dev.lucasnlm.antimine.mocks
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.Size
class MockDimensionRepository : IDimensionRepository {
override fun areaSize(): Float = 50.0f
override fun displaySize(): Size = Size(50 * 20, 50 * 30)
override fun actionBarSize(): Int = 50
}

View file

@ -0,0 +1,13 @@
package dev.lucasnlm.antimine.mocks
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
class MockHapticFeedbackInteractor : IHapticFeedbackInteractor {
override fun toggleFlagFeedback() {
// Empty
}
override fun explosionFeedback() {
// Empty
}
}

View file

@ -0,0 +1,17 @@
package dev.lucasnlm.antimine.mocks
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
class MockMinefieldRepository : IMinefieldRepository {
override fun fromDifficulty(
difficulty: Difficulty,
dimensionRepository: IDimensionRepository,
preferencesRepository: IPreferencesRepository
): Minefield = Minefield(9, 9, 9)
override fun randomSeed(): Long = 0L
}

View file

@ -0,0 +1,28 @@
package dev.lucasnlm.antimine.mocks
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
class MockPreferencesRepository : IPreferencesRepository {
override fun customGameMode(): Minefield = Minefield(9, 9, 9)
override fun updateCustomGameMode(minefield: Minefield) { }
override fun getBoolean(key: String, defaultValue: Boolean): Boolean = false
override fun getInt(key: String, defaultValue: Int): Int = 0
override fun putBoolean(key: String, value: Boolean) { }
override fun putInt(key: String, value: Int) { }
override fun useFlagAssistant(): Boolean = true
override fun useHapticFeedback(): Boolean = false
override fun useLargeAreas(): Boolean = false
override fun useAnimations(): Boolean = false
override fun useDoubleClickToOpen(): Boolean = false
}

View file

@ -0,0 +1,18 @@
package dev.lucasnlm.antimine.mocks
import dev.lucasnlm.antimine.common.level.database.models.Save
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
class MockSavesRepository : ISavesRepository {
override suspend fun fetchCurrentSave(): Save? {
return null
}
override suspend fun saveGame(save: Save): Long? {
return 1
}
override fun setLimit(maxSavesStorage: Int) {
// Empty
}
}

View file

@ -7,8 +7,8 @@ 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 java.util.Random
import kotlin.math.floor import kotlin.math.floor
import kotlin.random.Random
class LevelFacade { class LevelFacade {
private val minefield: Minefield private val minefield: Minefield
@ -28,7 +28,7 @@ class LevelFacade {
var mines: Sequence<Area> = sequenceOf() var mines: Sequence<Area> = sequenceOf()
private set private set
constructor(minefield: Minefield, seed: Long = randomSeed()) { constructor(minefield: Minefield, seed: Long) {
this.minefield = minefield this.minefield = minefield
this.randomGenerator = Random(seed) this.randomGenerator = Random(seed)
this.seed = seed this.seed = seed
@ -195,6 +195,7 @@ class LevelFacade {
} }
fun doubleClick(index: Int): Int = getArea(index).run { fun doubleClick(index: Int): Int = getArea(index).run {
@Suppress("unused")
return when { return when {
isCovered -> { isCovered -> {
openField(getArea(index)) openField(getArea(index))
@ -212,6 +213,7 @@ class LevelFacade {
} }
} }
@Suppress("unused")
fun longPressOpenArea(index: Int): Sequence<Area> { fun longPressOpenArea(index: Int): Sequence<Area> {
val neighbors = getArea(index).findNeighbors().filter { val neighbors = getArea(index).findNeighbors().filter {
it.mark.isNone() && it.isCovered it.mark.isNone() && it.isCovered
@ -356,8 +358,4 @@ class LevelFacade {
fun setCurrentSaveId(id: Int) { fun setCurrentSaveId(id: Int) {
this.saveId = id.coerceAtLeast(0) this.saveId = id.coerceAtLeast(0)
} }
companion object {
private fun randomSeed(): Long = Random().nextLong()
}
} }

View file

@ -8,9 +8,9 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dev.lucasnlm.antimine.common.level.models.Event import dev.lucasnlm.antimine.common.level.models.Event
import dev.lucasnlm.antimine.common.level.database.AppDataBase import dev.lucasnlm.antimine.common.level.database.AppDataBase
import dev.lucasnlm.antimine.common.level.database.dao.SaveDao
import dev.lucasnlm.antimine.common.level.repository.DimensionRepository import dev.lucasnlm.antimine.common.level.repository.DimensionRepository
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.ISavesRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.SavesRepository import dev.lucasnlm.antimine.common.level.repository.SavesRepository
@ -22,7 +22,7 @@ import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
@Module @Module
class LevelModule( open class LevelModule(
private val application: Application private val application: Application
) { ) {
private val appDataBase by lazy { private val appDataBase by lazy {
@ -40,20 +40,20 @@ class LevelModule(
} }
@Provides @Provides
fun provideGameEventObserver(): MutableLiveData<Event> = MutableLiveData() open fun provideGameEventObserver(): MutableLiveData<Event> = MutableLiveData()
@Provides @Provides
fun provideClock(): Clock = Clock() open fun provideClock(): Clock = Clock()
@Provides @Provides
fun provideGameViewModelFactory( open fun provideGameViewModelFactory(
application: Application, application: Application,
eventObserver: MutableLiveData<Event>, eventObserver: MutableLiveData<Event>,
savesRepository: ISavesRepository, savesRepository: ISavesRepository,
dimensionRepository: IDimensionRepository, dimensionRepository: IDimensionRepository,
preferencesRepository: IPreferencesRepository, preferencesRepository: IPreferencesRepository,
hapticFeedbackInteractor: IHapticFeedbackInteractor, hapticFeedbackInteractor: IHapticFeedbackInteractor,
minefieldRepository: MinefieldRepository, minefieldRepository: IMinefieldRepository,
analyticsManager: AnalyticsManager, analyticsManager: AnalyticsManager,
clock: Clock clock: Clock
) = GameViewModelFactory( ) = GameViewModelFactory(
@ -69,26 +69,20 @@ class LevelModule(
) )
@Provides @Provides
fun provideDimensionRepository( open fun provideDimensionRepository(
context: Context, context: Context,
preferencesRepository: IPreferencesRepository preferencesRepository: IPreferencesRepository
): IDimensionRepository = ): IDimensionRepository =
DimensionRepository(context, preferencesRepository) DimensionRepository(context, preferencesRepository)
@Provides @Provides
fun provideDataBase(): AppDataBase = appDataBase open fun provideSavesRepository(): ISavesRepository = savesRepository
@Provides @Provides
fun provideSaveDao(): SaveDao = savesDao open fun provideMinefieldRepository(): IMinefieldRepository = MinefieldRepository()
@Provides @Provides
fun provideSavesRepository(): ISavesRepository = savesRepository open fun provideHapticFeedbackInteractor(
@Provides
fun provideMinefieldRepository(): MinefieldRepository = MinefieldRepository()
@Provides
fun provideHapticFeedbackInteractor(
application: Application, application: Application,
preferencesRepository: IPreferencesRepository preferencesRepository: IPreferencesRepository
): IHapticFeedbackInteractor = ): IHapticFeedbackInteractor =

View file

@ -3,16 +3,20 @@ package dev.lucasnlm.antimine.common.level.repository
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.content.res.TypedArray import android.content.res.TypedArray
import android.util.DisplayMetrics
import dev.lucasnlm.antimine.common.R import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
interface IDimensionRepository { interface IDimensionRepository {
fun areaSize(): Float fun areaSize(): Float
fun displaySize(): DisplayMetrics fun displaySize(): Size
fun actionBarSize(): Int fun actionBarSize(): Int
} }
data class Size(
val width: Int,
val height: Int
)
class DimensionRepository( class DimensionRepository(
private val context: Context, private val context: Context,
private val preferencesRepository: IPreferencesRepository private val preferencesRepository: IPreferencesRepository
@ -24,7 +28,9 @@ class DimensionRepository(
context.resources.getDimension(R.dimen.field_size) context.resources.getDimension(R.dimen.field_size)
} }
override fun displaySize(): DisplayMetrics = Resources.getSystem().displayMetrics override fun displaySize(): Size = with(Resources.getSystem().displayMetrics) {
return Size(this.widthPixels, this.heightPixels)
}
override fun actionBarSize(): Int { override fun actionBarSize(): Int {
val styledAttributes: TypedArray = val styledAttributes: TypedArray =

View file

@ -3,6 +3,7 @@ package dev.lucasnlm.antimine.common.level.repository
import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import kotlin.random.Random
interface IMinefieldRepository { interface IMinefieldRepository {
fun fromDifficulty( fun fromDifficulty(
@ -10,6 +11,8 @@ interface IMinefieldRepository {
dimensionRepository: IDimensionRepository, dimensionRepository: IDimensionRepository,
preferencesRepository: IPreferencesRepository preferencesRepository: IPreferencesRepository
): Minefield ): Minefield
fun randomSeed(): Long
} }
class MinefieldRepository : IMinefieldRepository { class MinefieldRepository : IMinefieldRepository {
@ -46,11 +49,9 @@ class MinefieldRepository : IMinefieldRepository {
val fieldSize = dimensionRepository.areaSize() val fieldSize = dimensionRepository.areaSize()
val display = dimensionRepository.displaySize() val display = dimensionRepository.displaySize()
val width = display.widthPixels
val height = display.heightPixels
val finalWidth = ((width / fieldSize).toInt() - 1).coerceAtLeast(6) val finalWidth = ((display.width / fieldSize).toInt() - 1).coerceAtLeast(6)
val finalHeight = ((height / fieldSize).toInt() - 3).coerceAtLeast(9) val finalHeight = ((display.height / fieldSize).toInt() - 3).coerceAtLeast(9)
return Minefield( return Minefield(
finalWidth, finalWidth,
@ -58,4 +59,6 @@ class MinefieldRepository : IMinefieldRepository {
(finalWidth * finalHeight * 0.2).toInt() (finalWidth * finalHeight * 0.2).toInt()
) )
} }
override fun randomSeed(): Long = Random.nextLong()
} }

View file

@ -4,7 +4,6 @@ import android.app.Application
import android.os.Handler import android.os.Handler
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository
import dev.lucasnlm.antimine.common.level.LevelFacade import dev.lucasnlm.antimine.common.level.LevelFacade
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
@ -12,6 +11,7 @@ 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.database.models.Save import dev.lucasnlm.antimine.common.level.database.models.Save
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.ISavesRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.utils.Clock import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
@ -31,7 +31,7 @@ class GameViewModel(
private val dimensionRepository: IDimensionRepository, private val dimensionRepository: IDimensionRepository,
private val preferencesRepository: IPreferencesRepository, private val preferencesRepository: IPreferencesRepository,
private val hapticFeedbackInteractor: IHapticFeedbackInteractor, private val hapticFeedbackInteractor: IHapticFeedbackInteractor,
private val minefieldRepository: MinefieldRepository, private val minefieldRepository: IMinefieldRepository,
private val analyticsManager: AnalyticsManager, private val analyticsManager: AnalyticsManager,
private val clock: Clock private val clock: Clock
) : ViewModel() { ) : ViewModel() {
@ -55,7 +55,7 @@ class GameViewModel(
newDifficulty, dimensionRepository, preferencesRepository newDifficulty, dimensionRepository, preferencesRepository
) )
levelFacade = LevelFacade(minefield) levelFacade = LevelFacade(minefield, minefieldRepository.randomSeed())
mineCount.postValue(minefield.mines) mineCount.postValue(minefield.mines)
difficulty.postValue(newDifficulty) difficulty.postValue(newDifficulty)

View file

@ -6,8 +6,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import dev.lucasnlm.antimine.common.level.models.Event import dev.lucasnlm.antimine.common.level.models.Event
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.ISavesRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository
import dev.lucasnlm.antimine.common.level.utils.Clock import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
@ -21,14 +21,14 @@ class GameViewModelFactory @Inject constructor(
private val dimensionRepository: IDimensionRepository, private val dimensionRepository: IDimensionRepository,
private val preferencesRepository: IPreferencesRepository, private val preferencesRepository: IPreferencesRepository,
private val hapticFeedbackInteractor: IHapticFeedbackInteractor, private val hapticFeedbackInteractor: IHapticFeedbackInteractor,
private val minefieldRepository: MinefieldRepository, private val minefieldRepository: IMinefieldRepository,
private val analyticsManager: AnalyticsManager, private val analyticsManager: AnalyticsManager,
private val clock: Clock private val clock: Clock
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = override fun <T : ViewModel?> create(modelClass: Class<T>): T =
if (modelClass.isAssignableFrom(GameViewModel::class.java)) { if (modelClass.isAssignableFrom(GameViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
GameViewModel( GameViewModel(
application, application,
eventObserver, eventObserver,

View file

@ -58,7 +58,7 @@ class LevelFacadeTest {
plantMinesExcept(3) plantMinesExcept(3)
assertNotEquals(field.filter { it.hasMine }.map { it.id }.first(), 3) assertNotEquals(field.filter { it.hasMine }.map { it.id }.first(), 3)
field.forEach { field.forEach {
if (it.id == 7) { if (it.id == 6) {
assertTrue(it.hasMine) assertTrue(it.hasMine)
} else { } else {
assertFalse(it.hasMine) assertFalse(it.hasMine)
@ -84,35 +84,34 @@ class LevelFacadeTest {
assertTrue( assertTrue(
levelFacadeOf(3, 3, 1, 200L).apply { levelFacadeOf(3, 3, 1, 200L).apply {
plantMinesExcept(3) plantMinesExcept(3)
}.at(7).hasMine }.at(6).hasMine
) )
assertTrue( assertTrue(
levelFacadeOf(3, 3, 1, 250L).apply { levelFacadeOf(3, 3, 1, 250L).apply {
plantMinesExcept(3) plantMinesExcept(3)
}.at(0).hasMine }.at(1).hasMine
) )
assertTrue( assertTrue(
levelFacadeOf(3, 3, 1, 100L).apply { levelFacadeOf(3, 3, 1, 100L).apply {
plantMinesExcept(3) plantMinesExcept(3)
}.at(8).hasMine }.at(4).hasMine
) )
assertTrue( assertTrue(
levelFacadeOf(3, 3, 1, 170L).apply { levelFacadeOf(3, 3, 1, 170L).apply {
plantMinesExcept(3) plantMinesExcept(3)
println(field) }.at(6).hasMine
}.at(2).hasMine
) )
} }
@Test @Test
fun testMineTips() { fun testMineTips() {
levelFacadeOf(3, 3, 1, 200L).run { levelFacadeOf(3, 3, 1, 150L).run {
plantMinesExcept(3) plantMinesExcept(3)
assertEquals( assertEquals(
listOf( listOf(
0, 0, 0, 1, 0, 1,
1, 1, 1, 1, 1, 1,
1, 0, 1 0, 0, 0
), ),
field.map { it.minesAround }.toList() field.map { it.minesAround }.toList()
) )
@ -122,9 +121,9 @@ class LevelFacadeTest {
plantMinesExcept(3) plantMinesExcept(3)
assertEquals( assertEquals(
listOf( listOf(
1, 0, 1, 0, 1, 1,
2, 2, 2, 1, 2, 0,
1, 0, 1 0, 2, 1
), ),
field.map { it.minesAround }.toList() field.map { it.minesAround }.toList()
) )
@ -134,9 +133,9 @@ class LevelFacadeTest {
plantMinesExcept(3) plantMinesExcept(3)
assertEquals( assertEquals(
listOf( listOf(
0, 0, 1, 0, 2, 0,
3, 3, 2, 1, 3, 0,
1, 0, 1 0, 2, 1
), ),
field.map { it.minesAround }.toList() field.map { it.minesAround }.toList()
) )
@ -146,10 +145,10 @@ class LevelFacadeTest {
plantMinesExcept(3) plantMinesExcept(3)
assertEquals( assertEquals(
listOf( listOf(
0, 0, 0, 1, 0, 0, 2, 1,
3, 0, 3, 1, 0, 5, 0, 2,
2, 3, 2, 1, 2, 4, 0, 2,
0, 2, 0, 1 0, 2, 1, 1
), ),
field.map { it.minesAround }.toList() field.map { it.minesAround }.toList()
) )
@ -159,10 +158,10 @@ class LevelFacadeTest {
plantMinesExcept(3) plantMinesExcept(3)
assertEquals( assertEquals(
listOf( listOf(
0, 0, 1, 0, 0, 1, 1, 1,
2, 2, 1, 0, 0, 2, 0, 2,
0, 0, 0, 0, 0, 2, 0, 2,
0, 0, 0, 0 0, 1, 1, 1
), ),
field.map { it.minesAround }.toList() field.map { it.minesAround }.toList()
) )
@ -172,10 +171,10 @@ class LevelFacadeTest {
plantMinesExcept(3) plantMinesExcept(3)
assertEquals( assertEquals(
listOf( listOf(
1, 0, 1, 0,
1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0 0, 1, 1, 1,
0, 1, 0, 1,
0, 1, 1, 1
), ),
field.map { it.minesAround }.toList() field.map { it.minesAround }.toList()
) )
@ -324,11 +323,11 @@ class LevelFacadeTest {
@Test @Test
fun testOpenSafeZone() { fun testOpenSafeZone() {
levelFacadeOf(3, 3, 1, 200L).run { levelFacadeOf(3, 3, 1, 0).run {
plantMinesExcept(3) plantMinesExcept(3)
assertEquals(field.filter { it.isCovered }.count(), field.count()) assertEquals(field.filterNot { it.isCovered }.count(), 0)
singleClick(1) singleClick(1)
assertEquals(field.filter { it.isCovered }.count(), field.count() - 6) assertEquals(field.filterNot { it.isCovered }.count(), 6)
assertEquals( assertEquals(
field.filterNot { it.isCovered }.map { it.id }.toList(), field.filterNot { it.isCovered }.map { it.id }.toList(),
listOf(0, 1, 2, 3, 4, 5) listOf(0, 1, 2, 3, 4, 5)
@ -381,7 +380,7 @@ class LevelFacadeTest {
plantMinesExcept(3) plantMinesExcept(3)
val mine = field.last { it.hasMine } val mine = field.last { it.hasMine }
assertEquals( assertEquals(
listOf(35, 33, 22, 27, 17, 32, 25, 8, 7, 2), listOf(33, 26, 28, 31, 19, 14, 18, 7, 6, 4),
takeExplosionRadius(mine).map { it.id }.toList() takeExplosionRadius(mine).map { it.id }.toList()
) )
} }
@ -390,7 +389,7 @@ class LevelFacadeTest {
plantMinesExcept(3) plantMinesExcept(3)
val mine = field.first { it.hasMine } val mine = field.first { it.hasMine }
assertEquals( assertEquals(
listOf(2, 8, 7, 17, 22, 25, 27, 32, 33, 35), listOf(4, 14, 7, 28, 6, 19, 26, 18, 33, 31),
takeExplosionRadius(mine).map { it.id }.toList() takeExplosionRadius(mine).map { it.id }.toList()
) )
} }
@ -399,7 +398,7 @@ class LevelFacadeTest {
plantMinesExcept(3) plantMinesExcept(3)
val mine = field.filter { it.hasMine }.elementAt(4) val mine = field.filter { it.hasMine }.elementAt(4)
assertEquals( assertEquals(
listOf(22, 17, 27, 33, 35, 8, 32, 25, 2, 7), listOf(18, 19, 6, 7, 14, 26, 31, 33, 28, 4),
takeExplosionRadius(mine).map { it.id }.toList() takeExplosionRadius(mine).map { it.id }.toList()
) )
} }
@ -485,7 +484,7 @@ class LevelFacadeTest {
val width = 12 val width = 12
val height = 12 val height = 12
val mines = 9 val mines = 9
levelFacadeOf(width, height, mines, 200L).run { levelFacadeOf(width, height, mines, 150L).run {
plantMinesExcept(3) plantMinesExcept(3)
field.filterNot { it.hasMine }.take(width * height - 1 - mines).forEach { field.filterNot { it.hasMine }.take(width * height - 1 - mines).forEach {
singleClick(it.id) singleClick(it.id)

View file

@ -1,12 +1,12 @@
package dev.lucasnlm.antimine.common.level package dev.lucasnlm.antimine.common.level
import android.util.DisplayMetrics
import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.Size
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@ -74,10 +74,7 @@ class MinefieldFactoryTest {
val dimensionRepository: IDimensionRepository = mock { val dimensionRepository: IDimensionRepository = mock {
on { areaSize() } doReturn 10.0f on { areaSize() } doReturn 10.0f
on { actionBarSize() } doReturn 10 on { actionBarSize() } doReturn 10
on { displaySize() } doReturn DisplayMetrics().apply { on { displaySize() } doReturn Size(500, 1000)
widthPixels = 500
heightPixels = 1000
}
} }
MinefieldRepository().fromDifficulty( MinefieldRepository().fromDifficulty(

View file

@ -0,0 +1,97 @@
package dev.lucasnlm.antimine.common.level.di
import android.app.Application
import android.content.Context
import androidx.lifecycle.MutableLiveData
import dagger.Module
import dagger.Provides
import dev.lucasnlm.antimine.common.level.database.models.Save
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.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.Size
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
@Module
class TestLevelModule(
private val application: Application
) {
@Provides
fun provideGameEventObserver(): MutableLiveData<Event> = MutableLiveData()
@Provides
fun provideClock(): Clock = Clock()
@Provides
fun provideGameViewModelFactory(
application: Application,
eventObserver: MutableLiveData<Event>,
savesRepository: ISavesRepository,
dimensionRepository: IDimensionRepository,
preferencesRepository: IPreferencesRepository,
hapticFeedbackInteractor: IHapticFeedbackInteractor,
minefieldRepository: MinefieldRepository,
analyticsManager: AnalyticsManager,
clock: Clock
) = GameViewModelFactory(
application,
eventObserver,
savesRepository,
dimensionRepository,
preferencesRepository,
hapticFeedbackInteractor,
minefieldRepository,
analyticsManager,
clock
)
@Provides
fun provideDimensionRepository(
context: Context,
preferencesRepository: IPreferencesRepository
): IDimensionRepository = object : IDimensionRepository {
override fun areaSize(): Float = 50.0f
override fun displaySize(): Size = Size(50 * 15, 50 * 30)
override fun actionBarSize(): Int = 50
}
@Provides
fun provideSavesRepository(): ISavesRepository = object : ISavesRepository {
override suspend fun fetchCurrentSave(): Save? = null
override suspend fun saveGame(save: Save): Long? = null
override fun setLimit(maxSavesStorage: Int) { }
}
@Provides
fun provideMinefieldRepository(): IMinefieldRepository = object : IMinefieldRepository {
override fun fromDifficulty(
difficulty: Difficulty,
dimensionRepository: IDimensionRepository,
preferencesRepository: IPreferencesRepository
) = Minefield(9, 9, 9)
override fun randomSeed(): Long = 200
}
@Provides
fun provideHapticFeedbackInteractor(
application: Application,
preferencesRepository: IPreferencesRepository
): IHapticFeedbackInteractor = object : IHapticFeedbackInteractor {
override fun toggleFlagFeedback() { }
override fun explosionFeedback() { }
}
}

View file

@ -32,7 +32,7 @@ ext.versions = [
// Kotlin // Kotlin
kotlin : '1.3.70', kotlin : '1.3.70',
coroutines : '1.3.4', coroutines : '1.3.5',
// Jetpack // Jetpack
lifecycle : '1.1.1', lifecycle : '1.1.1',
@ -47,5 +47,8 @@ ext.versions = [
junit : '4.12', junit : '4.12',
mockito : '2.24.0', mockito : '2.24.0',
mockitoKotlin : '2.1.0', mockitoKotlin : '2.1.0',
testCore : '1.2.0' testCore : '1.2.0',
espresso : '3.2.0',
fragmentTest : '1.1.0',
robolectric : '4.3'
] ]