Merge pull request #71 from lucasnlm/add-stats

Add stats
This commit is contained in:
Lucas Nunes 2020-05-20 16:01:13 -03:00 committed by GitHub
commit 85e398536d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 564 additions and 48 deletions

View file

@ -139,11 +139,27 @@
<activity
android:name="dev.lucasnlm.antimine.about.TextActivity"
android:theme="@style/AppTheme" />
android:theme="@style/AppTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="dev.lucasnlm.antimine.about.AboutActivity" />
</activity>
<activity
android:name="dev.lucasnlm.antimine.about.AboutActivity"
android:theme="@style/AppTheme" />
android:theme="@style/AppTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="dev.lucasnlm.antimine.GameActivity" />
</activity>
<activity
android:name="dev.lucasnlm.antimine.stats.StatsActivity"
android:theme="@style/AppTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="dev.lucasnlm.antimine.GameActivity" />
</activity>
<activity
android:name="dev.lucasnlm.antimine.history.HistoryActivity"

View file

@ -37,6 +37,7 @@ import dev.lucasnlm.antimine.level.view.EndGameDialogFragment
import dev.lucasnlm.antimine.level.view.LevelFragment
import dev.lucasnlm.antimine.preferences.PreferencesActivity
import dev.lucasnlm.antimine.share.viewmodel.ShareViewModel
import dev.lucasnlm.antimine.stats.StatsActivity
import kotlinx.android.synthetic.main.activity_game.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@ -250,6 +251,7 @@ class GameActivity : DaggerAppCompatActivity() {
R.id.rate -> openRateUsLink("Drawer")
R.id.share_now -> shareCurrentGame()
R.id.previous_games -> openSaveHistory()
R.id.stats -> openStats()
R.id.install_new -> installFromInstantApp()
else -> handled = false
}
@ -373,6 +375,13 @@ class GameActivity : DaggerAppCompatActivity() {
}
}
private fun openStats() {
analyticsManager.sentEvent(Analytics.OpenStats())
Intent(this, StatsActivity::class.java).apply {
startActivity(this)
}
}
private fun showSettings() {
analyticsManager.sentEvent(Analytics.OpenSettings())
Intent(this, PreferencesActivity::class.java).apply {

View file

@ -4,7 +4,6 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
@ -22,7 +21,7 @@ class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_empty)
bindToolbar()
setTitle(R.string.about)
aboutViewModel = ViewModelProviders.of(this).get(AboutViewModel::class.java)
@ -60,23 +59,6 @@ class AboutActivity : AppCompatActivity() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(SOURCE_CODE)))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
private fun bindToolbar() {
supportActionBar?.apply {
setTitle(R.string.about)
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(true)
}
}
companion object {
private const val SOURCE_CODE = "https://github.com/lucasnlm/antimine-android"
}

View file

