diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bfe3c94..b46fc77 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -83,16 +83,10 @@ 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")
ksp("androidx.room:room-compiler:2.6.1")
-
- // Gson for JSON serialization
- implementation("com.google.code.gson:gson:2.10.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 820ecc2..5664823 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -32,11 +32,6 @@
-
-
> = loadLearnedFactors()
-
- /**
- * Updates the scaling factor based on a new sample provided by the user
- * @param fromGame The source game
- * @param toGame The target game
- * @param fromScore The original score
- * @param toScore The user-provided equivalent score
- */
- suspend fun updateScalingFactor(fromGame: String, toGame: String, fromScore: Int, toScore: Int) {
- withContext(Dispatchers.IO) {
- // Calculate the actual scaling factor from this sample
- val actualFactor = toScore.toDouble() / fromScore.toDouble()
-
- // Get or create learning data for this game conversion
- val gameFactors = learnedFactors.getOrPut(fromGame) { mutableMapOf() }
- val learningData = gameFactors.getOrPut(toGame) { LearningData() }
-
- // Determine which bucket this score falls into
- val bucket = when {
- fromScore < 100000 -> ScoreBucket.LOW
- fromScore < 500000 -> ScoreBucket.MID
- else -> ScoreBucket.HIGH
- }
-
- // Update the appropriate bucket
- when (bucket) {
- ScoreBucket.LOW -> {
- learningData.lowScoreSamples++
- learningData.lowScoreTotal += actualFactor
- }
- ScoreBucket.MID -> {
- learningData.midScoreSamples++
- learningData.midScoreTotal += actualFactor
- }
- ScoreBucket.HIGH -> {
- learningData.highScoreSamples++
- learningData.highScoreTotal += actualFactor
- }
- }
-
- // Save the updated data
- saveLearnedFactors()
- }
- }
-
- /**
- * Gets the learned scaling factor for a conversion, falling back to default if no samples
- */
- fun getLearnedScalingFactor(fromGame: String, toGame: String, score: Int): Double {
- // Check if we have learned data for this conversion
- val learningData = learnedFactors[fromGame]?.get(toGame)
-
- // If no learning data, fall back to default
- if (learningData == null) {
- return ScalingFactors.getScalingFactor(fromGame, toGame, score)
- }
-
- // Determine which bucket this score falls into
- return when {
- score < 100000 -> {
- if (learningData.lowScoreSamples > 0) {
- learningData.lowScoreTotal / learningData.lowScoreSamples
- } else {
- ScalingFactors.getScalingFactor(fromGame, toGame, score)
- }
- }
- score < 500000 -> {
- if (learningData.midScoreSamples > 0) {
- learningData.midScoreTotal / learningData.midScoreSamples
- } else {
- ScalingFactors.getScalingFactor(fromGame, toGame, score)
- }
- }
- else -> {
- if (learningData.highScoreSamples > 0) {
- learningData.highScoreTotal / learningData.highScoreSamples
- } else {
- ScalingFactors.getScalingFactor(fromGame, toGame, score)
- }
- }
- }
- }
-
- /**
- * Gets the number of samples collected for this game conversion
- */
- fun getSampleCount(fromGame: String, toGame: String): Int {
- val learningData = learnedFactors[fromGame]?.get(toGame) ?: return 0
- return learningData.lowScoreSamples + learningData.midScoreSamples + learningData.highScoreSamples
- }
-
- /**
- * Resets all learned scaling factors
- */
- suspend fun resetAllFactors() {
- withContext(Dispatchers.IO) {
- learnedFactors.clear()
- saveLearnedFactors()
- }
- }
-
- /**
- * Resets learned scaling factors for a specific game conversion
- */
- suspend fun resetFactors(fromGame: String, toGame: String) {
- withContext(Dispatchers.IO) {
- learnedFactors[fromGame]?.remove(toGame)
- saveLearnedFactors()
- }
- }
-
- private fun loadLearnedFactors(): MutableMap> {
- val json = prefs.getString(KEY_LEARNED_FACTORS, null) ?: return mutableMapOf()
- val type = object : TypeToken>>() {}.type
- return try {
- gson.fromJson(json, type)
- } catch (e: Exception) {
- mutableMapOf()
- }
- }
-
- private fun saveLearnedFactors() {
- val json = gson.toJson(learnedFactors)
- prefs.edit().putString(KEY_LEARNED_FACTORS, json).apply()
- }
-
- /**
- * Data class to track learning progress for a specific game conversion
- */
- data class LearningData(
- var lowScoreSamples: Int = 0,
- var lowScoreTotal: Double = 0.0,
- var midScoreSamples: Int = 0,
- var midScoreTotal: Double = 0.0,
- var highScoreSamples: Int = 0,
- var highScoreTotal: Double = 0.0
- )
-
- enum class ScoreBucket {
- LOW, MID, HIGH
- }
-
- companion object {
- private const val PREFS_NAME = "ScalingFactorsPrefs"
- private const val KEY_LEARNED_FACTORS = "learned_factors"
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt b/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt
index ecc25c0..de27901 100644
--- a/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt
+++ b/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt
@@ -12,15 +12,9 @@ interface ScoreDao {
@Query("SELECT * FROM scores WHERE gameVersion = :gameVersion")
fun getScoresForGame(gameVersion: String): LiveData>
- @Query("SELECT * FROM scores WHERE gameVersion = :gameVersion ORDER BY dateRecorded ASC")
- fun getScoresForGameByDate(gameVersion: String): LiveData>
-
@Query("SELECT DISTINCT gameVersion FROM scores")
fun getGamesWithScores(): LiveData>
- @Query("SELECT COUNT(*) FROM scores")
- fun getTotalScoreCount(): LiveData
-
@Query("SELECT AVG(scoreValue) FROM scores WHERE gameVersion = :gameVersion")
fun getAverageScore(gameVersion: String): LiveData
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/ScalingFactorTestActivity.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/ScalingFactorTestActivity.kt
deleted file mode 100644
index 981323c..0000000
--- a/app/src/main/java/com/accidentalproductions/tetristats/ui/ScalingFactorTestActivity.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.accidentalproductions.tetristats.ui
-
-import android.os.Bundle
-import android.widget.ArrayAdapter
-import androidx.appcompat.app.AppCompatActivity
-import com.accidentalproductions.tetristats.databinding.ActivityScalingFactorTestBinding
-import com.accidentalproductions.tetristats.util.GameScoreSample
-import com.accidentalproductions.tetristats.util.ScalingFactorAnalyzer
-
-class ScalingFactorTestActivity : AppCompatActivity() {
- private lateinit var binding: ActivityScalingFactorTestBinding
- private val analyzer = ScalingFactorAnalyzer()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityScalingFactorTestBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- setupGameDropdown()
- setupSkillLevelDropdown()
- setupButtons()
- }
-
- private fun setupGameDropdown() {
- val games = listOf(
- "NES Tetris",
- "Game Boy Tetris",
- "Tetris DX",
- "Tetris DS",
- "Tetris Effect",
- "Rosy Retrospection DX",
- "Apotris",
- "Modretro Tetris",
- "Tetris Mobile"
- )
- val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, games)
- binding.spinnerGame.setAdapter(adapter)
- }
-
- private fun setupSkillLevelDropdown() {
- val skillLevels = listOf("beginner", "intermediate", "advanced")
- val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, skillLevels)
- binding.spinnerSkillLevel.setAdapter(adapter)
- }
-
- private fun setupButtons() {
- binding.buttonAddSample.setOnClickListener {
- val game = binding.spinnerGame.text.toString()
- val score = binding.editTextScore.text.toString().toIntOrNull()
- val level = binding.editTextLevel.text.toString().toIntOrNull()
- val skillLevel = binding.spinnerSkillLevel.text.toString()
- val notes = binding.editTextNotes.text.toString()
-
- if (score != null && level != null) {
- val sample = GameScoreSample(game, score, level, skillLevel, notes)
- analyzer.addSample(sample)
- clearInputs()
- updateSampleCount()
- }
- }
-
- binding.buttonAnalyze.setOnClickListener {
- analyzer.printAnalysisReport()
- binding.textViewReport.text = analyzer.generateScalingFactorCode()
- }
-
- binding.buttonClear.setOnClickListener {
- analyzer.clearSamples()
- updateSampleCount()
- binding.textViewReport.text = ""
- }
- }
-
- private fun updateSampleCount() {
- binding.textViewSampleCount.text = "Samples: ${analyzer.sampleCount}"
- }
-
- private fun clearInputs() {
- binding.editTextScore.text?.clear()
- binding.editTextLevel.text?.clear()
- binding.editTextNotes.text?.clear()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/analysis/ScalingFactorFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/analysis/ScalingFactorFragment.kt
deleted file mode 100644
index 85cc5e4..0000000
--- a/app/src/main/java/com/accidentalproductions/tetristats/ui/analysis/ScalingFactorFragment.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-package com.accidentalproductions.tetristats.ui.analysis
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ArrayAdapter
-import androidx.fragment.app.Fragment
-import com.accidentalproductions.tetristats.databinding.FragmentScalingFactorBinding
-import com.accidentalproductions.tetristats.util.GameScoreSample
-import com.accidentalproductions.tetristats.util.ScalingFactorAnalyzer
-
-class ScalingFactorFragment : Fragment() {
- private var _binding: FragmentScalingFactorBinding? = null
- private val binding get() = _binding!!
- private val analyzer = ScalingFactorAnalyzer()
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- _binding = FragmentScalingFactorBinding.inflate(inflater, container, false)
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- setupGameDropdown()
- setupSkillLevelDropdown()
- setupButtons()
- }
-
- private fun setupGameDropdown() {
- val games = listOf(
- "NES Tetris",
- "Game Boy Tetris",
- "Tetris DX",
- "Tetris DS",
- "Tetris Effect",
- "Rosy Retrospection DX",
- "Apotris",
- "Modretro Tetris",
- "Tetris Mobile"
- )
- val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
- binding.spinnerGame.setAdapter(adapter)
- }
-
- private fun setupSkillLevelDropdown() {
- val skillLevels = listOf("beginner", "intermediate", "advanced")
- val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, skillLevels)
- binding.spinnerSkillLevel.setAdapter(adapter)
- }
-
- private fun setupButtons() {
- binding.buttonAddSample.setOnClickListener {
- val game = binding.spinnerGame.text.toString()
- val score = binding.editTextScore.text.toString().toIntOrNull()
- val level = binding.editTextLevel.text.toString().toIntOrNull()
- val skillLevel = binding.spinnerSkillLevel.text.toString()
- val notes = binding.editTextNotes.text.toString()
-
- if (score != null && level != null) {
- val sample = GameScoreSample(game, score, level, skillLevel, notes)
- analyzer.addSample(sample)
- clearInputs()
- updateSampleCount()
- }
- }
-
- binding.buttonAnalyze.setOnClickListener {
- analyzer.printAnalysisReport()
- binding.textViewReport.text = analyzer.generateScalingFactorCode()
- }
-
- binding.buttonClear.setOnClickListener {
- analyzer.clearSamples()
- updateSampleCount()
- binding.textViewReport.text = ""
- }
- }
-
- private fun updateSampleCount() {
- binding.textViewSampleCount.text = "Samples: ${analyzer.sampleCount}"
- }
-
- private fun clearInputs() {
- binding.editTextScore.text?.clear()
- binding.editTextLevel.text?.clear()
- binding.editTextNotes.text?.clear()
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryFragment.kt
index deb4109..f39d338 100644
--- a/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryFragment.kt
+++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryFragment.kt
@@ -5,10 +5,8 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
-import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
-import androidx.recyclerview.widget.LinearLayoutManager
import com.accidentalproductions.tetristats.data.Score
import com.accidentalproductions.tetristats.databinding.FragmentEntryBinding
@@ -16,10 +14,6 @@ class EntryFragment : Fragment() {
private var _binding: FragmentEntryBinding? = null
private val binding get() = _binding!!
private val viewModel: EntryViewModel by viewModels { EntryViewModelFactory(requireActivity().application) }
- private lateinit var equivalentScoreAdapter: EquivalentScoreAdapter
-
- // Flag to track if we already showed the requirements toast
- private var hasShownRequirementsToast = false
override fun onCreateView(
inflater: LayoutInflater,
@@ -34,74 +28,8 @@ class EntryFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
setupGameVersionDropdown()
- setupRecyclerView()
+ setupScoreConverter()
setupSubmitButton()
- setupAutoAnalysis()
- }
-
- override fun onResume() {
- super.onResume()
- // Refresh conversions when returning to this fragment
- refreshConversions()
- }
-
- /**
- * Force refresh the conversions using the last submitted values
- */
- private fun refreshConversions() {
- if (viewModel.showConversion.value == true) {
- // If we have last submitted values, regenerate conversions
- val game = viewModel.lastSubmittedGame.value
- val score = viewModel.lastSubmittedScore.value
-
- 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
- }
- }
}
private fun setupGameVersionDropdown() {
@@ -112,85 +40,64 @@ 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)
}
-
- private fun setupRecyclerView() {
- equivalentScoreAdapter = EquivalentScoreAdapter()
- binding.recyclerViewEquivalentScores.apply {
- adapter = equivalentScoreAdapter
- layoutManager = LinearLayoutManager(context)
+
+ private fun setupScoreConverter() {
+ // Setup "From Game" dropdown with games that have scores
+ viewModel.gamesWithScores.observe(viewLifecycleOwner) { games ->
+ val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
+ binding.autoCompleteFromGame.setAdapter(adapter)
+ }
+
+ // Update score selection when game is selected
+ binding.autoCompleteFromGame.setOnItemClickListener { _, _, _, _ ->
+ val selectedGame = binding.autoCompleteFromGame.text.toString()
+ viewModel.setSelectedFromGame(selectedGame)
+ updateScoreDropdown(selectedGame)
+ }
+
+ // Setup "To Game" dropdown
+ val allGames = listOf(
+ "NES Tetris",
+ "Game Boy Tetris",
+ "Tetris DX",
+ "Tetris DS",
+ "Tetris Effect",
+ "Rosy Retrospection DX",
+ "Apotris"
+ )
+ val toGameAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, allGames)
+ binding.autoCompleteToGame.setAdapter(toGameAdapter)
+
+ // Handle score conversion
+ binding.buttonConvert.setOnClickListener {
+ viewModel.convertScore()
+ }
+
+ // Observe converted score
+ viewModel.convertedScore.observe(viewLifecycleOwner) { score ->
+ binding.cardConvertedScore.visibility = View.VISIBLE
+ binding.textViewConvertedScore.text = "%,d".format(score)
+ }
+
+ // Update selected games
+ binding.autoCompleteToGame.setOnItemClickListener { _, _, _, _ ->
+ viewModel.setSelectedToGame(binding.autoCompleteToGame.text.toString())
}
}
-
- private fun setupAutoAnalysis() {
- // Hide the analysis card by default
- binding.cardAnalysisResults.visibility = View.GONE
-
- // Observe if we should show conversions
- 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
- }
- }
-
- // Only setup equivalence UI when we have scores
- 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()
- }
- }
-
- // Update selected game
- binding.autoCompleteEquivalentGame.setOnItemClickListener { _, _, _, _ ->
- val selectedGame = binding.autoCompleteEquivalentGame.text.toString()
- viewModel.setSelectedEquivalentGame(selectedGame)
- }
-
- // Handle adding equivalent scores
- binding.buttonAddEquivalent.setOnClickListener {
- val equivalentScore = binding.editTextEquivalentScore.text.toString().toIntOrNull()
- if (equivalentScore != null) {
- viewModel.addEquivalentScore(equivalentScore)
- binding.editTextEquivalentScore.text?.clear()
- Toast.makeText(context, "Equivalent score added! The converter is learning.", Toast.LENGTH_SHORT).show()
- } else {
- Toast.makeText(context, "Please enter a valid score", Toast.LENGTH_SHORT).show()
- }
- }
-
- // 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()
- }
-
- // 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
- binding.cardAnalysisResults.visibility = View.GONE
+
+ private fun updateScoreDropdown(gameVersion: String) {
+ viewModel.getScoresForGame(gameVersion).observe(viewLifecycleOwner) { scores ->
+ val scoreStrings = scores.map { "${it.scoreValue} (Level ${it.endLevel ?: "?"})"}
+ val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, scoreStrings)
+ binding.spinnerScoreSelect.setAdapter(adapter)
+
+ binding.spinnerScoreSelect.setOnItemClickListener { _, _, position, _ ->
+ viewModel.setSelectedScore(scores[position])
}
}
}
@@ -212,24 +119,6 @@ class EntryFragment : Fragment() {
linesCleared = linesCleared
)
clearInputs()
-
- // Force immediate refresh of conversions
- if (viewModel.showConversion.value == false) {
- Toast.makeText(
- context,
- "Enter at least 3 scores across 2 different games to see conversions",
- Toast.LENGTH_LONG
- ).show()
- } else {
- refreshConversions()
-
- // Scroll down to show the analysis results
- binding.root.post {
- binding.root.fullScroll(View.FOCUS_DOWN)
- }
- }
- } else {
- Toast.makeText(context, "Please enter a game and score", Toast.LENGTH_SHORT).show()
}
}
}
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryViewModel.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryViewModel.kt
index 62af537..2d1bcae 100644
--- a/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryViewModel.kt
+++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryViewModel.kt
@@ -1,94 +1,49 @@
package com.accidentalproductions.tetristats.ui.entry
import android.app.Application
-import android.util.Log
import androidx.lifecycle.*
-import com.accidentalproductions.tetristats.TetriStatsApplication
import com.accidentalproductions.tetristats.data.Score
import com.accidentalproductions.tetristats.data.ScoreDatabase
import com.accidentalproductions.tetristats.data.ScalingFactors
-import com.accidentalproductions.tetristats.data.ScalingFactorsManager
import kotlinx.coroutines.launch
class EntryViewModel(application: Application) : AndroidViewModel(application) {
private val database = ScoreDatabase.getDatabase(application)
private val scoreDao = database.scoreDao()
- private val scalingFactorsManager = (application as TetriStatsApplication).scalingFactorsManager
- // All games list for reference
- private val allGames = listOf(
- "NES Tetris",
- "Game Boy Tetris",
- "Tetris DX",
- "Tetris DS",
- "Tetris Effect",
- "Rosy Retrospection DX",
- "Apotris",
- "Modretro Tetris",
- "Tetris Mobile"
- )
-
- // Track user played games and score counts
val gamesWithScores = scoreDao.getGamesWithScores()
- val totalScoreCount = scoreDao.getTotalScoreCount()
- // For auto-analysis
- private val _lastSubmittedGame = MutableLiveData()
- private val _lastSubmittedScore = MutableLiveData()
- private val _equivalentScores = MutableLiveData>()
- private val _showConversion = MutableLiveData(false)
-
- // Current game selection for learning
- private val _selectedEquivalentGame = MutableLiveData()
-
- val equivalentScores: LiveData> = _equivalentScores
- val lastSubmittedGame: LiveData = _lastSubmittedGame
- val lastSubmittedScore: LiveData = _lastSubmittedScore
- val showConversion: LiveData = _showConversion
-
- init {
- // Set up observers to update conversion criteria whenever relevant data changes
- gamesWithScores.observeForever {
- checkConversionCriteria()
- }
-
- totalScoreCount.observeForever {
- checkConversionCriteria()
- }
- }
+ private val _selectedFromGame = MutableLiveData()
+ private val _selectedScore = MutableLiveData()
+ private val _selectedToGame = MutableLiveData()
+ private val _convertedScore = MutableLiveData()
+
+ val convertedScore: LiveData = _convertedScore
fun getScoresForGame(gameVersion: String): LiveData> {
return scoreDao.getScoresForGame(gameVersion)
}
-
- fun setSelectedEquivalentGame(game: String) {
- _selectedEquivalentGame.value = game
+
+ fun setSelectedFromGame(game: String) {
+ _selectedFromGame.value = game
}
- /**
- * Check if we should show conversions based on score count and game count
- */
- fun checkConversionCriteria() {
- val scoreCount = totalScoreCount.value ?: 0
- val gameCount = gamesWithScores.value?.size ?: 0
-
- // Only show conversions if there are at least 3 scores across at least 2 games
- val shouldShow = scoreCount >= 3 && gameCount >= 2
-
- // For debugging
- Log.d("TetriStats", "Checking conversion criteria: scores=$scoreCount, games=$gameCount, shouldShow=$shouldShow")
-
- _showConversion.postValue(shouldShow)
+ fun setSelectedScore(score: Score) {
+ _selectedScore.value = score
}
-
- /**
- * Force refresh the equivalent scores - use this to ensure UI has latest values
- */
- fun refreshEquivalentScores(fromGame: String, score: Int) {
- // Only refresh if conversions should be showing
- if (_showConversion.value == true) {
- Log.d("TetriStats", "Refreshing equivalent scores for $fromGame score $score")
- generateEquivalentScores(fromGame, score)
+
+ fun setSelectedToGame(game: String) {
+ _selectedToGame.value = game
+ }
+
+ fun convertScore() {
+ val fromGame = _selectedFromGame.value
+ val score = _selectedScore.value
+ val toGame = _selectedToGame.value
+
+ if (fromGame != null && score != null && toGame != null) {
+ val convertedScore = ScalingFactors.convertScore(fromGame, toGame, score.scoreValue)
+ _convertedScore.value = convertedScore
}
}
@@ -108,75 +63,8 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
linesCleared = linesCleared
)
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
-
- // Immediately check conversion criteria with current values
- checkConversionCriteria()
-
- // Immediate refresh regardless if we just reached the criteria threshold
- if (totalScoreCount.value ?: 0 >= 3 && (gamesWithScores.value?.size ?: 0) >= 2) {
- generateEquivalentScores(gameVersion, score)
- }
}
}
-
- /**
- * Generates equivalent scores in played games based on the submitted score
- */
- private fun generateEquivalentScores(fromGame: String, score: Int) {
- val playedGames = gamesWithScores.value ?: listOf()
- val equivalents = mutableListOf()
-
- // Generate equivalent scores for played games except the source game
- for (game in playedGames) {
- if (game != fromGame) {
- // Get the learned scaling factor
- val factor = scalingFactorsManager.getLearnedScalingFactor(fromGame, game, score)
- val equivalentScore = (score * factor).toInt()
- val sampleCount = scalingFactorsManager.getSampleCount(fromGame, game)
-
- equivalents.add(
- EquivalentScore(
- gameName = game,
- score = equivalentScore,
- sampleCount = sampleCount,
- usesDynamicFactor = sampleCount > 0
- )
- )
- }
- }
-
- // Use setValue for immediate update on main thread rather than postValue
- _equivalentScores.value = equivalents
- }
-
- /**
- * Add a learning sample with an equivalent score in another game
- */
- fun addEquivalentScore(equivalentScore: Int) {
- val fromGame = _lastSubmittedGame.value
- val originalScore = _lastSubmittedScore.value
- val toGame = _selectedEquivalentGame.value
-
- if (fromGame != null && originalScore != null && toGame != null) {
- viewModelScope.launch {
- scalingFactorsManager.updateScalingFactor(fromGame, toGame, originalScore, equivalentScore)
-
- // Regenerate the equivalent scores to update the UI
- generateEquivalentScores(fromGame, originalScore)
- }
- }
- }
-
- override fun onCleared() {
- super.onCleared()
- // Remove our observers to prevent leaks
- gamesWithScores.removeObserver { checkConversionCriteria() }
- totalScoreCount.removeObserver { checkConversionCriteria() }
- }
}
class EntryViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EquivalentScoreAdapter.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EquivalentScoreAdapter.kt
deleted file mode 100644
index aa0d780..0000000
--- a/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EquivalentScoreAdapter.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.accidentalproductions.tetristats.ui.entry
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.accidentalproductions.tetristats.databinding.ItemEquivalentScoreBinding
-
-class EquivalentScoreAdapter : ListAdapter(EquivalentScoreDiffCallback()) {
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- val binding = ItemEquivalentScoreBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- return ViewHolder(binding)
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bind(getItem(position))
- }
-
- class ViewHolder(private val binding: ItemEquivalentScoreBinding) : RecyclerView.ViewHolder(binding.root) {
- fun bind(item: EquivalentScore) {
- binding.textViewGameName.text = item.gameName
- binding.textViewEquivalentScore.text = "%,d".format(item.score)
-
- // Show sample count if any samples exist
- if (item.sampleCount > 0) {
- binding.textViewSampleCount.visibility = android.view.View.VISIBLE
- binding.textViewSampleCount.text = "Based on ${item.sampleCount} learning ${if (item.sampleCount == 1) "sample" else "samples"}"
- } else {
- binding.textViewSampleCount.visibility = android.view.View.GONE
- }
- }
- }
-}
-
-class EquivalentScoreDiffCallback : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: EquivalentScore, newItem: EquivalentScore): Boolean {
- return oldItem.gameName == newItem.gameName
- }
-
- override fun areContentsTheSame(oldItem: EquivalentScore, newItem: EquivalentScore): Boolean {
- return oldItem == newItem
- }
-}
-
-/**
- * Data class representing an equivalent score in another game
- */
-data class EquivalentScore(
- val gameName: String,
- val score: Int,
- val sampleCount: Int = 0,
- val usesDynamicFactor: Boolean = false
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeFragment.kt
index e01b434..34a7699 100644
--- a/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeFragment.kt
+++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeFragment.kt
@@ -1,6 +1,5 @@
package com.accidentalproductions.tetristats.ui.home
-import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -8,9 +7,7 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
-import com.accidentalproductions.tetristats.R
import com.accidentalproductions.tetristats.databinding.FragmentHomeBinding
-import com.accidentalproductions.tetristats.ui.ScalingFactorTestActivity
class HomeFragment : Fragment() {
@@ -35,11 +32,6 @@ class HomeFragment : Fragment() {
homeViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
-
- binding.buttonScalingAnalysis.setOnClickListener {
- startActivity(Intent(requireContext(), ScalingFactorTestActivity::class.java))
- }
-
return root
}
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt
index 94a7097..520981c 100644
--- a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt
+++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt
@@ -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, dates: List) {
- 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()
- val dates = mutableListOf()
-
- 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() {
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt
index c656adb..dbd3bd7 100644
--- a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt
+++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt
@@ -18,10 +18,6 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) {
scoreDao.getScoresForGame(game)
}
- val scoresByDate: LiveData> = _selectedGame.switchMap { game ->
- scoreDao.getScoresForGameByDate(game)
- }
-
val averageScore: LiveData = _selectedGame.switchMap { game ->
scoreDao.getAverageScore(game)
}
diff --git a/app/src/main/java/com/accidentalproductions/tetristats/util/ScalingFactorAnalyzer.kt b/app/src/main/java/com/accidentalproductions/tetristats/util/ScalingFactorAnalyzer.kt
deleted file mode 100644
index dad0a02..0000000
--- a/app/src/main/java/com/accidentalproductions/tetristats/util/ScalingFactorAnalyzer.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-package com.accidentalproductions.tetristats.util
-
-import com.accidentalproductions.tetristats.data.RangeScalingFactor
-
-data class GameScoreSample(
- val game: String,
- val score: Int,
- val level: Int,
- val skillLevel: String, // "beginner", "intermediate", "advanced"
- val notes: String // Any special conditions or mechanics used
-)
-
-class ScalingFactorAnalyzer {
- private val samples = mutableListOf()
- private val baseGame = "NES Tetris"
-
- val sampleCount: Int
- get() = samples.size
-
- fun addSample(sample: GameScoreSample) {
- samples.add(sample)
- }
-
- fun addSamples(newSamples: List) {
- samples.addAll(newSamples)
- }
-
- fun clearSamples() {
- samples.clear()
- }
-
- fun analyzeScoringCurves(): Map {
- val groupedSamples = samples.groupBy { it.game }
- val conversionFactors = mutableMapOf()
-
- groupedSamples.forEach { (game, scores) ->
- if (game != baseGame) {
- val lowScores = scores.filter { it.score < 100000 }
- val midScores = scores.filter { it.score in 100000..500000 }
- val highScores = scores.filter { it.score > 500000 }
-
- val lowFactor = calculateAverageFactor(lowScores, baseGame)
- val midFactor = calculateAverageFactor(midScores, baseGame)
- val highFactor = calculateAverageFactor(highScores, baseGame)
-
- conversionFactors[game] = RangeScalingFactor(lowFactor, midFactor, highFactor)
- }
- }
-
- return conversionFactors
- }
-
- private fun calculateAverageFactor(samples: List, baseGame: String): Double {
- if (samples.isEmpty()) return 1.0
-
- val baseGameSamples = this.samples.filter { it.game == baseGame }
- if (baseGameSamples.isEmpty()) return 1.0
-
- // Find matching base game samples by skill level
- val factors = samples.map { sample ->
- val matchingBaseSamples = baseGameSamples.filter {
- it.skillLevel == sample.skillLevel &&
- it.level == sample.level
- }
-
- if (matchingBaseSamples.isNotEmpty()) {
- matchingBaseSamples.map { it.score.toDouble() / sample.score }
- } else {
- // If no exact match, find closest level
- val closestBaseSample = baseGameSamples.minByOrNull {
- kotlin.math.abs(it.level - sample.level)
- }
- if (closestBaseSample != null) {
- listOf(closestBaseSample.score.toDouble() / sample.score)
- } else {
- emptyList()
- }
- }
- }.flatten()
-
- return if (factors.isNotEmpty()) {
- factors.average()
- } else {
- 1.0
- }
- }
-
- fun generateScalingFactorCode(): String {
- val factors = analyzeScoringCurves()
- val code = StringBuilder()
-
- code.appendLine("val FACTORS = mapOf(")
-
- // Add base game entries
- val games = samples.map { it.game }.distinct()
- games.forEach { game ->
- code.appendLine(" \"$game\" to mapOf(")
-
- // Only add factors for games that aren't the current game
- games.filter { it != game }.forEach { otherGame ->
- val factor = if (game == baseGame) {
- // If this is the base game, use the inverse of the other game's factor
- factors[otherGame]?.let { RangeScalingFactor(1.0/it.low, 1.0/it.mid, 1.0/it.high) }
- ?: RangeScalingFactor(1.0, 1.0, 1.0)
- } else {
- // If this isn't the base game, use the factor relative to the base game
- if (otherGame == baseGame) {
- factors[game] ?: RangeScalingFactor(1.0, 1.0, 1.0)
- } else {
- // For non-base game pairs, calculate the relative factor
- val baseFactor = factors[game] ?: RangeScalingFactor(1.0, 1.0, 1.0)
- val otherFactor = factors[otherGame] ?: RangeScalingFactor(1.0, 1.0, 1.0)
- RangeScalingFactor(
- baseFactor.low / otherFactor.low,
- baseFactor.mid / otherFactor.mid,
- baseFactor.high / otherFactor.high
- )
- }
- }
- code.appendLine(" \"$otherGame\" to RangeScalingFactor(${factor.low}, ${factor.mid}, ${factor.high}),")
- }
-
- code.appendLine(" ),")
- }
-
- code.appendLine(")")
- return code.toString()
- }
-
- fun validateConversion(fromGame: String, toGame: String, score: Int): Double {
- val factor = analyzeScoringCurves()[toGame] ?: return 1.0
- return when {
- score < 100000 -> factor.low
- score < 500000 -> factor.mid
- else -> factor.high
- }
- }
-
- fun printAnalysisReport() {
- println("=== Scaling Factor Analysis Report ===")
- println("Base Game: $baseGame")
- println("Total Samples: ${samples.size}")
- println("\nSamples per Game:")
- samples.groupBy { it.game }.forEach { (game, samples) ->
- println("$game: ${samples.size} samples")
- println(" Skill Levels: ${samples.map { it.skillLevel }.distinct()}")
- println(" Level Range: ${samples.minOf { it.level }} - ${samples.maxOf { it.level }}")
- println(" Score Range: ${samples.minOf { it.score }} - ${samples.maxOf { it.score }}")
- }
-
- println("\nCalculated Scaling Factors:")
- analyzeScoringCurves().forEach { (game, factor) ->
- println("$game:")
- println(" Low (<100k): ${factor.low}")
- println(" Mid (100k-500k): ${factor.mid}")
- println(" High (>500k): ${factor.high}")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_analytics_24.xml b/app/src/main/res/drawable/ic_analytics_24.xml
deleted file mode 100644
index c523f42..0000000
--- a/app/src/main/res/drawable/ic_analytics_24.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_scaling_factor_test.xml b/app/src/main/res/layout/activity_scaling_factor_test.xml
deleted file mode 100644
index 517160b..0000000
--- a/app/src/main/res/layout/activity_scaling_factor_test.xml
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_entry.xml b/app/src/main/res/layout/fragment_entry.xml
index ec2e0ea..f98930d 100644
--- a/app/src/main/res/layout/fragment_entry.xml
+++ b/app/src/main/res/layout/fragment_entry.xml
@@ -112,12 +112,10 @@
-
+
-
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
-
-
-
-
-
-
-
-
-
-
+ android:hint="From Game"
+ android:inputType="none"/>
+
-
-
+
-
+ android:hint="Select Score"
+ android:inputType="none"/>
+
-
-
-
+
+
+
+
+ android:text="Convert Score"
+ android:layout_marginBottom="16dp"/>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 4dad34c..f3d9b08 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -15,18 +15,8 @@
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
+ app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_scaling_factor.xml b/app/src/main/res/layout/fragment_scaling_factor.xml
deleted file mode 100644
index b8e7aab..0000000
--- a/app/src/main/res/layout/fragment_scaling_factor.xml
+++ /dev/null
@@ -1,161 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_stats.xml b/app/src/main/res/layout/fragment_stats.xml
index 2cf4a0b..426a01d 100644
--- a/app/src/main/res/layout/fragment_stats.xml
+++ b/app/src/main/res/layout/fragment_stats.xml
@@ -31,33 +31,6 @@
android:inputType="none"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
index c0de471..d87901b 100644
--- a/app/src/main/res/navigation/mobile_navigation.xml
+++ b/app/src/main/res/navigation/mobile_navigation.xml
@@ -22,5 +22,4 @@
android:name="com.accidentalproductions.tetristats.ui.stats.StatsFragment"
android:label="Statistics"
tools:layout="@layout/fragment_stats" />
-
\ No newline at end of file