Compare commits

..

No commits in common. "main" and "v1.0-18" have entirely different histories.

8 changed files with 52 additions and 244 deletions

View file

@ -83,9 +83,6 @@ dependencies {
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
// MPAndroidChart for progress visualization
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")

View file

@ -16,9 +16,7 @@ object ScalingFactors {
"Tetris DS" to RangeScalingFactor(3.0, 3.3, 4.5),
"Tetris Effect" to RangeScalingFactor(2.5, 3.8, 4.5),
"Rosy Retrospection DX" to RangeScalingFactor(4.0, 1.5, 1.8),
"Apotris" to RangeScalingFactor(1.8, 3.8, 4.4),
"Modretro Tetris" to RangeScalingFactor(2.0, 2.5, 3.0),
"Tetris Mobile" to RangeScalingFactor(2.2, 2.8, 3.5)
"Apotris" to RangeScalingFactor(1.8, 3.8, 4.4)
),
"Game Boy Tetris" to mapOf(
"NES Tetris" to 1.33,
@ -26,9 +24,7 @@ object ScalingFactors {
"Tetris DS" to RangeScalingFactor(4.0, 2.0, 2.0),
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 2.3),
"Rosy Retrospection DX" to 1.1,
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33),
"Modretro Tetris" to RangeScalingFactor(1.5, 1.8, 2.0),
"Tetris Mobile" to RangeScalingFactor(1.6, 1.9, 2.1)
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33)
),
"Tetris DX" to mapOf(
"NES Tetris" to 1.33,
@ -36,9 +32,7 @@ object ScalingFactors {
"Tetris DS" to RangeScalingFactor(4.0, 2.0, 2.0),
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 2.3),
"Rosy Retrospection DX" to 1.1,
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33),
"Modretro Tetris" to RangeScalingFactor(1.5, 1.8, 2.0),
"Tetris Mobile" to RangeScalingFactor(1.6, 1.9, 2.1)
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33)
),
"Tetris DS" to mapOf(
"NES Tetris" to RangeScalingFactor(0.33, 0.3, 0.22),
@ -46,9 +40,7 @@ object ScalingFactors {
"Tetris DX" to RangeScalingFactor(0.25, 0.5, 0.5),
"Tetris Effect" to RangeScalingFactor(0.83, 0.91, 1.0),
"Rosy Retrospection DX" to RangeScalingFactor(0.25, 0.91, 0.67),
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.9),
"Modretro Tetris" to RangeScalingFactor(0.4, 0.5, 0.6),
"Tetris Mobile" to RangeScalingFactor(0.45, 0.55, 0.65)
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.9)
),
"Tetris Effect" to mapOf(
"NES Tetris" to RangeScalingFactor(0.4, 0.26, 0.22),
@ -56,9 +48,7 @@ object ScalingFactors {
"Tetris DX" to RangeScalingFactor(0.25, 0.43, 0.43),
"Tetris DS" to RangeScalingFactor(1.2, 1.1, 1.0),
"Rosy Retrospection DX" to RangeScalingFactor(0.25, 0.43, 0.57),
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.85),
"Modretro Tetris" to RangeScalingFactor(0.45, 0.55, 0.65),
"Tetris Mobile" to RangeScalingFactor(0.5, 0.6, 0.7)
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.85)
),
"Rosy Retrospection DX" to mapOf(
"NES Tetris" to RangeScalingFactor(0.25, 0.67, 0.57),
@ -66,9 +56,7 @@ object ScalingFactors {
"Tetris DX" to 0.91,
"Tetris DS" to RangeScalingFactor(4.0, 1.5, 1.8),
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 1.8),
"Apotris" to RangeScalingFactor(1.1, 0.67, 0.5),
"Modretro Tetris" to RangeScalingFactor(1.3, 1.5, 1.7),
"Tetris Mobile" to RangeScalingFactor(1.4, 1.6, 1.8)
"Apotris" to RangeScalingFactor(1.1, 0.67, 0.5)
),
"Apotris" to mapOf(
"NES Tetris" to RangeScalingFactor(0.56, 0.26, 0.23),
@ -76,29 +64,7 @@ object ScalingFactors {
"Tetris DX" to RangeScalingFactor(0.75, 0.75, 0.5),
"Tetris DS" to RangeScalingFactor(3.0, 1.5, 1.0),
"Tetris Effect" to RangeScalingFactor(3.0, 1.7, 1.2),
"Rosy Retrospection DX" to RangeScalingFactor(1.1, 0.67, 0.5),
"Modretro Tetris" to RangeScalingFactor(1.2, 0.9, 0.7),
"Tetris Mobile" to RangeScalingFactor(1.3, 1.0, 0.8)
),
"Modretro Tetris" to mapOf(
"NES Tetris" to RangeScalingFactor(0.5, 0.4, 0.33),
"Game Boy Tetris" to RangeScalingFactor(0.67, 0.56, 0.5),
"Tetris DX" to RangeScalingFactor(0.67, 0.56, 0.5),
"Tetris DS" to RangeScalingFactor(2.5, 2.0, 1.67),
"Tetris Effect" to RangeScalingFactor(2.22, 1.82, 1.54),
"Rosy Retrospection DX" to RangeScalingFactor(0.77, 0.67, 0.59),
"Apotris" to RangeScalingFactor(0.83, 1.11, 1.43),
"Tetris Mobile" to RangeScalingFactor(1.1, 1.1, 1.1)
),
"Tetris Mobile" to mapOf(
"NES Tetris" to RangeScalingFactor(0.45, 0.36, 0.29),
"Game Boy Tetris" to RangeScalingFactor(0.63, 0.53, 0.48),
"Tetris DX" to RangeScalingFactor(0.63, 0.53, 0.48),
"Tetris DS" to RangeScalingFactor(2.22, 1.82, 1.54),
"Tetris Effect" to RangeScalingFactor(2.0, 1.67, 1.43),
"Rosy Retrospection DX" to RangeScalingFactor(0.71, 0.63, 0.56),
"Apotris" to RangeScalingFactor(0.77, 1.0, 1.25),
"Modretro Tetris" to RangeScalingFactor(0.91, 0.91, 0.91)
"Rosy Retrospection DX" to RangeScalingFactor(1.1, 0.67, 0.5)
)
)