@ -2,7 +2,6 @@ package dev.lucasnlm.antimine.about
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem
import android.view.View
import dev.lucasnlm.antimine.R
@ -16,8 +15,8 @@ class TextActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = intent.getStringExtra(Constants.TEXT_TITLE)
setContentView(R.layout.activity_text)
bindToolbar()
GlobalScope.launch {
withContext(Dispatchers.Main) {
@ -41,23 +40,4 @@ class TextActivity : AppCompatActivity() {
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
var handled = false
if (item.itemId == android.R.id.home) {
onBackPressed()
handled = true
}
return handled || super.onOptionsItemSelected(item)
}
private fun bindToolbar() {
supportActionBar?.apply {
title = intent.getStringExtra(Constants.TEXT_TITLE)
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(true)
}
}
}

View file

@ -6,6 +6,7 @@ import dagger.android.ContributesAndroidInjector
import dev.lucasnlm.antimine.TvGameActivity
import dev.lucasnlm.antimine.core.scope.ActivityScope
import dev.lucasnlm.antimine.history.views.HistoryFragment
import dev.lucasnlm.antimine.stats.StatsActivity
@Module
interface ActivityModule {
@ -17,6 +18,10 @@ interface ActivityModule {
@ContributesAndroidInjector
fun contributeHistoryFragmentInjector(): HistoryFragment
@ActivityScope
@ContributesAndroidInjector
fun contributeStatsActivityInjector(): StatsActivity
@ActivityScope
@ContributesAndroidInjector
fun contributeTvGameActivityInjector(): TvGameActivity

View file

@ -0,0 +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

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
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="-"
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="-"
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>
<TextView
android:padding="16dp"
android:text="@string/performance"
android:textColor="@color/text_color" />
<TextView
android:id="@+id/performance"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
</TableLayout>

View file

@ -8,6 +8,34 @@
android:fitsSystemWindows="false"
tools:context="dev.lucasnlm.antimine.history.views.HistoryFragment">
<LinearLayout
android:id="@+id/stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/events" />
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:importantForAccessibility="no"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stats"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/saveHistory"
android:layout_width="match_parent"
@ -16,7 +44,6 @@
android:overScrollMode="never"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -45,6 +45,11 @@
android:checkable="false"
android:title="@string/previous_games" />
<item
android:id="@+id/stats"
android:checkable="false"
android:title="@string/events" />
</group>
<group

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

@ -2,6 +2,7 @@ package dev.lucasnlm.antimine.common.level
import dev.lucasnlm.antimine.common.level.database.models.Save
import dev.lucasnlm.antimine.common.level.database.models.SaveStatus
import dev.lucasnlm.antimine.common.level.database.models.Stats
import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Mark
@ -354,6 +355,27 @@ class LevelFacade {
)
}
fun getStats(duration: Long): Stats? {
val gameStatus: SaveStatus = when {
checkVictory() -> SaveStatus.VICTORY
hasAnyMineExploded() -> SaveStatus.DEFEAT
else -> SaveStatus.ON_GOING
}
return if (gameStatus == SaveStatus.ON_GOING) {
null
} else {
Stats(
0,
duration,
mines.count(),
if (gameStatus == SaveStatus.VICTORY) 1 else 0,
minefield.width,
minefield.height,
mines.count { !it.isCovered }
)
}
}
fun setCurrentSaveId(id: Int) {
this.saveId = id.coerceAtLeast(0)
}

View file

@ -8,12 +8,15 @@ import dev.lucasnlm.antimine.common.level.database.converters.FieldConverter
import dev.lucasnlm.antimine.common.level.database.converters.MinefieldConverter
import dev.lucasnlm.antimine.common.level.database.converters.SaveStatusConverter
import dev.lucasnlm.antimine.common.level.database.dao.SaveDao
import dev.lucasnlm.antimine.common.level.database.dao.StatsDao
import dev.lucasnlm.antimine.common.level.database.models.Save
import dev.lucasnlm.antimine.common.level.database.models.Stats
@Database(
entities = [
Save::class
], version = 2, exportSchema = false
Save::class,
Stats::class
], version = 3, exportSchema = false
)
@TypeConverters(
FieldConverter::class,
@ -23,4 +26,5 @@ import dev.lucasnlm.antimine.common.level.database.models.Save
)
abstract class AppDataBase : RoomDatabase() {
abstract fun saveDao(): SaveDao
abstract fun statsDao(): StatsDao
}

View file

@ -0,0 +1,16 @@
package dev.lucasnlm.antimine.common.level.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import dev.lucasnlm.antimine.common.level.database.models.Stats
@Dao
interface StatsDao {
@Query("SELECT * FROM stats")
suspend fun getAll(): List<Stats>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(vararg stats: Stats): Array<Long>
}

View file

@ -0,0 +1,29 @@
package dev.lucasnlm.antimine.common.level.database.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Stats(
@PrimaryKey(autoGenerate = true)
val uid: Int,
@ColumnInfo(name = "duration")
val duration: Long,
@ColumnInfo(name = "mines")
val mines: Int,
@ColumnInfo(name = "victory")
val victory: Int,
@ColumnInfo(name = "width")
val width: Int,
@ColumnInfo(name = "height")
val height: Int,
@ColumnInfo(name = "openArea")
val openArea: Int
)

View file

@ -12,8 +12,10 @@ import dev.lucasnlm.antimine.common.level.repository.DimensionRepository
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.repository.MinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.SavesRepository
import dev.lucasnlm.antimine.common.level.repository.StatsRepository
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.HapticFeedbackInteractor
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
@ -35,10 +37,18 @@ open class LevelModule(
appDataBase.saveDao()
}
private val statsDao by lazy {
appDataBase.statsDao()
}
private val savesRepository by lazy {
SavesRepository(savesDao)
}
private val statsRepository by lazy {
StatsRepository(statsDao)
}
@Provides
open fun provideGameEventObserver(): MutableLiveData<Event> = MutableLiveData()
@ -50,6 +60,7 @@ open class LevelModule(
application: Application,
eventObserver: MutableLiveData<Event>,
savesRepository: ISavesRepository,
statsRepository: IStatsRepository,
dimensionRepository: IDimensionRepository,
preferencesRepository: IPreferencesRepository,
hapticFeedbackInteractor: IHapticFeedbackInteractor,
@ -60,6 +71,7 @@ open class LevelModule(
application,
eventObserver,
savesRepository,
statsRepository,
dimensionRepository,
preferencesRepository,
hapticFeedbackInteractor,
@ -78,6 +90,9 @@ open class LevelModule(
@Provides
open fun provideSavesRepository(): ISavesRepository = savesRepository
@Provides
open fun provideStatsRepository(): IStatsRepository = statsRepository
@Provides
open fun provideMinefieldRepository(): IMinefieldRepository = MinefieldRepository()

View file

@ -0,0 +1,21 @@
package dev.lucasnlm.antimine.common.level.repository
import dev.lucasnlm.antimine.common.level.database.dao.StatsDao
import dev.lucasnlm.antimine.common.level.database.models.Stats
interface IStatsRepository {
suspend fun getAllStats(): List<Stats>
suspend fun addStats(stats: Stats): Long?
}
class StatsRepository(
private val statsDao: StatsDao
) : IStatsRepository {
override suspend fun getAllStats(): List<Stats> {
return statsDao.getAll()
}
override suspend fun addStats(stats: Stats): Long? {
return statsDao.insertAll(stats).firstOrNull()
}
}

View file

@ -13,6 +13,7 @@ import dev.lucasnlm.antimine.common.level.database.models.Save
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.core.analytics.AnalyticsManager
@ -28,6 +29,7 @@ class GameViewModel(
val application: Application,
val eventObserver: MutableLiveData<Event>,
private val savesRepository: ISavesRepository,
private val statsRepository: IStatsRepository,
private val dimensionRepository: IDimensionRepository,
private val preferencesRepository: IPreferencesRepository,
private val hapticFeedbackInteractor: IHapticFeedbackInteractor,
@ -146,6 +148,14 @@ class GameViewModel(
}
}
private suspend fun saveStats() {
if (initialized && levelFacade.hasMines) {
levelFacade.getStats(elapsedTimeSeconds.value ?: 0L)?.let {
statsRepository.addStats(it)
}
}
}
fun resumeGame() {
if (initialized && levelFacade.hasMines && !levelFacade.isGameOver()) {
eventObserver.postValue(Event.Resume)
@ -303,6 +313,7 @@ class GameViewModel(
}
GlobalScope.launch {
saveStats()
saveGame()
}
}
@ -321,6 +332,7 @@ class GameViewModel(
}
GlobalScope.launch {
saveStats()
saveGame()
}
}

View file

@ -8,6 +8,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.core.analytics.AnalyticsManager
@ -18,6 +19,7 @@ class GameViewModelFactory @Inject constructor(
private val application: Application,
private val eventObserver: MutableLiveData<Event>,
private val savesRepository: ISavesRepository,
private val statsRepository: IStatsRepository,
private val dimensionRepository: IDimensionRepository,
private val preferencesRepository: IPreferencesRepository,
private val hapticFeedbackInteractor: IHapticFeedbackInteractor,
@ -33,6 +35,7 @@ class GameViewModelFactory @Inject constructor(
application,
eventObserver,
savesRepository,
statsRepository,
dimensionRepository,
preferencesRepository,
hapticFeedbackInteractor,

View file

@ -65,6 +65,8 @@ sealed class Analytics(
class OpenAbout : Analytics("Open About")
class OpenStats : Analytics("Open Stats")
class OpenSettings : Analytics("Open Settings")
class OpenSaveHistory : Analytics("Open Save History")

View file

@ -3,6 +3,7 @@
<string name="app_name">Anti-Mine</string>
<string name="app_description">Musíte vyčistit obdélníkovou desku obsahující skryté miny, aniž by kterákoliv z nich vybuchla.</string>
<string name="remaining_mines">Zbývající miny</string>
<string name="games">Hry</string>
<string name="previous_games">Předchozí hry</string>
<string name="install">Instalovat</string>
<string name="minefield">Obtížnost</string>
@ -94,4 +95,8 @@
<string name="rating_message">Pokud se vám tato hra líbí, dejte nám prosím zpětnou vazbu. Hodně nám to pomůže.</string>
<string name="rating_button">Ano ❤️️️</string>
<string name="rating_button_no">Ne</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>

View file

@ -3,6 +3,7 @@
<string name="app_name">Anti-Mine</string>
<string name="app_description">Du musst eine rechteckige Tafel mit versteckten minen räumen, ohne dass eine davon explodiert.</string>
<string name="remaining_mines">Verbleibende Minen</string>
<string name="games">Games</string>
<string name="previous_games">Vorherige Spiele</string>
<string name="install">Installieren</string>
<string name="minefield">Schwierigkeitsgrad</string>
@ -94,4 +95,8 @@
<string name="rating_message">Wenn dir dieses Spiel gefällt, gib uns bitte eine Rückmeldung. Es wird uns sehr helfen.</string>
<string name="rating_button">Ja ❤️️️</string>
<string name="rating_button_no">Nein</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>

View file

@ -3,6 +3,7 @@
<string name="app_name">Antimine</string>
<string name="app_description">Πρέπει να καθαρίσετε μια ορθογώνια πλακέτα που περιέχει κρυμμένες \"νάρκες\" χωρίς να πυροδοτήσετε καμία από αυτές.</string>
<string name="remaining_mines">Υπόλοιπες νάρκες</string>
<string name="games">Games</string>
<string name="previous_games">Προηγούμενα Παιχνίδια</string>
<string name="install">Εγκατάσταση</string>
<string name="minefield">Δυσκολία</string>
@ -94,4 +95,8 @@
<string name="rating_message">Αν σας αρέσει αυτό το παιχνίδι, παρακαλούμε δώστε μας τα σχόλιά σας. Θα μας βοηθήσει πολύ.</string>
<string name="rating_button">Ναι ❤️️️</string>
<string name="rating_button_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>

View file

@ -3,6 +3,7 @@
<string name="app_name">Anti-Mina</string>
<string name="app_description">Usted tiene que limpiar un tablero cuadrado que contiene minas escondidas sin detornarlas.</string>
<string name="remaining_mines">Minas restantes</string>
<string name="games">Games</string>
<string name="previous_games">Juegos anteriores</string>
<string name="install">Instalar</string>
<string name="minefield">Dificultad</string>
@ -78,7 +79,7 @@
<string name="auto_flag">Asistente de Juego</string>
<string name="desc_convered_area">Área cubierta</string>
<string name="desc_marked_area">Área marcada</string>
<string name="desc_question_area">Area dudosa</string>
<string name="desc_question_area">Área dudosa</string>
<string name="desc_wrongly_marked_area">Área marcada incorrectamente</string>
<string name="settings_general">General</string>
<string name="settings_vibration_desc">Vibrar al activar la explosión o la bandera</string>
@ -94,4 +95,8 @@
<string name="rating_message">Si te gusta este juego, por favor danos un comentario. Nos ayudará mucho.</string>
<string name="rating_button">Sí ❤️️️</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>

View file

@ -3,6 +3,7 @@
<string name="app_name">Anti-Mine</string>
<string name="app_description">Vous devez vider un tableau rectangulaire contenant des mines cachées sans en détonner.</string>
<string name="remaining_mines">Mines restantes</string>
<string name="games">Games</string>
<string name="previous_games">Parties précédentes</string>
<string name="install">Installer</string>
<string name="minefield">Difficulté</string>
@ -94,4 +95,8 @@
<string name="rating_message">Si vous aimez ce jeu, n\'hésitez pas à jour donner un retour. Ça nous serait très utile.</string>
<string name="rating_button">Oui ❤️</string>
<string name="rating_button_no">Non</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>

View file

@ -3,6 +3,7 @@
<string name="app_name">Antimine</string>
<string name="app_description">L\'obbiettivo del gioco è ripulire un campo rettangolare che contiene mine nascoste senza detonarne nessuna.</string>
<string name="remaining_mines">Mine rimanenti</string>
<string name="games">Games</string>
<string name="previous_games">Previous Games</string>
<string name="install">Installare</string>
<string name="minefield">Difficoltà</string>
@ -94,4 +95,8 @@
<string name="rating_message">Se ti piace questo gioco, per favore inviaci suggerimenti. Puoi aiutare a migliorarlo.</string>
<string name="rating_button">Sì ❤️️️</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>

View file

@ -3,6 +3,7 @@
<string name="app_name">Anti-Mine</string>
<string name="app_description">Encontre todas as minas escondidas no campo minado.</string>
<string name="remaining_mines">Minas Restantes</string>
<string name="games">Games</string>
<string name="previous_games">Jogos Anteriores</string>
<string name="install">Instalar</string>
<string name="minefield">Dificuldade</string>
@ -94,4 +95,8 @@
<string name="rating_message">Se você está gostando do jogo, por favor deixe um comentário! Isso nos ajuda muito.</string>
<string name="rating_button">Sim ❤️️️</string>
<string name="rating_button_no">Não</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>

View file

@ -3,6 +3,7 @@
<string name="app_name">Anti-Mine</string>
<string name="app_description">Вам необходимо расчистить прямоугольную площадь со спрятанными минами, не взорвав ни одну из них.</string>
<string name="remaining_mines">Мин осталось</string>
<string name="games">Games</string>
<string name="previous_games">Предыдущая партия</string>
<string name="install">Установить</string>
<string name="minefield">Сложность</string>
@ -94,4 +95,8 @@
<string name="rating_message">Нравится игра? Оставьте отзыв, пожалуйста. Это нам очень поможет.</string>
<string name="rating_button">Да ❤️️️</string>
<string name="rating_button_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>

View file

@ -3,6 +3,7 @@
<string name="app_name">Anti-Mine</string>
<string name="app_description">Hiçbirini patlatmadan gizli mayın içeren dikdörtgen bir tahtayı temizlemelisiniz.</string>
<string name="remaining_mines">Kalan mayınlar</string>
<string name="games">Games</string>
<string name="previous_games">Önceki Oyun</string>
<string name="install">Yükle</string>
<string name="minefield">Zorluk</string>
@ -94,4 +95,8 @@
<string name="rating_message">Bu oyunu beğendiyseniz, lütfen bize bir geri bildirim verin. Bize çok yardımcı olacak.</string>
<string name="rating_button">Evet ❤️️️</string>
<string name="rating_button_no">Hayır</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>

View file

@ -3,6 +3,7 @@
<string name="app_name">Сапер</string>
<string name="app_description">Вам потрібно очистити поле від схованих мін, не детонуючи їх.</string>
<string name="remaining_mines">Залишилося мін</string>
<string name="games">Games</string>
<string name="previous_games">Попередня гра</string>
<string name="install">Встановити</string>
<string name="minefield">Складність</string>
@ -94,4 +95,8 @@
<string name="rating_message">Якщо вам подобається ця гра, то залиште нам свій відгук. Це нам дуже допоможе.</string>
<string name="rating_button">Так ❤️️️</string>
<string name="rating_button_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>

View file

@ -3,12 +3,15 @@
<string name="app_name">Dò mìn</string>
<string name="app_description">Bạn phải mở tất cả các ô trên một bãi mìn mà không làm nổ cục mìn nào.</string>
<string name="remaining_mines">Số mìn còn lại</string>
<string name="games">Games</string>
<string name="previous_games">Previous Games</string>
<string name="install">Cài đặt</string>
<string name="minefield">Độ khó</string>
<string name="standard">Tiêu chuẩn</string>
<string name="beginner">Dễ</string>
<string name="intermediate">Trung bình</string>
<string name="expert">Khó</string>
<string name="open">Mở</string>
<string name="open_menu">Mở menu</string>
<string name="close_menu">Đóng menu</string>
<string name="settings">Cài đặt</string>
@ -92,4 +95,8 @@
<string name="rating_message">Nếu bạn thích trò chơi này, hãy gửi phản hồi cho chúng tôi. Nhận xét của bạn sẽ giúp chúng tôi rất nhiều.</string>
<string name="rating_button">Có ❤️️️</string>
<string name="rating_button_no">Không</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>

View file

@ -3,6 +3,7 @@
<string name="app_name">反雷 - 扫雷</string>
<string name="app_description">你需要清除一个隐藏着地雷的矩形面板,不能使任何地雷爆炸。</string>
<string name="remaining_mines">剩余地雷</string>
<string name="games">Games</string>
<string name="previous_games">前一盘棋</string>
<string name="install">安装</string>
<string name="minefield">难度</string>
@ -94,4 +95,8 @@
<string name="rating_message">如果你喜欢这个游戏,请给我们反馈。这将对我们有很多帮助。</string>
<string name="rating_button">是 ❤️️️</string>
<string name="rating_button_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>

View file

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:locale="en">
<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="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,4 +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>

View file

@ -6,12 +6,14 @@ 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.database.models.Stats
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.IStatsRepository
import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.Size
import dev.lucasnlm.antimine.common.level.utils.Clock
@ -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,
@ -91,6 +95,13 @@ class TestLevelModule(
override fun randomSeed(): Long = 200
}
@Provides
fun provideStatsRepository(): IStatsRepository = object : IStatsRepository {
override suspend fun getAllStats(): List<Stats> = listOf()
override suspend fun addStats(stats: Stats): Long? = null
}
@Provides
fun provideHapticFeedbackInteractor(
application: Application,