Add Stats screen

This commit is contained in:
Lucas Lima 2020-05-20 14:14:53 -03:00
parent 78ea2d9f87
commit a8ab8c4240
No known key found for this signature in database
GPG key ID: C828A958035D9C34
10 changed files with 244 additions and 8 deletions

View file

@ -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)
}

View file

@ -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
)

View file

@ -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<StatsModel>()
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)
}
}
}
}

View file

@ -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">
<TableRow>
<TextView
android:padding="16dp"
android:text="@string/games"
android:textColor="@color/text_color" />
<TextView
android:id="@+id/totalGames"
android:gravity="end"
android:padding="16dp"
android:text="0"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:text="@string/mines"
android:textColor="@color/text_color" />
<TextView
android:id="@+id/minesCount"
android:gravity="end"
android:padding="16dp"
android:text="100" />
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:text="@string/total_time"
android:textColor="@color/text_color" />
<TextView
android:id="@+id/totalTime"
android:gravity="end"
android:padding="16dp"
android:text="100" />
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:text="@string/average_time"
android:textColor="@color/text_color" />
<TextView
android:id="@+id/averageTime"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:text="@string/open_areas"
android:textColor="@color/text_color" />
<TextView
android:id="@+id/openAreas"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
@ -41,8 +87,10 @@
android:textColor="@color/text_color" />
<TextView
android:id="@+id/performance"
android:gravity="end"
android:padding="16dp"
android:text="100" />
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
</TableLayout>

View file

@ -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<Event>,
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()

View file

@ -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<Stats>
) : IStatsRepository {
override suspend fun getAllStats(): List<Stats> = list
override suspend fun addStats(stats: Stats): Long? {
return null
}
}

View file

@ -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)
}
}

View file

@ -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 }
)
}
}

View file

@ -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
)

View file

@ -3,6 +3,7 @@
<string name="app_name">Antimine</string>
<string name="app_description">You have to clear a rectangular board containing hidden \"mines\" without detonating any of them.</string>
<string name="remaining_mines">Remaining mines</string>
<string name="games">Games</string>
<string name="previous_games">Previous Games</string>
<string name="install">Install</string>
<string name="minefield">Difficulty</string>
@ -94,6 +95,8 @@
<string name="rating_message">If you like this game, please give us a feedback. It will help us a lot.</string>
<string name="rating_button">Yes ❤️️️</string>
<string name="rating_button_no">No</string>
<string name="open_areas">Open Areas</string>
<string name="total_time">Total Time</string>
<string name="average_time">Average Time</string>
<string name="performance">Performance</string>
</resources>