From a8ab8c4240944946e323516bae581b4e132b64dd Mon Sep 17 00:00:00 2001 From: Lucas Lima Date: Wed, 20 May 2020 14:14:53 -0300 Subject: [PATCH] Add Stats screen --- .../lucasnlm/antimine/stats/StatsActivity.kt | 33 ++++++++ .../antimine/stats/model/StatsModel.kt | 10 +++ .../stats/viewmodel/StatsViewModel.kt | 41 ++++++++++ app/src/main/res/layout/activity_stats.xml | 56 +++++++++++++- .../lucasnlm/antimine/di/TestLevelModule.kt | 7 ++ .../antimine/mocks/MockStatsRepository.kt | 14 ++++ .../stats/viewmodel/StatsViewModelTest.kt | 76 +++++++++++++++++++ .../antimine/common/level/LevelFacade.kt | 5 +- .../common/level/database/models/Stats.kt | 7 +- common/src/main/res/values/strings.xml | 3 + 10 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/dev/lucasnlm/antimine/stats/model/StatsModel.kt create mode 100644 app/src/main/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModel.kt create mode 100644 app/src/test/java/dev/lucasnlm/antimine/mocks/MockStatsRepository.kt create mode 100644 app/src/test/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModelTest.kt diff --git a/app/src/main/java/dev/lucasnlm/antimine/stats/StatsActivity.kt b/app/src/main/java/dev/lucasnlm/antimine/stats/StatsActivity.kt index 0cf53493..b5afd585 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/stats/StatsActivity.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/stats/StatsActivity.kt @@ -1,13 +1,46 @@ package dev.lucasnlm.antimine.stats import android.os.Bundle +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders import dagger.android.support.DaggerAppCompatActivity import dev.lucasnlm.antimine.R +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository +import dev.lucasnlm.antimine.stats.viewmodel.StatsViewModel +import kotlinx.android.synthetic.main.activity_stats.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import javax.inject.Inject class StatsActivity : DaggerAppCompatActivity() { + @Inject + lateinit var statsRepository: IStatsRepository + + private lateinit var viewModel: StatsViewModel + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_stats) setTitle(R.string.events) + + viewModel = ViewModelProviders.of(this).get(StatsViewModel::class.java) + viewModel.statsObserver.observe(this, Observer { + minesCount.text = it.mines.toString() + totalTime.text = formatTime(it.duration) + averageTime.text = formatTime(it.averageDuration) + totalGames.text = it.totalGames.toString() + performance.text = formatPercentage(100.0 * it.victory / it.totalGames) + openAreas.text = it.openArea.toString() + }) + + GlobalScope.launch { + viewModel.loadStats(statsRepository) + } } + + private fun formatPercentage(value: Double) = + String.format("%.2f%%", value) + + private fun formatTime(durationSecs: Long) = + String.format("%02d:%02d:%02d", durationSecs / 3600, durationSecs % 3600 / 60, durationSecs % 60) } diff --git a/app/src/main/java/dev/lucasnlm/antimine/stats/model/StatsModel.kt b/app/src/main/java/dev/lucasnlm/antimine/stats/model/StatsModel.kt new file mode 100644 index 00000000..6ec23f64 --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/stats/model/StatsModel.kt @@ -0,0 +1,10 @@ +package dev.lucasnlm.antimine.stats.model + +data class StatsModel( + val totalGames: Int, + val duration: Long, + val averageDuration: Long, + val mines: Int, + val victory: Int, + val openArea: Int +) diff --git a/app/src/main/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModel.kt b/app/src/main/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModel.kt new file mode 100644 index 00000000..9f31a2e5 --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModel.kt @@ -0,0 +1,41 @@ +package dev.lucasnlm.antimine.stats.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository +import dev.lucasnlm.antimine.stats.model.StatsModel + +class StatsViewModel : ViewModel() { + val statsObserver = MutableLiveData() + + suspend fun getStatsModel(statsRepository: IStatsRepository): StatsModel? { + val stats = statsRepository.getAllStats() + val statsCount = stats.count() + + return if (statsCount > 0) { + val result = stats.fold( + StatsModel(statsCount, 0L, 0L, 0, 0, 0) + ) { acc, value -> + StatsModel( + acc.totalGames, + acc.duration + value.duration, + 0, + acc.mines + value.mines, + acc.victory + value.victory, + acc.openArea + value.openArea + ) + } + result.copy(averageDuration = result.duration / result.totalGames) + } else { + StatsModel(0, 0, 0, 0, 0, 0) + } + } + + suspend fun loadStats(statsRepository: IStatsRepository) { + getStatsModel(statsRepository)?.let { + if (it.totalGames > 0) { + statsObserver.postValue(it) + } + } + } +} diff --git a/app/src/main/res/layout/activity_stats.xml b/app/src/main/res/layout/activity_stats.xml index be54a09c..5b79b190 100644 --- a/app/src/main/res/layout/activity_stats.xml +++ b/app/src/main/res/layout/activity_stats.xml @@ -5,32 +5,78 @@ android:layout_height="match_parent" android:stretchColumns="1" android:fitsSystemWindows="true" + android:divider="?android:listDivider" + android:showDividers="middle" tools:context=".stats.StatsActivity"> + + + + + + android:text="-" + tools:ignore="HardcodedText" /> - + android:text="-" + tools:ignore="HardcodedText" /> + + + + + + + + + + + + @@ -41,8 +87,10 @@ android:textColor="@color/text_color" /> + android:text="-" + tools:ignore="HardcodedText" /> diff --git a/app/src/test/java/dev/lucasnlm/antimine/di/TestLevelModule.kt b/app/src/test/java/dev/lucasnlm/antimine/di/TestLevelModule.kt index 2ac11b84..4ed1356b 100644 --- a/app/src/test/java/dev/lucasnlm/antimine/di/TestLevelModule.kt +++ b/app/src/test/java/dev/lucasnlm/antimine/di/TestLevelModule.kt @@ -10,6 +10,7 @@ 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.repository.IStatsRepository import dev.lucasnlm.antimine.common.level.utils.Clock import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory @@ -19,6 +20,7 @@ import dev.lucasnlm.antimine.mocks.MockDimensionRepository import dev.lucasnlm.antimine.mocks.MockHapticFeedbackInteractor import dev.lucasnlm.antimine.mocks.MockMinefieldRepository import dev.lucasnlm.antimine.mocks.MockSavesRepository +import dev.lucasnlm.antimine.mocks.MockStatsRepository @Module class TestLevelModule( @@ -35,6 +37,7 @@ class TestLevelModule( application: Application, eventObserver: MutableLiveData, savesRepository: ISavesRepository, + statsRepository: IStatsRepository, dimensionRepository: IDimensionRepository, preferencesRepository: IPreferencesRepository, hapticFeedbackInteractor: IHapticFeedbackInteractor, @@ -45,6 +48,7 @@ class TestLevelModule( application, eventObserver, savesRepository, + statsRepository, dimensionRepository, preferencesRepository, hapticFeedbackInteractor, @@ -62,6 +66,9 @@ class TestLevelModule( @Provides override fun provideSavesRepository(): ISavesRepository = MockSavesRepository() + @Provides + override fun provideStatsRepository(): IStatsRepository = MockStatsRepository(listOf()) + @Provides override fun provideMinefieldRepository(): IMinefieldRepository = MockMinefieldRepository() diff --git a/app/src/test/java/dev/lucasnlm/antimine/mocks/MockStatsRepository.kt b/app/src/test/java/dev/lucasnlm/antimine/mocks/MockStatsRepository.kt new file mode 100644 index 00000000..814d0a0e --- /dev/null +++ b/app/src/test/java/dev/lucasnlm/antimine/mocks/MockStatsRepository.kt @@ -0,0 +1,14 @@ +package dev.lucasnlm.antimine.mocks + +import dev.lucasnlm.antimine.common.level.database.models.Stats +import dev.lucasnlm.antimine.common.level.repository.IStatsRepository + +class MockStatsRepository( + private val list: List +) : IStatsRepository { + override suspend fun getAllStats(): List = list + + override suspend fun addStats(stats: Stats): Long? { + return null + } +} diff --git a/app/src/test/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModelTest.kt b/app/src/test/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModelTest.kt new file mode 100644 index 00000000..11c39216 --- /dev/null +++ b/app/src/test/java/dev/lucasnlm/antimine/stats/viewmodel/StatsViewModelTest.kt @@ -0,0 +1,76 @@ +package dev.lucasnlm.antimine.stats.viewmodel + +import dev.lucasnlm.antimine.common.level.database.models.Stats +import dev.lucasnlm.antimine.mocks.MockStatsRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@ExperimentalCoroutinesApi +class StatsViewModelTest { + private val listOfStats = listOf( + Stats(0, 1000, 10, 1, 10, 10, 90), + Stats(1, 1200, 24, 0, 10, 10, 20) + ) + + @Test + fun testStatsTotalGames() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(2, statsModel?.totalGames) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0, emptyStatsModel?.totalGames) + } + + @Test + fun testStatsDuration() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(2200L, statsModel?.duration) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0L, emptyStatsModel?.duration) + } + + @Test + fun testStatsAverageDuration() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(1100L, statsModel?.averageDuration) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0L, emptyStatsModel?.averageDuration) + } + + @Test + fun testStatsMines() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(34, statsModel?.mines) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0, emptyStatsModel?.mines) + } + + @Test + fun testVictory() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(1, statsModel?.victory) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0, emptyStatsModel?.victory) + } + + @Test + fun testOpenArea() = runBlockingTest { + val viewModel = StatsViewModel() + val statsModel = viewModel.getStatsModel(MockStatsRepository(listOfStats)) + assertEquals(110, statsModel?.openArea) + + val emptyStatsModel = viewModel.getStatsModel(MockStatsRepository(listOf())) + assertEquals(0, emptyStatsModel?.openArea) + } +} diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt index c29189d7..362091c2 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/LevelFacade.kt @@ -368,10 +368,11 @@ class LevelFacade { Stats( 0, duration, - mines.count().toLong(), + mines.count(), if (gameStatus == SaveStatus.VICTORY) 1 else 0, minefield.width, - minefield.height + minefield.height, + mines.count { !it.isCovered } ) } } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/database/models/Stats.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/database/models/Stats.kt index 1e3be1ae..49b8a378 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/database/models/Stats.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/database/models/Stats.kt @@ -13,7 +13,7 @@ data class Stats( val duration: Long, @ColumnInfo(name = "mines") - val mines: Long, + val mines: Int, @ColumnInfo(name = "victory") val victory: Int, @@ -22,5 +22,8 @@ data class Stats( val width: Int, @ColumnInfo(name = "height") - val height: Int + val height: Int, + + @ColumnInfo(name = "openArea") + val openArea: Int ) diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 663cca8c..f2b8a91d 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Antimine You have to clear a rectangular board containing hidden \"mines\" without detonating any of them. Remaining mines + Games Previous Games Install Difficulty @@ -94,6 +95,8 @@ If you like this game, please give us a feedback. It will help us a lot. Yes ❤️️️ No + Open Areas Total Time + Average Time Performance