View file

@ -12,9 +12,6 @@ interface ScoreDao {
@Query("SELECT * FROM scores WHERE gameVersion = :gameVersion")
fun getScoresForGame(gameVersion: String): LiveData<List<Score>>
@Query("SELECT * FROM scores WHERE gameVersion = :gameVersion ORDER BY dateRecorded ASC")
fun getScoresForGameByDate(gameVersion: String): LiveData<List<Score>>
@Query("SELECT DISTINCT gameVersion FROM scores")
fun getGamesWithScores(): LiveData<List<String>>

View file

@ -56,50 +56,6 @@ class EntryFragment : Fragment() {
if (game != null && score != null) {
viewModel.refreshEquivalentScores(game, score)
// Make sure UI updates immediately by forcing an adapter refresh
viewModel.equivalentScores.value?.let { scores ->
equivalentScoreAdapter.submitList(null) // Clear first
equivalentScoreAdapter.submitList(scores) // Then add new list
}
// Ensure card is visible
updateAnalysisCard()
}
}
}
/**
* Update the analysis card visibility and contents based on current state
*/
private fun updateAnalysisCard() {
if (viewModel.showConversion.value != true) {
binding.cardAnalysisResults.visibility = View.GONE
return
}
val game = viewModel.lastSubmittedGame.value
val score = viewModel.lastSubmittedScore.value
if (game != null && score != null) {
// Get the list of games with scores
val playedGames = viewModel.gamesWithScores.value ?: listOf()
// Make sure we don't show the source game in the equivalent dropdown
val filteredGames = playedGames.filter { it != game }
if (filteredGames.isNotEmpty()) {
binding.textViewOriginalScore.text = "Your $game score of ${"%,d".format(score)} is equivalent to:"
binding.cardAnalysisResults.visibility = View.VISIBLE
val filteredAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, filteredGames)
binding.autoCompleteEquivalentGame.setAdapter(filteredAdapter)
// Select first game by default
binding.autoCompleteEquivalentGame.setText(filteredGames[0], false)
viewModel.setSelectedEquivalentGame(filteredGames[0])
} else {
// If no other games to convert to, hide the card
binding.cardAnalysisResults.visibility = View.GONE
}
}
}
@ -112,9 +68,7 @@ class EntryFragment : Fragment() {
"Tetris DS",
"Tetris Effect",
"Rosy Retrospection DX",
"Apotris",
"Modretro Tetris",
"Tetris Mobile"
"Apotris"
)
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
binding.autoCompleteGameVersion.setAdapter(adapter)
@ -136,10 +90,8 @@ class EntryFragment : Fragment() {
viewModel.showConversion.observe(viewLifecycleOwner) { shouldShow ->
// No need to show toast here - we'll do it only after score submission
if (shouldShow) {
// Update card when showConversion changes
updateAnalysisCard()
} else {
binding.cardAnalysisResults.visibility = View.GONE
// Refresh conversions whenever showConversion becomes true
refreshConversions()
}
}
@ -147,8 +99,11 @@ class EntryFragment : Fragment() {
viewModel.gamesWithScores.observe(viewLifecycleOwner) { games ->
// Setup the game dropdown for adding equivalents - only with played games
if (games.isNotEmpty()) {
// Update card when games list changes
updateAnalysisCard()
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
binding.autoCompleteEquivalentGame.setAdapter(adapter)
// Also refresh conversions when game list changes
refreshConversions()
}
}
@ -171,22 +126,39 @@ class EntryFragment : Fragment() {
}
// Observe last submitted score details
viewModel.lastSubmittedGame.observe(viewLifecycleOwner) { _ ->
// Update the analysis card when last submitted game changes
updateAnalysisCard()
}
// Observe last submitted score value
viewModel.lastSubmittedScore.observe(viewLifecycleOwner) { _ ->
// Update the analysis card when score changes
updateAnalysisCard()
viewModel.lastSubmittedGame.observe(viewLifecycleOwner) { game ->
// Only continue if showConversion is true
if (viewModel.showConversion.value != true) {
binding.cardAnalysisResults.visibility = View.GONE
return@observe
}
viewModel.lastSubmittedScore.value?.let { score ->
binding.textViewOriginalScore.text = "Your $game score of ${"%,d".format(score)} is equivalent to:"
// Get the list of games with scores
val playedGames = viewModel.gamesWithScores.value ?: listOf()
// Make sure we don't show the source game in the equivalent dropdown
val filteredGames = playedGames.filter { it != game }
if (filteredGames.isNotEmpty()) {
binding.cardAnalysisResults.visibility = View.VISIBLE
val filteredAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, filteredGames)
binding.autoCompleteEquivalentGame.setAdapter(filteredAdapter)
// Select first game by default
binding.autoCompleteEquivalentGame.setText(filteredGames[0], false)
viewModel.setSelectedEquivalentGame(filteredGames[0])
} else {
// If no other games to convert to, hide the card
binding.cardAnalysisResults.visibility = View.GONE
}
}
}
// Observe equivalent scores
viewModel.equivalentScores.observe(viewLifecycleOwner) { scores ->
if (scores.isNotEmpty()) {
// Force a clean update by clearing first
equivalentScoreAdapter.submitList(null)
equivalentScoreAdapter.submitList(scores)
} else if (viewModel.showConversion.value == true) {
// If we should be showing conversions but have no scores, probably no other games
@ -213,7 +185,7 @@ class EntryFragment : Fragment() {
)
clearInputs()
// Force immediate refresh of conversions
// Check after submission if we should show requirements toast
if (viewModel.showConversion.value == false) {
Toast.makeText(
context,
@ -221,9 +193,7 @@ class EntryFragment : Fragment() {
Toast.LENGTH_LONG
).show()
} else {
refreshConversions()
// Scroll down to show the analysis results
// Only scroll down if we're going to show conversions
binding.root.post {
binding.root.fullScroll(View.FOCUS_DOWN)
}

View file

@ -23,9 +23,7 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
"Tetris DS",
"Tetris Effect",
"Rosy Retrospection DX",
"Apotris",
"Modretro Tetris",
"Tetris Mobile"
"Apotris"
)
// Track user played games and score counts
@ -110,14 +108,13 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
scoreDao.insert(newScore)
// After inserting, update the last submitted values
_lastSubmittedGame.value = gameVersion // Use immediate value change instead of postValue
_lastSubmittedScore.value = score // Use immediate value change instead of postValue
_lastSubmittedGame.postValue(gameVersion)
_lastSubmittedScore.postValue(score)
// Immediately check conversion criteria with current values
checkConversionCriteria()
// The criteria check will happen automatically through the observers in init
// Immediate refresh regardless if we just reached the criteria threshold
if (totalScoreCount.value ?: 0 >= 3 && (gamesWithScores.value?.size ?: 0) >= 2) {
// Only generate equivalent scores if we meet the criteria
if (_showConversion.value == true) {
generateEquivalentScores(gameVersion, score)
}
}
@ -149,8 +146,7 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
}
}
// Use setValue for immediate update on main thread rather than postValue
_equivalentScores.value = equivalents
_equivalentScores.postValue(equivalents)
}
/**

View file

@ -1,6 +1,5 @@
package com.accidentalproductions.tetristats.ui.stats
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -9,16 +8,7 @@ import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.accidentalproductions.tetristats.R
import com.accidentalproductions.tetristats.databinding.FragmentStatsBinding
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.formatter.ValueFormatter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class StatsFragment : Fragment() {
private var _binding: FragmentStatsBinding? = null
@ -40,7 +30,6 @@ class StatsFragment : Fragment() {
setupRecyclerView()
setupGameFilter()
setupProgressChart()
observeStats()
}
@ -63,64 +52,6 @@ class StatsFragment : Fragment() {
viewModel.setSelectedGame(selectedGame)
}
}
private fun setupProgressChart() {
with(binding.chartProgress) {
description.isEnabled = false
legend.isEnabled = true
setTouchEnabled(true)
setDrawGridBackground(false)
isDragEnabled = true
setScaleEnabled(true)
setPinchZoom(true)
axisRight.isEnabled = false
xAxis.position = XAxis.XAxisPosition.BOTTOM
xAxis.granularity = 1f
xAxis.setDrawGridLines(false)
axisLeft.setDrawGridLines(true)
axisLeft.axisMinimum = 0f
}
}
private fun updateProgressChart(scores: List<Entry>, dates: List<Long>) {
if (scores.isEmpty()) {
binding.chartProgress.clear()
binding.chartProgress.invalidate()
return
}
val dataSet = LineDataSet(scores, "Score Progress").apply {
mode = LineDataSet.Mode.CUBIC_BEZIER
color = resources.getColor(R.color.tetris_navy, null)
lineWidth = 2f
setDrawCircles(true)
setCircleColor(resources.getColor(R.color.tetris_navy, null))
circleRadius = 4f
setDrawValues(false)
highLightColor = Color.rgb(244, 117, 117)
}
val lineData = LineData(dataSet)
binding.chartProgress.data = lineData
// Format X-axis labels (dates)
val dateFormat = SimpleDateFormat("MM/dd", Locale.getDefault())
binding.chartProgress.xAxis.valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
val index = value.toInt()
return if (index >= 0 && index < dates.size) {
dateFormat.format(Date(dates[index]))
} else {
""
}
}
}
binding.chartProgress.invalidate()
}
private fun observeStats() {
viewModel.filteredScores.observe(viewLifecycleOwner) { scores ->
@ -134,24 +65,6 @@ class StatsFragment : Fragment() {
viewModel.highScore.observe(viewLifecycleOwner) { highScore ->
binding.textViewHighScore.text = "%,d".format(highScore)
}
viewModel.scoresByDate.observe(viewLifecycleOwner) { scores ->
// Convert scores to entries for the chart
if (scores.isNotEmpty()) {
val entries = mutableListOf<Entry>()
val dates = mutableListOf<Long>()
scores.forEachIndexed { index, score ->
entries.add(Entry(index.toFloat(), score.scoreValue.toFloat()))
dates.add(score.dateRecorded)
}
updateProgressChart(entries, dates)
} else {
binding.chartProgress.clear()
binding.chartProgress.invalidate()
}
}
}
override fun onDestroyView() {

View file

@ -18,10 +18,6 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) {
scoreDao.getScoresForGame(game)
}
val scoresByDate: LiveData<List<Score>> = _selectedGame.switchMap { game ->
scoreDao.getScoresForGameByDate(game)
}
val averageScore: LiveData<Double> = _selectedGame.switchMap { game ->
scoreDao.getAverageScore(game)
}

View file

@ -31,33 +31,6 @@
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Progress Chart"
android:textAppearance="?attr/textAppearanceSubtitle1" />
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/chartProgress"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewScores"
android:layout_width="match_parent"